Bang/output.md

7.2 KiB

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

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

import { bangs } from "./bang";
import "./global.css";

function noSearchDefaultPageRender() {
  const app = document.querySelector<HTMLDivElement>("#app")!;
  app.innerHTML = `
    <main class="main-container">
      <div class="content-container">
        <h1 class="bang-title">💣 Bang!</h1>
        <p>
          Add the following URL as a custom search engine to your browser in order to enable
          <a href="https://duckduckgo.com/bang.html" target="_blank">
            all of DuckDuckGo's bangs
          </a>
          right from your browser's search bar!
        </p>
        <div class="url-container"> 
          <input 
            type="text" 
            class="url-input"
            value="${import.meta.env.VITE_BANG_URL}?q=%s"
            readonly 
          />
          <button class="copy-button">
            <img src="/clipboard.svg" alt="Copy" />
          </button>
        </div>
        <h3 class="settings-title">How to add Bang! Search Engine</h3>
        <p style="margin-bottom: 8px; font-size: 14px;">
          Below are links to the search engine settings in your browser.
            <br />
          Copy the URL for the browser engine you are using & paste it into a new tab.
            <br />
          From there you should see an option to add a search engine. Copy the link above
          & fill out the form as follows.
        </p>
        <table class="form-table">
          <tr>
            <td><b>Name:</b></td>
            <td>Bang!</td>
          </tr>
          <tr>
            <td><b>Engine URL:</b></td>
            <td>${import.meta.env.VITE_BANG_URL}?q=%s</td>
          </tr>
          <tr>
            <td><b>Alias:</b></td>
            <td>@bang</td>
          </tr>
        </table>
        <div class="settings-links-container">
          <div class="browser-link-container">
            <img src="/firefox.svg" alt="Firefox" width="24" />
            <input
              type="text"
              readonly value="about:preferences#search" 
              class="firefox-textbox" 
              title="Click to copy Firefox settings URL"
            />
            <button class="copy-firefox">
              <img src="/clipboard.svg" alt="Copy" />
            </button>
          </div>
          <div class="browser-link-container">
            <img src="/chrome.svg" alt="Chrome" width="24" />
            <input
              type="text"
              readonly value="chrome://settings/searchEngines" 
              class="chrome-textbox"
              title="Click to copy Chrome settings URL"
            />
            <button class="copy-chrome">
              <img src="/clipboard.svg" alt="Copy" />
            </button>
          </div>
        </div>
      </div>
    </main>
  `;

  const copyButton = app.querySelector<HTMLButtonElement>(".copy-button")!;
  const copyIcon = copyButton.querySelector("img")!;
  const urlInput = app.querySelector<HTMLInputElement>(".url-input")!;
  const copyFirefox = app.querySelector<HTMLInputElement>(".copy-firefox")!;
  const copyFirefoxIcon = copyFirefox.querySelector("img")!;
  const firefoxInput =  app.querySelector<HTMLInputElement>(".firefox-textbox")!;
  const copyChrome = app.querySelector<HTMLInputElement>(".copy-chrome")!;
  const copyChromeIcon = copyChrome.querySelector("img")!;
  const chromeInput =  app.querySelector<HTMLInputElement>(".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

/// <reference types="vite/client" />

tsconfig.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

import { defineConfig } from "vite";
import { VitePWA } from "vite-plugin-pwa";

export default defineConfig({
  plugins: [
    VitePWA({
      registerType: "autoUpdate",
    }),
  ],
});