feat: improve sidebar and deployment setup

This commit is contained in:
JetSprow
2026-04-29 06:07:52 +10:00
parent 581d4448ef
commit 2a50d789dd
13 changed files with 659 additions and 61 deletions

View File

@@ -22,7 +22,7 @@ WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npx prisma generate
CMD ["sh", "-c", "npx prisma db push --accept-data-loss && npx tsx prisma/seed.ts"]
CMD ["sh", "-c", "npm run db:push && npm run db:seed"]
# --- runner: minimal production image ---
FROM base AS runner

View File

@@ -140,9 +140,9 @@ npm run build
首次启动:
```bash
docker compose up -d --build
docker compose --profile setup run --rm init sh -lc 'npx prisma db push --accept-data-loss'
docker compose exec app npm run db:seed
docker compose build init app
docker compose --profile setup run --rm init
docker compose up -d app
```
更新部署:
@@ -150,7 +150,7 @@ docker compose exec app npm run db:seed
```bash
git pull --ff-only
docker compose build init app
docker compose --profile setup run --rm init sh -lc 'npx prisma db push --accept-data-loss'
docker compose --profile setup run --rm init sh -lc 'npm run db:push'
docker compose up -d app
```
@@ -159,7 +159,7 @@ docker compose up -d app
- 查看状态:`docker compose ps`
- 查看日志:`docker compose logs -f app`
- 页面仍是旧版本:确认已执行 `docker compose build init app``docker compose up -d app`
- Schema 没有生效:单独运行 `docker compose --profile setup run --rm init sh -lc 'npx prisma db push --accept-data-loss'`
- Schema 没有生效:单独运行 `docker compose --profile setup run --rm init sh -lc 'npm run db:push'`
## 节点与探测

553
package-lock.json generated
View File

@@ -39,10 +39,12 @@
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"dotenv": "^17.4.2",
"eslint": "^9",
"eslint-config-next": "16.2.4",
"prisma": "^7.7.0",
"tailwindcss": "^4",
"tsx": "^4.21.0",
"typescript": "^5"
}
},
@@ -567,18 +569,6 @@
"node": ">=16"
}
},
"node_modules/@dotenvx/dotenvx/node_modules/dotenv": {
"version": "17.4.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/@dotenvx/dotenvx/node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -809,6 +799,448 @@
"tslib": "^2.4.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
"integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
"integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
"integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
"integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
"integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
"integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
"integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
"integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
@@ -4201,6 +4633,19 @@
}
}
},
"node_modules/c12/node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"devOptional": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/call-bind": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
@@ -5149,10 +5594,9 @@
}
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"devOptional": true,
"version": "17.4.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
@@ -5461,6 +5905,48 @@
"benchmarks"
]
},
"node_modules/esbuild": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.7",
"@esbuild/android-arm": "0.27.7",
"@esbuild/android-arm64": "0.27.7",
"@esbuild/android-x64": "0.27.7",
"@esbuild/darwin-arm64": "0.27.7",
"@esbuild/darwin-x64": "0.27.7",
"@esbuild/freebsd-arm64": "0.27.7",
"@esbuild/freebsd-x64": "0.27.7",
"@esbuild/linux-arm": "0.27.7",
"@esbuild/linux-arm64": "0.27.7",
"@esbuild/linux-ia32": "0.27.7",
"@esbuild/linux-loong64": "0.27.7",
"@esbuild/linux-mips64el": "0.27.7",
"@esbuild/linux-ppc64": "0.27.7",
"@esbuild/linux-riscv64": "0.27.7",
"@esbuild/linux-s390x": "0.27.7",
"@esbuild/linux-x64": "0.27.7",
"@esbuild/netbsd-arm64": "0.27.7",
"@esbuild/netbsd-x64": "0.27.7",
"@esbuild/openbsd-arm64": "0.27.7",
"@esbuild/openbsd-x64": "0.27.7",
"@esbuild/openharmony-arm64": "0.27.7",
"@esbuild/sunos-x64": "0.27.7",
"@esbuild/win32-arm64": "0.27.7",
"@esbuild/win32-ia32": "0.27.7",
"@esbuild/win32-x64": "0.27.7"
}
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -6378,6 +6864,21 @@
"node": ">=14.14"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -11948,6 +12449,26 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/tsx": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5"
},
"bin": {
"tsx": "dist/cli.mjs"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
},
"node_modules/tw-animate-css": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz",

View File

@@ -7,7 +7,7 @@
"build": "prisma generate && next build",
"start": "next start",
"lint": "eslint",
"db:seed": "npx tsx prisma/seed.ts",
"db:seed": "tsx prisma/seed.ts",
"db:push": "prisma db push --accept-data-loss"
},
"dependencies": {
@@ -42,10 +42,12 @@
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"dotenv": "^17.4.2",
"eslint": "^9",
"eslint-config-next": "16.2.4",
"prisma": "^7.7.0",
"tailwindcss": "^4",
"tsx": "^4.21.0",
"typescript": "^5"
},
"overrides": {

View File

@@ -23,7 +23,7 @@ echo "[3/7] Building updated images..."
$COMPOSE build init app
echo "[4/7] Syncing Prisma schema inside Docker network..."
$COMPOSE --profile setup run --rm init sh -lc 'npx prisma db push --accept-data-loss'
$COMPOSE --profile setup run --rm init sh -lc 'npm run db:push'
echo "[5/7] Restarting services..."
$COMPOSE up -d app

View File

@@ -2,16 +2,20 @@ import type { ReactNode } from "react";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { PublicNotice } from "../public-notice";
import { SiteFooter } from "@/components/shared/site-footer";
export function AuthShell({ children }: { children: ReactNode }) {
return (
<main className="grid min-h-[100dvh] place-items-center px-4 py-10">
<main className="flex min-h-[100dvh] flex-col px-4 py-10">
<div className="flex flex-1 items-center justify-center">
<div className="w-full">
<div className="mx-auto w-full max-w-md">
<PublicNotice />
</div>
{children}
</div>
</div>
<SiteFooter className="mt-6" />
</main>
);
}

View File

@@ -3,11 +3,15 @@
import type { ReactNode } from "react";
import { ShieldCheck } from "lucide-react";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { SiteFooter } from "@/components/shared/site-footer";
export function PaymentFrame({ children }: { children: ReactNode }) {
return (
<main className="grid min-h-[100dvh] place-items-center px-4 py-10">
<main className="flex min-h-[100dvh] flex-col px-4 py-10">
<div className="flex flex-1 items-center justify-center">
<div className="w-full max-w-2xl">{children}</div>
</div>
<SiteFooter className="mt-6 max-w-2xl" />
</main>
);
}

View File

@@ -9,6 +9,7 @@ export function AdminMobileNav() {
title="J-Board"
subtitle="管理后台"
groups={adminNavGroups}
collapsibleGroups
/>
);
}

View File

@@ -11,10 +11,11 @@ interface MobileHeaderProps {
links?: SidebarLink[];
groups?: SidebarGroup[];
matchMode?: "exact" | "prefix";
collapsibleGroups?: boolean;
actions?: ReactNode;
}
export function MobileHeader({ title, subtitle, links, groups, matchMode, actions }: MobileHeaderProps) {
export function MobileHeader({ title, subtitle, links, groups, matchMode, collapsibleGroups, actions }: MobileHeaderProps) {
const [open, setOpen] = useState(false);
return (
@@ -42,6 +43,8 @@ export function MobileHeader({ title, subtitle, links, groups, matchMode, action
links={links}
groups={groups}
matchMode={matchMode}
collapsibleGroups={collapsibleGroups}
railCollapsible={false}
onNavigate={() => setOpen(false)}
/>
</MobileDrawer>

View File

@@ -4,7 +4,7 @@ import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { cn } from "@/lib/utils";
import { signOut } from "next-auth/react";
import { ChevronDown } from "lucide-react";
import { ChevronDown, LogOut, PanelLeftClose, PanelLeftOpen } from "lucide-react";
import { useMemo, useState, type ReactNode } from "react";
export interface SidebarLink {
@@ -27,6 +27,7 @@ interface SidebarProps {
groups?: SidebarGroup[];
matchMode?: "exact" | "prefix";
collapsibleGroups?: boolean;
railCollapsible?: boolean;
headerAction?: ReactNode;
onNavigate?: () => void;
}
@@ -38,6 +39,7 @@ export function Sidebar({
groups,
matchMode = "prefix",
collapsibleGroups = false,
railCollapsible = true,
headerAction,
onNavigate,
}: SidebarProps) {
@@ -45,6 +47,8 @@ export function Sidebar({
const router = useRouter();
const navGroups = useMemo(() => groups ?? [{ label: "导航", links }], [groups, links]);
const [signingOut, setSigningOut] = useState(false);
const [railCollapsed, setRailCollapsed] = useState(false);
const shouldCollapseRail = railCollapsible && railCollapsed;
const isActive = (href: string) =>
matchMode === "exact" ? pathname === href : pathname === href || pathname.startsWith(`${href}/`);
@@ -71,36 +75,58 @@ export function Sidebar({
}, {}),
);
return (
<aside className="nav-rail flex h-full w-[15rem] flex-col overflow-hidden rounded-xl text-sidebar-foreground">
<div className="border-b border-sidebar-border px-4 py-4">
<div className="flex items-center gap-2.5">
<div className="flex size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sm font-bold text-sidebar-primary-foreground">
<aside
className={cn(
"nav-rail flex h-full flex-col overflow-hidden rounded-xl text-sidebar-foreground transition-[width] duration-200 ease-out",
shouldCollapseRail ? "w-[4.75rem]" : "w-[15rem]",
)}
>
<div className={cn("border-b border-sidebar-border py-4", shouldCollapseRail ? "px-3" : "px-4")}>
<div className={cn("flex items-center", shouldCollapseRail ? "flex-col gap-2" : "gap-2.5")}>
<div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-sidebar-primary text-sm font-bold text-sidebar-primary-foreground">
S
</div>
{!shouldCollapseRail && (
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold tracking-[-0.02em]">{title}</p>
{subtitle && (
<p className="mt-0.5 truncate text-xs text-sidebar-foreground/55">{subtitle}</p>
)}
</div>
{headerAction}
)}
{!shouldCollapseRail && headerAction}
{railCollapsible && (
<button
type="button"
className="btn-base flex size-7 shrink-0 items-center justify-center rounded-md border border-sidebar-border bg-sidebar-accent/35 text-sidebar-foreground/62 hover:bg-sidebar-accent hover:text-sidebar-foreground"
aria-label={shouldCollapseRail ? "展开侧边栏" : "收起侧边栏"}
title={shouldCollapseRail ? "展开侧边栏" : "收起侧边栏"}
onClick={() => setRailCollapsed((value) => !value)}
>
{shouldCollapseRail ? <PanelLeftOpen className="size-3.5" /> : <PanelLeftClose className="size-3.5" />}
</button>
)}
</div>
</div>
<nav className="flex-1 space-y-4 overflow-y-auto px-3 py-3" aria-label={`${title} 导航`}>
{navGroups.map((group) => (
<nav
className={cn("flex-1 space-y-4 overflow-y-auto py-3", shouldCollapseRail ? "px-2" : "px-3")}
aria-label={`${title} 导航`}
>
{navGroups.map((group, groupIndex) => (
<div key={group.label} className="space-y-2">
{(() => {
const hasActive = group.links.some((link) => isActive(link.href));
const isCollapsed =
collapsibleGroups &&
!hasActive &&
!shouldCollapseRail &&
(collapsedGroups[group.label] ?? Boolean(group.defaultCollapsed));
const isOpen = !isCollapsed;
const isOpen = shouldCollapseRail || !isCollapsed;
return (
<>
{collapsibleGroups ? (
{collapsibleGroups && !shouldCollapseRail ? (
<button
type="button"
className="flex w-full items-center justify-between rounded-md px-2.5 py-1 text-left text-[0.68rem] font-medium tracking-wide text-sidebar-foreground/45 transition-colors hover:bg-sidebar-accent/40 hover:text-sidebar-foreground/70"
@@ -109,10 +135,7 @@ export function Sidebar({
onClick={() =>
setCollapsedGroups((prev) => ({
...prev,
[group.label]: !(
!hasActive &&
(prev[group.label] ?? Boolean(group.defaultCollapsed))
),
[group.label]: !(prev[group.label] ?? (!hasActive && Boolean(group.defaultCollapsed))),
}))
}
>
@@ -124,11 +147,13 @@ export function Sidebar({
)}
/>
</button>
) : (
) : shouldCollapseRail && groupIndex > 0 ? (
<div className="mx-auto h-px w-6 bg-sidebar-border/70" aria-hidden />
) : !shouldCollapseRail ? (
<p className="px-2.5 text-[0.68rem] font-medium tracking-wide text-sidebar-foreground/38">
{group.label}
</p>
)}
) : null}
<div
id={`sidebar-group-${group.label}`}
className={cn("space-y-1", !isOpen && "hidden")}
@@ -143,11 +168,14 @@ export function Sidebar({
onClick={onNavigate}
aria-current={active ? "page" : undefined}
className={cn(
"nav-link-premium group flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm",
"nav-link-premium group flex items-center rounded-lg py-2 text-sm",
shouldCollapseRail ? "justify-center px-0" : "gap-2.5 px-2.5",
active
? "bg-sidebar-primary text-sidebar-primary-foreground font-medium"
: "text-sidebar-foreground/68 hover:bg-sidebar-accent hover:text-sidebar-foreground"
)}
aria-label={shouldCollapseRail ? link.label : undefined}
title={shouldCollapseRail ? link.label : undefined}
>
<span
className={cn(
@@ -157,8 +185,8 @@ export function Sidebar({
>
{link.icon}
</span>
<span className="min-w-0 flex-1 truncate">{link.label}</span>
{active && <span className="size-1.5 rounded-full bg-sidebar-primary-foreground/80" aria-hidden />}
{!shouldCollapseRail && <span className="min-w-0 flex-1 truncate">{link.label}</span>}
{active && !shouldCollapseRail && <span className="size-1.5 rounded-full bg-sidebar-primary-foreground/80" aria-hidden />}
</Link>
);
})}
@@ -169,14 +197,20 @@ export function Sidebar({
</div>
))}
</nav>
<div className="border-t border-sidebar-border px-3 py-3">
<div className={cn("border-t border-sidebar-border py-3", shouldCollapseRail ? "px-2" : "px-3")}>
<button
type="button"
onClick={handleSignOut}
disabled={signingOut}
className="btn-base btn-cream w-full rounded-lg px-2.5 py-2 text-left text-sm font-medium text-sidebar-foreground/75 hover:text-sidebar-foreground"
className={cn(
"btn-base btn-cream flex w-full items-center rounded-lg py-2 text-sm font-medium text-sidebar-foreground/75 hover:text-sidebar-foreground",
shouldCollapseRail ? "justify-center px-0" : "gap-2 px-2.5 text-left",
)}
aria-label={signingOut ? "退出中" : "退出登录"}
title={shouldCollapseRail ? (signingOut ? "退出中..." : "退出登录") : undefined}
>
{signingOut ? "退出中..." : "退出登录"}
<LogOut className="size-4 shrink-0" />
{shouldCollapseRail ? <span className="sr-only">{signingOut ? "退出中..." : "退出登录"}</span> : signingOut ? "退出中..." : "退出登录"}
</button>
</div>
</aside>

View File

@@ -0,0 +1,27 @@
import { GitFork } from "lucide-react";
import { cn } from "@/lib/utils";
const GITHUB_URL = "https://github.com/JetSprow/J-Board";
export function SiteFooter({ className }: { className?: string }) {
return (
<footer
className={cn(
"mx-auto flex w-full max-w-md items-center justify-center gap-2 text-xs text-muted-foreground/70",
className,
)}
>
<span>J-Board</span>
<span className="h-1 w-1 rounded-full bg-muted-foreground/30" aria-hidden />
<a
href={GITHUB_URL}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1.5 rounded-md px-1.5 py-1 font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/15"
>
<GitFork className="size-3.5" />
<span>GitHub</span>
</a>
</footer>
);
}

View File

@@ -11,6 +11,7 @@ export function UserMobileNav({ userName, unreadCount }: { userName: string; unr
subtitle={userName}
groups={userNavGroups}
matchMode="exact"
collapsibleGroups
actions={<NotificationPopover unreadCount={unreadCount} className="size-10" />}
/>
);

View File

@@ -44,6 +44,7 @@ export function UserSidebar({ userName, unreadCount, onNavigate }: { userName: s
subtitle={userName}
groups={userNavGroups}
matchMode="exact"
collapsibleGroups
headerAction={<NotificationPopover unreadCount={unreadCount} />}
onNavigate={onNavigate}
/>