commit cda7cd10feb7824ef672fbee0aff45d4180dc964 Author: Dhaverd Date: Wed Jan 21 12:09:39 2026 +0800 initial-commit diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d7526ff --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM node:20-alpine AS development-dependencies-env +COPY . /app +WORKDIR /app +RUN npm ci + +FROM node:20-alpine AS production-dependencies-env +COPY ./package.json package-lock.json /app/ +WORKDIR /app +RUN npm ci --omit=dev + +FROM node:20-alpine AS build-env +COPY . /app/ +COPY --from=development-dependencies-env /app/node_modules /app/node_modules +WORKDIR /app +RUN npm run build + +FROM node:20-alpine +COPY ./package.json package-lock.json server.js /app/ +COPY --from=production-dependencies-env /app/node_modules /app/node_modules +COPY --from=build-env /app/build /app/build +WORKDIR /app +CMD ["npm", "run", "start"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6215fdd --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# Welcome to React Router! + +A modern, production-ready template for building full-stack React applications using React Router. + +## Features + +- 🚀 Server-side rendering +- ⚡️ Hot Module Replacement (HMR) +- 📦 Asset bundling and optimization +- 🔄 Data loading and mutations +- 🔒 TypeScript by default +- 🎉 TailwindCSS for styling +- 📖 [React Router docs](https://reactrouter.com/) + +## Getting Started + +### Installation + +Install the dependencies: + +```bash +npm install +``` + +### Development + +Start the development server with HMR: + +```bash +npm run dev +``` + +Your application will be available at `http://localhost:5173`. + +## Building for Production + +Create a production build: + +```bash +npm run build +``` + +## Deployment + +### Docker Deployment + +To build and run using Docker: + +```bash +docker build -t my-app . + +# Run the container +docker run -p 3000:3000 my-app +``` + +The containerized application can be deployed to any platform that supports Docker, including: + +- AWS ECS +- Google Cloud Run +- Azure Container Apps +- Digital Ocean App Platform +- Fly.io +- Railway + +### DIY Deployment + +If you're familiar with deploying Node applications, the built-in app server is production-ready. + +Make sure to deploy the output of `npm run build` + +``` +├── package.json +├── package-lock.json (or pnpm-lock.yaml, or bun.lockb) +├── server.js +├── build/ +│ ├── client/ # Static assets +│ └── server/ # Server-side code +``` + +## Styling + +This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. + +--- + +Built with ❤️ using React Router. diff --git a/app/app.css b/app/app.css new file mode 100644 index 0000000..505c5da --- /dev/null +++ b/app/app.css @@ -0,0 +1,135 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +@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 { + margin: 0!important; + height: 100%; + width: 100%; +} + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} \ No newline at end of file diff --git a/app/assets/images/Hunt_paper.png b/app/assets/images/Hunt_paper.png new file mode 100644 index 0000000..886b2ea Binary files /dev/null and b/app/assets/images/Hunt_paper.png differ diff --git a/app/assets/images/Hunt_paper_side.png b/app/assets/images/Hunt_paper_side.png new file mode 100644 index 0000000..649f79a Binary files /dev/null and b/app/assets/images/Hunt_paper_side.png differ diff --git a/app/assets/styles/home.css b/app/assets/styles/home.css new file mode 100644 index 0000000..bc80f3b --- /dev/null +++ b/app/assets/styles/home.css @@ -0,0 +1,21 @@ +.search-container { + width: 100%; + padding-right: 10px; + padding-left: 10px; + padding-top: 15px; +} + +.search-label { + margin-left: 5px; + color: var(--input); + margin-bottom: 10px; +} + +.search-input { + color: var(--input); +} + +.bg-dark { + background-color: #282c34; + height: 100%; +} \ No newline at end of file diff --git a/app/assets/styles/paper.css b/app/assets/styles/paper.css new file mode 100644 index 0000000..3c9123c --- /dev/null +++ b/app/assets/styles/paper.css @@ -0,0 +1,25 @@ +.bg-paper { + background-image: url('../images/Hunt_paper_side.png'); + background-size: contain; + background-repeat: no-repeat; + background-position-x: center; + background-attachment: scroll; + min-height: 200vh; +} + +.list-container { + padding-top: 20%; + padding-left: 20%; + padding-right: 20%; + display: flex; + flex-direction: column; +} + +@media (min-width:300px) { + .list-container { + font-size: 0.8rem; + padding-top: 25%; + padding-left: 25%; + padding-right: 20%; + } +} \ No newline at end of file diff --git a/app/root.tsx b/app/root.tsx new file mode 100644 index 0000000..1a574e7 --- /dev/null +++ b/app/root.tsx @@ -0,0 +1,24 @@ +import {Links, Meta, Outlet, Scripts} from "react-router"; +import './app.css'; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + Hunt Showdown Blacklist + + + + + + + + {children} + + + ); +} + +export default function App() { + return ; +} diff --git a/app/routes.ts b/app/routes.ts new file mode 100644 index 0000000..8269de1 --- /dev/null +++ b/app/routes.ts @@ -0,0 +1,6 @@ +import {type RouteConfig, index, route} from "@react-router/dev/routes"; + +export default [ + index("routes/home.tsx"), + route("paper", "routes/paper.tsx"), +] satisfies RouteConfig; diff --git a/app/routes/home.tsx b/app/routes/home.tsx new file mode 100644 index 0000000..7ad9cdb --- /dev/null +++ b/app/routes/home.tsx @@ -0,0 +1,96 @@ +import {useState} from "react"; +import '../assets/styles/home.css'; +import { + createTheme, + Divider, + outlinedInputClasses, + TextField, + type Theme, + ThemeProvider, + useTheme +} from "@mui/material"; + +export default function Home() { + const [searchText, setSearchText] = useState(''); + + const customTheme = (outerTheme: Theme) => + createTheme({ + palette: { + mode: outerTheme.palette.mode, + }, + components: { + MuiTextField: { + styleOverrides: { + root: { + '--TextField-brandBorderColor': '#E0E3E7', + '--TextField-brandBorderHoverColor': '#B2BAC2', + '--TextField-brandBorderFocusedColor': '#6F7E8C', + '& label': { + color: 'var(--TextField-brandBorderColor)', + }, + '& label.Mui-focused': { + color: 'var(--TextField-brandBorderColor)', + }, + '& input': { + color: 'var(--TextField-brandBorderColor)', + borderColor: 'var(--TextField-brandBorderColor)', + }, + }, + }, + }, + MuiOutlinedInput: { + styleOverrides: { + notchedOutline: { + borderColor: 'var(--TextField-brandBorderColor)', + }, + root: { + [`&:hover .${outlinedInputClasses.notchedOutline}`]: { + borderColor: 'var(--TextField-brandBorderHoverColor)', + }, + [`&.Mui-focused .${outlinedInputClasses.notchedOutline}`]: { + borderColor: 'var(--TextField-brandBorderFocusedColor)', + }, + }, + }, + }, + MuiDivider: { + styleOverrides: { + root: { + '--TextField-brandBorderColor': '#E0E3E7', + borderColor: 'var(--TextField-brandBorderColor)', + borderWidth: 1, + width: '95%', + } + } + } + } + }); + const outerTheme = useTheme(); + + function handleSearch(text: string) { + setSearchText(text); + } + + return ( + +
+
+ handleSearch(e.target.value)} + fullWidth + /> +
+
+ +
+
+

{searchText} //

+
+
+
+ ); +} diff --git a/app/routes/paper.tsx b/app/routes/paper.tsx new file mode 100644 index 0000000..1695c5f --- /dev/null +++ b/app/routes/paper.tsx @@ -0,0 +1,12 @@ +import '../assets/styles/paper.css'; + +export default function Home() { + return ( + <> +
+
+
+
+ + ); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9c6e595 --- /dev/null +++ b/package.json @@ -0,0 +1,46 @@ +{ + "name": "hunt-blacklist-frontend", + "private": true, + "type": "module", + "scripts": { + "build": "react-router build", + "dev": "cross-env NODE_ENV=development node server.js", + "start": "node server.js", + "typecheck": "react-router typegen && tsc -b" + }, + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/material": "^7.3.7", + "@react-router/express": "7.10.1", + "@react-router/node": "7.10.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "compression": "^1.8.1", + "express": "^5.1.0", + "isbot": "^5.1.31", + "lucide-react": "^0.562.0", + "morgan": "^1.10.1", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "react-router": "7.10.1", + "tailwind-merge": "^3.4.0" + }, + "devDependencies": { + "@react-router/dev": "7.10.1", + "@tailwindcss/vite": "^4.1.13", + "@types/compression": "^1.8.1", + "@types/express": "^5.0.3", + "@types/express-serve-static-core": "^5.0.7", + "@types/morgan": "^1.9.10", + "@types/node": "^22", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "cross-env": "^10.0.0", + "tailwindcss": "^4.1.13", + "tw-animate-css": "^1.4.0", + "typescript": "^5.9.2", + "vite": "^7.1.7", + "vite-tsconfig-paths": "^5.1.4" + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..04a12a4 Binary files /dev/null and b/public/favicon.ico differ diff --git a/react-router.config.ts b/react-router.config.ts new file mode 100644 index 0000000..6ff16f9 --- /dev/null +++ b/react-router.config.ts @@ -0,0 +1,7 @@ +import type { Config } from "@react-router/dev/config"; + +export default { + // Config options... + // Server-side render by default, to enable SPA mode set this to `false` + ssr: true, +} satisfies Config; diff --git a/server.js b/server.js new file mode 100644 index 0000000..50fea2b --- /dev/null +++ b/server.js @@ -0,0 +1,47 @@ +import compression from "compression"; +import express from "express"; +import morgan from "morgan"; + +// Short-circuit the type-checking of the built output. +const BUILD_PATH = "./build/server/index.js"; +const DEVELOPMENT = process.env.NODE_ENV === "development"; +const PORT = Number.parseInt(process.env.PORT || "3000"); + +const app = express(); + +app.use(compression()); +app.disable("x-powered-by"); + +if (DEVELOPMENT) { + console.log("Starting development server"); + const viteDevServer = await import("vite").then((vite) => + vite.createServer({ + server: { middlewareMode: true }, + }), + ); + app.use(viteDevServer.middlewares); + app.use(async (req, res, next) => { + try { + const source = await viteDevServer.ssrLoadModule("./server/app.ts"); + return await source.app(req, res, next); + } catch (error) { + if (typeof error === "object" && error instanceof Error) { + viteDevServer.ssrFixStacktrace(error); + } + next(error); + } + }); +} else { + console.log("Starting production server"); + app.use( + "/assets", + express.static("build/client/assets", { immutable: true, maxAge: "1y" }), + ); + app.use(morgan("tiny")); + app.use(express.static("build/client", { maxAge: "1h" })); + app.use(await import(BUILD_PATH).then((mod) => mod.app)); +} + +app.listen(PORT, () => { + console.log(`Server is running on http://localhost:${PORT}`); +}); diff --git a/server/app.ts b/server/app.ts new file mode 100644 index 0000000..d5e56eb --- /dev/null +++ b/server/app.ts @@ -0,0 +1,22 @@ +import "react-router"; +import { createRequestHandler } from "@react-router/express"; +import express from "express"; + +declare module "react-router" { + interface AppLoadContext { + VALUE_FROM_EXPRESS: string; + } +} + +export const app = express(); + +app.use( + createRequestHandler({ + build: () => import("virtual:react-router/server-build"), + getLoadContext() { + return { + VALUE_FROM_EXPRESS: "Hello from Express", + }; + }, + }), +); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..cea27d4 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.vite.json" } + ], + "compilerOptions": { + "checkJs": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true + } +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..12107b4 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "include": ["server.js", "vite.config.ts"], + "compilerOptions": { + "composite": true, + "strict": true, + "types": ["node"], + "lib": ["ES2022"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler" + } +} diff --git a/tsconfig.vite.json b/tsconfig.vite.json new file mode 100644 index 0000000..e193eb7 --- /dev/null +++ b/tsconfig.vite.json @@ -0,0 +1,28 @@ +{ + "extends": "./tsconfig.json", + "include": [ + ".react-router/types/**/*", + "app/**/*", + "app/**/.server/**/*", + "app/**/.client/**/*", + "server/**/*" + ], + "compilerOptions": { + "composite": true, + "strict": true, + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["vite/client"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "baseUrl": ".", + "rootDirs": [".", "./.react-router/types"], + "paths": { + "~/*": ["./app/*"], + "@/*": ["./app/*"] + }, + "esModuleInterop": true, + "resolveJsonModule": true + } +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..163f695 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,21 @@ +import { reactRouter } from "@react-router/dev/vite"; +import tailwindcss from "@tailwindcss/vite"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; +import path from "path"; + +export default defineConfig(({ isSsrBuild }) => ({ + build: { + rollupOptions: isSsrBuild + ? { + input: "./server/app.ts", + } + : undefined, + }, + plugins: [tailwindcss(), reactRouter(), tsconfigPaths()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./app"), + }, + }, +}));