diff options
| -rw-r--r-- | .gitignore | 8 | ||||
| -rw-r--r-- | app/app.css | 15 | ||||
| -rw-r--r-- | app/lib/db.server.ts | 45 | ||||
| -rw-r--r-- | app/root.tsx | 76 | ||||
| -rw-r--r-- | app/routes.ts | 5 | ||||
| -rw-r--r-- | app/routes/home.tsx | 8 | ||||
| -rw-r--r-- | package.json | 33 | ||||
| -rw-r--r-- | react-router.config.ts | 5 | ||||
| -rw-r--r-- | tsconfig.json | 26 | ||||
| -rw-r--r-- | vite.config.ts | 8 |
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()], +}); |
