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:
@@ -0,0 +1,4 @@
|
|||||||
|
.git
|
||||||
|
.next
|
||||||
|
node_modules
|
||||||
|
*.log
|
||||||
@@ -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
|
||||||
@@ -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/
|
||||||
@@ -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"]
|
||||||
Vendored
+6
@@ -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.
|
||||||
@@ -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;
|
||||||
Generated
+2789
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 |
@@ -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");
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user