summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore8
-rw-r--r--app/app.css15
-rw-r--r--app/lib/db.server.ts45
-rw-r--r--app/root.tsx76
-rw-r--r--app/routes.ts5
-rw-r--r--app/routes/home.tsx8
-rw-r--r--package.json33
-rw-r--r--react-router.config.ts5
-rw-r--r--tsconfig.json26
-rw-r--r--vite.config.ts8
10 files changed, 229 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a81a0e4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+build/
+.react-router/
+*.db
+*.db-shm
+*.db-wal
+.env
+.DS_Store
diff --git a/app/app.css b/app/app.css
new file mode 100644
index 0000000..99345d8
--- /dev/null
+++ b/app/app.css
@@ -0,0 +1,15 @@
+@import "tailwindcss";
+
+@theme {
+ --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif,
+ "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+}
+
+html,
+body {
+ @apply bg-white dark:bg-gray-950;
+
+ @media (prefers-color-scheme: dark) {
+ color-scheme: dark;
+ }
+}
diff --git a/app/lib/db.server.ts b/app/lib/db.server.ts
new file mode 100644
index 0000000..6aa9313
--- /dev/null
+++ b/app/lib/db.server.ts
@@ -0,0 +1,45 @@
+import Database from "better-sqlite3";
+import path from "path";
+
+let db: Database.Database | null = null;
+
+export function getDb(): Database.Database {
+ if (!db) {
+ const dbPath = path.resolve("whois.db");
+ db = new Database(dbPath);
+ db.pragma("journal_mode = WAL");
+ db.pragma("foreign_keys = ON");
+ initSchema(db);
+ }
+ return db;
+}
+
+function initSchema(db: Database.Database) {
+ db.exec(`
+ CREATE TABLE IF NOT EXISTS bands (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ name_kana TEXT,
+ formed_at TEXT,
+ area TEXT,
+ genre TEXT,
+ url TEXT,
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
+ );
+
+ CREATE TABLE IF NOT EXISTS members (
+ id TEXT PRIMARY KEY,
+ band_id TEXT NOT NULL REFERENCES bands(id),
+ name TEXT NOT NULL,
+ name_kana TEXT,
+ role TEXT,
+ joined_at TEXT,
+ left_at TEXT,
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
+ );
+
+ CREATE INDEX IF NOT EXISTS idx_members_band_id ON members(band_id);
+ `);
+}
diff --git a/app/root.tsx b/app/root.tsx
new file mode 100644
index 0000000..2c88ff1
--- /dev/null
+++ b/app/root.tsx
@@ -0,0 +1,76 @@
+import {
+ isRouteErrorResponse,
+ Links,
+ Meta,
+ Outlet,
+ Scripts,
+ ScrollRestoration,
+} from "react-router";
+
+import type { Route } from "./+types/root";
+import "./app.css";
+
+export const links: Route.LinksFunction = () => [
+ { rel: "preconnect", href: "https://fonts.googleapis.com" },
+ {
+ rel: "preconnect",
+ href: "https://fonts.gstatic.com",
+ crossOrigin: "anonymous",
+ },
+ {
+ rel: "stylesheet",
+ href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
+ },
+];
+
+export function Layout({ children }: { children: React.ReactNode }) {
+ return (
+ <html lang="en" className="dark">
+ <head>
+ <meta charSet="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <title>whois.band</title>
+ <Meta />
+ <Links />
+ </head>
+ <body className="bg-gray-950 text-gray-100 antialiased">
+ {children}
+ <ScrollRestoration />
+ <Scripts />
+ </body>
+ </html>
+ );
+}
+
+export default function App() {
+ return <Outlet />;
+}
+
+export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
+ let message = "Oops!";
+ let details = "An unexpected error occurred.";
+ let stack: string | undefined;
+
+ if (isRouteErrorResponse(error)) {
+ message = error.status === 404 ? "404" : "Error";
+ details =
+ error.status === 404
+ ? "The requested page could not be found."
+ : error.statusText || details;
+ } else if (import.meta.env.DEV && error && error instanceof Error) {
+ details = error.message;
+ stack = error.stack;
+ }
+
+ return (
+ <main className="pt-16 p-4 container mx-auto">
+ <h1>{message}</h1>
+ <p>{details}</p>
+ {stack && (
+ <pre className="w-full p-4 overflow-x-auto">
+ <code>{stack}</code>
+ </pre>
+ )}
+ </main>
+ );
+}
diff --git a/app/routes.ts b/app/routes.ts
new file mode 100644
index 0000000..935792d
--- /dev/null
+++ b/app/routes.ts
@@ -0,0 +1,5 @@
+import { type RouteConfig, index } from "@react-router/dev/routes";
+
+export default [
+ index("routes/home.tsx"),
+] satisfies RouteConfig;
diff --git a/app/routes/home.tsx b/app/routes/home.tsx
new file mode 100644
index 0000000..03ae39a
--- /dev/null
+++ b/app/routes/home.tsx
@@ -0,0 +1,8 @@
+export default function Home() {
+ return (
+ <main className="container mx-auto px-4 py-16 text-center">
+ <h1 className="text-4xl font-bold tracking-tight">whois.band</h1>
+ <p className="mt-4 text-gray-400">Band identification service. Coming soon.</p>
+ </main>
+ );
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..e153c04
--- /dev/null
+++ b/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "whois-band",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "react-router build",
+ "dev": "react-router dev",
+ "start": "react-router-serve ./build/server/index.js",
+ "typecheck": "react-router typegen && tsc"
+ },
+ "dependencies": {
+ "@react-router/node": "^7.3.0",
+ "@react-router/serve": "^7.3.0",
+ "better-sqlite3": "^12.9.0",
+ "isbot": "^5.1.36",
+ "react": "^19.2.5",
+ "react-dom": "^19.2.5",
+ "react-router": "^7.3.0"
+ },
+ "devDependencies": {
+ "@react-router/dev": "^7.3.0",
+ "@tailwindcss/vite": "^4.2.4",
+ "@types/better-sqlite3": "^7.6.13",
+ "@types/node": "^22",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "tailwindcss": "^4.2.4",
+ "tsx": "^4.20.3",
+ "typescript": "^5.9.3",
+ "vite": "^6.3.5",
+ "vite-tsconfig-paths": "^6.1.1"
+ }
+}
diff --git a/react-router.config.ts b/react-router.config.ts
new file mode 100644
index 0000000..e45e273
--- /dev/null
+++ b/react-router.config.ts
@@ -0,0 +1,5 @@
+import type { Config } from "@react-router/dev/config";
+
+export default {
+ ssr: true,
+} satisfies Config;
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..cbe49c7
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "include": [
+ "**/*",
+ "**/.server/**/*",
+ "**/.client/**/*",
+ ".react-router/types/**/*"
+ ],
+ "compilerOptions": {
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "types": ["node", "vite/client"],
+ "target": "ES2022",
+ "module": "ES2022",
+ "moduleResolution": "bundler",
+ "jsx": "react-jsx",
+ "rootDirs": [".", "./.react-router/types"],
+ "paths": {
+ "~/*": ["./app/*"]
+ },
+ "esModuleInterop": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "strict": true
+ }
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..0afac80
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,8 @@
+import { reactRouter } from "@react-router/dev/vite";
+import tailwindcss from "@tailwindcss/vite";
+import tsconfigPaths from "vite-tsconfig-paths";
+import { defineConfig } from "vite";
+
+export default defineConfig({
+ plugins: [tailwindcss(), tsconfigPaths(), reactRouter()],
+});