feat(frontend): scaffold Next.js app tooling and static assets

Set up the App Router toolchain, Docker build, Apple emoji vendor script, and public branding assets.
This commit is contained in:
ahp
2026-10-07 19:40:26 +03:30
parent 640745925a
commit 0751ee5588
13 changed files with 2987 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
.git
.next
node_modules
*.log
+3
View File
@@ -0,0 +1,3 @@
# Local `npm run dev` (API on :8080 without Traefik). Leave empty when behind reverse proxy.
NEXT_PUBLIC_API_URL=http://localhost:8080
NEXT_PUBLIC_WS_URL=ws://localhost:8080/api/ws
+17
View File
@@ -0,0 +1,17 @@
# deps
node_modules/
# next
.next/
out/
# env
.env*.local
# misc
*.log
.DS_Store
npm-debug.log*
# vendored at install/build from emoji-datasource-apple (~27MB)
/public/emoji/
+45
View File
@@ -0,0 +1,45 @@
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
# Ignore lifecycle scripts here — vendor script is only present after full COPY.
RUN npm ci --ignore-scripts
FROM node:22-alpine AS builder
WORKDIR /app
# Empty = same-origin (Traefik reverse proxy). For direct API access set build args.
ARG NEXT_PUBLIC_API_URL=
ARG NEXT_PUBLIC_WS_URL=
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_WS_URL=$NEXT_PUBLIC_WS_URL
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Vendor Apple emoji PNGs into public/ (external CDNs are often blocked).
RUN node scripts/vendor-apple-emoji.mjs && npm run build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup -S app && adduser -S app -G app
COPY --from=builder --chown=app:app /app/public ./public
COPY --from=builder --chown=app:app /app/.next ./.next
COPY --from=builder --chown=app:app /app/package.json ./package.json
COPY --from=builder --chown=app:app /app/next.config.mjs ./next.config.mjs
COPY --from=builder --chown=app:app /app/node_modules ./node_modules
# Public assets are required at runtime for /logo.png and for the
# /_next/image optimizer's internal fetch of local paths.
# next.config.mjs is required at runtime so images.contentDispositionType
# and other config overrides are applied (not only baked into manifests).
USER app
EXPOSE 3000
CMD ["npm", "run", "start"]
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+25
View File
@@ -0,0 +1,25 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
eslint: { ignoreDuringBuilds: true },
images: {
// Default "attachment" can prevent some clients from rendering /_next/image in <img>.
contentDispositionType: "inline",
},
async redirects() {
return [
// Student self-service password recovery is disabled.
{ source: "/recovery", destination: "/login", permanent: false },
];
},
async headers() {
return [
{
source: "/c/:path*",
headers: [{ key: "X-Robots-Tag", value: "noindex, nofollow, noarchive" }],
},
];
},
};
export default nextConfig;
+2789
View File
File diff suppressed because it is too large Load Diff
+48
View File
@@ -0,0 +1,48 @@
{
"name": "pargar-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 3000",
"prebuild": "node scripts/vendor-apple-emoji.mjs",
"vendor:emoji": "node scripts/vendor-apple-emoji.mjs",
"build": "next build",
"start": "next start -p 3000",
"lint": "next lint",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@radix-ui/react-avatar": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.2",
"@tanstack/react-query": "^5.62.0",
"boring-avatars": "^2.0.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"emoji-datasource-apple": "^15.1.2",
"framer-motion": "^12.0.0",
"lucide-react": "^0.468.0",
"next": "^15.1.0",
"next-themes": "^0.4.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-multi-date-picker": "^4.5.2",
"sonner": "^1.7.1",
"tailwind-merge": "^2.6.0",
"tw-animate-css": "^1.4.0",
"zustand": "^5.0.2"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.0.0",
"@types/node": "^22.10.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.7.0"
}
}
+7
View File
@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env node
/**
* Copy Apple Color Emoji 64px PNGs into public/ so chat never depends on
* external CDNs (often blocked). Source: emoji-datasource-apple.
*/
import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const src = join(root, "node_modules/emoji-datasource-apple/img/apple/64");
const dest = join(root, "public/emoji/apple/64");
if (!existsSync(src)) {
console.warn("[vendor-apple-emoji] skip: emoji-datasource-apple not installed");
process.exit(0);
}
mkdirSync(dirname(dest), { recursive: true });
rmSync(dest, { recursive: true, force: true });
cpSync(src, dest, { recursive: true });
console.log("[vendor-apple-emoji] copied → public/emoji/apple/64");
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": { "@/*": ["./src/*"] }
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}