eksplicit mapping af envs
This commit is contained in:
19
admin/Dockerfile
Normal file
19
admin/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM node:20-alpine AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm ci
|
||||
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM node:20-alpine AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
COPY --from=builder /app/public ./public
|
||||
EXPOSE 3000
|
||||
CMD ["node", "server.js"]
|
||||
14
admin/README.md
Normal file
14
admin/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# admin
|
||||
|
||||
Next.js admin dashboard for Social Proximity. Anonymised stats only.
|
||||
|
||||
See [Docs/ADMIN.md](../Docs/ADMIN.md) for full documentation.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Runs on `localhost:3000`. Requires backend running on port 8000.
|
||||
9
admin/next.config.js
Normal file
9
admin/next.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: "standalone",
|
||||
env: {
|
||||
API_URL: process.env.API_URL ?? "http://localhost:8000",
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
5324
admin/package-lock.json
generated
Normal file
5324
admin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
admin/package.json
Normal file
24
admin/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "sighej-admin",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "14.2.3",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"typescript": "^5",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.3"
|
||||
}
|
||||
}
|
||||
0
admin/public/.gitkeep
Normal file
0
admin/public/.gitkeep
Normal file
13
admin/src/app/layout.tsx
Normal file
13
admin/src/app/layout.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body style={{ fontFamily: "system-ui, sans-serif", padding: "2rem", maxWidth: 800, margin: "0 auto" }}>
|
||||
<header style={{ marginBottom: "2rem" }}>
|
||||
<h1 style={{ margin: 0 }}>SigHej · Admin</h1>
|
||||
<p style={{ color: "#666", marginTop: 4 }}>Anonymised usage stats — no personal data</p>
|
||||
</header>
|
||||
<main>{children}</main>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
52
admin/src/app/page.tsx
Normal file
52
admin/src/app/page.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { StatCard } from "@/components/StatCard";
|
||||
|
||||
interface StatsData {
|
||||
active_sessions_now: number;
|
||||
total_sessions_today: number;
|
||||
total_matches_today: number;
|
||||
top_interest_categories: string[];
|
||||
}
|
||||
|
||||
async function fetchStats(): Promise<StatsData | null> {
|
||||
try {
|
||||
const res = await fetch(`${process.env.API_URL}/stats`, { next: { revalidate: 30 } });
|
||||
if (!res.ok) return null;
|
||||
return res.json();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function StatsPage() {
|
||||
const stats = await fetchStats();
|
||||
|
||||
if (!stats) {
|
||||
return <p>⚠️ Could not reach backend. Is it running?</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fit, minmax(160px, 1fr))",
|
||||
gap: "1rem",
|
||||
marginBottom: "2rem",
|
||||
}}
|
||||
>
|
||||
<StatCard label="Active sessions" value={stats.active_sessions_now} />
|
||||
<StatCard label="Sessions today" value={stats.total_sessions_today} />
|
||||
<StatCard label="Matches today" value={stats.total_matches_today} />
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2>Top interest categories</h2>
|
||||
<ol>
|
||||
{stats.top_interest_categories.map((interest) => (
|
||||
<li key={interest}>{interest}</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
admin/src/components/StatCard.tsx
Normal file
20
admin/src/components/StatCard.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
interface StatCardProps {
|
||||
label: string;
|
||||
value: number | string;
|
||||
}
|
||||
|
||||
export function StatCard({ label, value }: StatCardProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid #e0e0e0",
|
||||
borderRadius: 8,
|
||||
padding: "1.5rem",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: "2rem", fontWeight: 700 }}>{value}</div>
|
||||
<div style={{ color: "#666", marginTop: 4 }}>{label}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
admin/tsconfig.json
Normal file
23
admin/tsconfig.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"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