From d4d104cd604741ac0c308efe5050a0eec69bb389 Mon Sep 17 00:00:00 2001 From: yyamashita Date: Fri, 8 May 2026 23:37:51 +0900 Subject: Initial scaffold: React Router v7 + SQLite + Tailwind CSS v4 --- .gitignore | 8 ++++++ app/app.css | 15 ++++++++++ app/lib/db.server.ts | 45 ++++++++++++++++++++++++++++++ app/root.tsx | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++ app/routes.ts | 5 ++++ app/routes/home.tsx | 8 ++++++ package.json | 33 ++++++++++++++++++++++ react-router.config.ts | 5 ++++ tsconfig.json | 26 +++++++++++++++++ vite.config.ts | 8 ++++++ 10 files changed, 229 insertions(+) create mode 100644 .gitignore create mode 100644 app/app.css create mode 100644 app/lib/db.server.ts create mode 100644 app/root.tsx create mode 100644 app/routes.ts create mode 100644 app/routes/home.tsx create mode 100644 package.json create mode 100644 react-router.config.ts create mode 100644 tsconfig.json create mode 100644 vite.config.ts 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 ( + + + + + whois.band + + + + + {children} + + + + + ); +} + +export default function App() { + return ; +} + +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 ( +
+

{message}

+

{details}

+ {stack && ( +
+          {stack}
+        
+ )} +
+ ); +} 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 ( +
+

whois.band

+

Band identification service. Coming soon.

+
+ ); +} 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()], +}); -- cgit v1.2.3