diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 2040d49..48420c0 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,24 +1,21 @@ -networks: - default: - name: ${NETWORK} - external: true - services: bang-web-server: - env_file: - - .env build: context: ../ dockerfile: docker/Dockerfile container_name: bang - domainname: ${DOMAIN} + hostname: bang.gib + domainname: bang.gbrown.org networks: - - default - hostname: bang - ports: - - ${PORT}:${PORT} + - nginx-bridge + #ports: + #- 5000:5000 tty: true restart: unless-stopped volumes: - ../:/app command: serve -s /app/dist -l 5000 + +networks: + nginx-bridge: + external: true diff --git a/output.md b/output.md new file mode 100644 index 0000000..9cf5081 --- /dev/null +++ b/output.md @@ -0,0 +1,280 @@ + +docker/.env +``` +PORT=5000 +DOMAIN=bang.gbrown.org +NETWORK=node_apps + +``` + +docker/Dockerfile +``` +# Stage 1: Build the project +FROM node:18 AS builder +WORKDIR /app + +# Copy package.json and pnpm-lock.yaml to the working directory +COPY package.json pnpm-lock.yaml ./ + +# Install dependencies +RUN npm install -g pnpm +RUN pnpm install + +# Copy project files into the docker image +COPY . . + +# Build the project +RUN pnpm run build + +# Stage 2: Serve the app using the same version of Node +FROM node:18-alpine +WORKDIR /app + +# Install a simple http server +RUN npm install -g serve + +# Copy built assets from the builder stage +COPY --from=builder /app/dist ./ + +# Expose port 5000 for the server +EXPOSE 5000 + +# Start the server using the `serve` package +CMD ["serve", "-s", ".", "-l", "5000"] + +``` + +docker/docker-compose.yml +```yml +services: + bang-web-server: + build: + context: ../ + dockerfile: docker/Dockerfile + container_name: bang + hostname: bang.gib + domainname: bang.gbrown.org + networks: + - nginx-bridge + #ports: + #- 5000:5000 + tty: true + restart: unless-stopped + volumes: + - ../:/app + command: serve -s /app/dist -l 5000 + +networks: + nginx-bridge: + external: true + +``` + +src/main.ts +```ts +import { bangs } from "./bang"; +import "./global.css"; + +function noSearchDefaultPageRender() { + const app = document.querySelector("#app")!; + app.innerHTML = ` +
+
+

💣 Bang!

+

+ Add the following URL as a custom search engine to your browser in order to enable + + all of DuckDuckGo's bangs + + right from your browser's search bar! +

+
+ + +
+

How to add Bang! Search Engine

+

+ Below are links to the search engine settings in your browser. +
+ Copy the URL for the browser engine you are using & paste it into a new tab. +
+ From there you should see an option to add a search engine. Copy the link above + & fill out the form as follows. +

+ + + + + + + + + + + + + +
Name:Bang!
Engine URL:${import.meta.env.VITE_BANG_URL}?q=%s
Alias:@bang
+ +
+
+ `; + + const copyButton = app.querySelector(".copy-button")!; + const copyIcon = copyButton.querySelector("img")!; + const urlInput = app.querySelector(".url-input")!; + const copyFirefox = app.querySelector(".copy-firefox")!; + const copyFirefoxIcon = copyFirefox.querySelector("img")!; + const firefoxInput = app.querySelector(".firefox-textbox")!; + const copyChrome = app.querySelector(".copy-chrome")!; + const copyChromeIcon = copyChrome.querySelector("img")!; + const chromeInput = app.querySelector(".chrome-textbox")!; + + copyButton.addEventListener("click", async () => { + await navigator.clipboard.writeText(urlInput.value); + copyIcon.src = "/clipboard-check.svg"; + setTimeout(() => { + copyIcon.src = "/clipboard.svg"; + }, 2000); + }); + + copyFirefox.addEventListener("click", async () => { + await navigator.clipboard.writeText(firefoxInput.value); + copyFirefoxIcon.src = "/clipboard-check.svg"; + setTimeout(() => { + copyFirefoxIcon.src = "/clipboard.svg"; + }, 2000); + }); + + copyChrome.addEventListener("click", async () => { + await navigator.clipboard.writeText(chromeInput.value); + copyChromeIcon.src = "/clipboard-check.svg"; + setTimeout(() => { + copyChromeIcon.src = "/clipboard.svg"; + }, 2000); + }); +} + +const envDefaultBang = import.meta.env.VITE_DEFAULT_BANG ?? "g"; +const LS_DEFAULT_BANG = localStorage.getItem("default-bang") ?? envDefaultBang; +const defaultBang = bangs.find((b) => b.t === LS_DEFAULT_BANG); + +function getBangredirectUrl() { + const url = new URL(window.location.href); + const query = url.searchParams.get("q")?.trim() ?? ""; + if (!query) { + noSearchDefaultPageRender(); + return null; + } + + const match = query.match(/!(\S+)/i); + + const bangCandidate = match?.[1]?.toLowerCase(); + const selectedBang = bangs.find((b) => b.t === bangCandidate) ?? defaultBang; + + // Remove the first bang from the query + const cleanQuery = query.replace(/!\S+\s*/i, "").trim(); + + // Format of the url is: + // https://www.google.com/search?q={{{s}}} + const searchUrl = selectedBang?.u.replace( + "{{{s}}}", + // Replace %2F with / to fix formats like "!ghr+t3dotgg/unduck" + encodeURIComponent(cleanQuery).replace(/%2F/g, "/") + ); + if (!searchUrl) return null; + + return searchUrl; +} + +function doRedirect() { + const searchUrl = getBangredirectUrl(); + if (!searchUrl) return; + window.location.replace(searchUrl); +} + +doRedirect(); + +``` + +src/vite-env.d.ts +```ts +/// + +``` + +tsconfig.json +```json +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} + +``` + +vite.config.ts +```ts +import { defineConfig } from "vite"; +import { VitePWA } from "vite-plugin-pwa"; + +export default defineConfig({ + plugins: [ + VitePWA({ + registerType: "autoUpdate", + }), + ], +}); + +``` diff --git a/scripts/files_to_clipboard b/scripts/files_to_clipboard new file mode 100755 index 0000000..7654166 --- /dev/null +++ b/scripts/files_to_clipboard @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 + +import os +import sys +import argparse +from pathlib import Path +import pyperclip +import questionary + +# List of directories to exclude +EXCLUDED_DIRS = {'node_modules', '.next', '.venv', '.git', '__pycache__', '.idea', '.vscode', 'ui'} + +def collect_files(project_path): + """ + Collects files from the project directory, excluding specified directories and filtering by extensions. + Returns a list of file paths relative to the project directory. + """ + collected_files = [] + + for root, dirs, files in os.walk(project_path): + # Exclude specified directories + dirs[:] = [d for d in dirs if d not in EXCLUDED_DIRS] + + for file in files: + file_path = Path(root) / file + relative_path = file_path.relative_to(project_path) + collected_files.append(relative_path) + + return collected_files + +def main(): + # Parse command-line arguments + parser = argparse.ArgumentParser(description='Generate Markdown from selected files.') + parser.add_argument('path', nargs='?', default='.', help='Path to the project directory') + args = parser.parse_args() + + project_path = Path(args.path).resolve() + if not project_path.is_dir(): + print(f"Error: '{project_path}' is not a directory.") + sys.exit(1) + + # Collect files from the project directory + file_list = collect_files(project_path) + + if not file_list: + print("No files found in the project directory with the specified extensions.") + sys.exit(1) + + # Sort file_list for better organization + file_list.sort() + + # Interactive file selection using questionary + print("\nSelect the files you want to include:") + selected_files = questionary.checkbox( + "Press space to select files, and Enter when you're done:", + choices=[str(f) for f in file_list] + ).ask() + + if not selected_files: + print("No files selected.") + sys.exit(1) + + # Generate markdown + markdown_lines = [] + markdown_lines.append('') + + for selected_file in selected_files: + file_path = project_path / selected_file + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + # Determine the language for code block from file extension + language = file_path.suffix.lstrip('.') + markdown_lines.append(f'{selected_file}') + markdown_lines.append(f'```{language}') + markdown_lines.append(content) + markdown_lines.append('```') + markdown_lines.append('') + except Exception as e: + print(f"Error reading file {selected_file}: {e}") + + markdown_text = '\n'.join(markdown_lines) + + # Write markdown to file + output_file = 'output.md' + with open(output_file, 'w', encoding='utf-8') as f: + f.write(markdown_text) + print(f"\nMarkdown file '{output_file}' has been generated.") + + # Copy markdown content to clipboard + pyperclip.copy(markdown_text) + print("Markdown content has been copied to the clipboard.") + +if __name__ == "__main__": + # Check if required libraries are installed + try: + import questionary + import pyperclip + except ImportError as e: + missing_module = e.name + print(f"Error: Missing required module '{missing_module}'.") + print(f"Please install it by running: pip install {missing_module}") + sys.exit(1) + + main()