mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 01:14:10 +05:30
feat: improve sidebar and deployment setup
This commit is contained in:
@@ -22,7 +22,7 @@ WORKDIR /app
|
|||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npx prisma generate
|
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 ---
|
# --- runner: minimal production image ---
|
||||||
FROM base AS runner
|
FROM base AS runner
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -140,9 +140,9 @@ npm run build
|
|||||||
首次启动:
|
首次启动:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d --build
|
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
|
||||||
docker compose exec app npm run db:seed
|
docker compose up -d app
|
||||||
```
|
```
|
||||||
|
|
||||||
更新部署:
|
更新部署:
|
||||||
@@ -150,7 +150,7 @@ docker compose exec app npm run db:seed
|
|||||||
```bash
|
```bash
|
||||||
git pull --ff-only
|
git pull --ff-only
|
||||||
docker compose build init app
|
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
|
docker compose up -d app
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ docker compose up -d app
|
|||||||
- 查看状态:`docker compose ps`
|
- 查看状态:`docker compose ps`
|
||||||
- 查看日志:`docker compose logs -f app`
|
- 查看日志:`docker compose logs -f app`
|
||||||
- 页面仍是旧版本:确认已执行 `docker compose build init app` 和 `docker compose up -d 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
553
package-lock.json
generated
@@ -39,10 +39,12 @@
|
|||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"dotenv": "^17.4.2",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.2.4",
|
"eslint-config-next": "16.2.4",
|
||||||
"prisma": "^7.7.0",
|
"prisma": "^7.7.0",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -567,18 +569,6 @@
|
|||||||
"node": ">=16"
|
"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": {
|
"node_modules/@dotenvx/dotenvx/node_modules/execa": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
|
||||||
@@ -809,6 +799,448 @@
|
|||||||
"tslib": "^2.4.0"
|
"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": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
"version": "4.9.1",
|
"version": "4.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
|
"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": {
|
"node_modules/call-bind": {
|
||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
|
||||||
@@ -5149,10 +5594,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.6.1",
|
"version": "17.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
|
||||||
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -5461,6 +5905,48 @@
|
|||||||
"benchmarks"
|
"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": {
|
"node_modules/escalade": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||||
@@ -6378,6 +6864,21 @@
|
|||||||
"node": ">=14.14"
|
"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": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
@@ -11948,6 +12449,26 @@
|
|||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"license": "0BSD"
|
"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": {
|
"node_modules/tw-animate-css": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"build": "prisma generate && next build",
|
"build": "prisma generate && next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"db:seed": "npx tsx prisma/seed.ts",
|
"db:seed": "tsx prisma/seed.ts",
|
||||||
"db:push": "prisma db push --accept-data-loss"
|
"db:push": "prisma db push --accept-data-loss"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -42,10 +42,12 @@
|
|||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"dotenv": "^17.4.2",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.2.4",
|
"eslint-config-next": "16.2.4",
|
||||||
"prisma": "^7.7.0",
|
"prisma": "^7.7.0",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ echo "[3/7] Building updated images..."
|
|||||||
$COMPOSE build init app
|
$COMPOSE build init app
|
||||||
|
|
||||||
echo "[4/7] Syncing Prisma schema inside Docker network..."
|
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..."
|
echo "[5/7] Restarting services..."
|
||||||
$COMPOSE up -d app
|
$COMPOSE up -d app
|
||||||
|
|||||||
@@ -2,16 +2,20 @@ import type { ReactNode } from "react";
|
|||||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { PublicNotice } from "../public-notice";
|
import { PublicNotice } from "../public-notice";
|
||||||
|
import { SiteFooter } from "@/components/shared/site-footer";
|
||||||
|
|
||||||
export function AuthShell({ children }: { children: ReactNode }) {
|
export function AuthShell({ children }: { children: ReactNode }) {
|
||||||
return (
|
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="w-full">
|
<div className="flex flex-1 items-center justify-center">
|
||||||
<div className="mx-auto w-full max-w-md">
|
<div className="w-full">
|
||||||
<PublicNotice />
|
<div className="mx-auto w-full max-w-md">
|
||||||
|
<PublicNotice />
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
</div>
|
</div>
|
||||||
{children}
|
|
||||||
</div>
|
</div>
|
||||||
|
<SiteFooter className="mt-6" />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,15 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { ShieldCheck } from "lucide-react";
|
import { ShieldCheck } from "lucide-react";
|
||||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||||
|
import { SiteFooter } from "@/components/shared/site-footer";
|
||||||
|
|
||||||
export function PaymentFrame({ children }: { children: ReactNode }) {
|
export function PaymentFrame({ children }: { children: ReactNode }) {
|
||||||
return (
|
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="w-full max-w-2xl">{children}</div>
|
<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>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export function AdminMobileNav() {
|
|||||||
title="J-Board"
|
title="J-Board"
|
||||||
subtitle="管理后台"
|
subtitle="管理后台"
|
||||||
groups={adminNavGroups}
|
groups={adminNavGroups}
|
||||||
|
collapsibleGroups
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ interface MobileHeaderProps {
|
|||||||
links?: SidebarLink[];
|
links?: SidebarLink[];
|
||||||
groups?: SidebarGroup[];
|
groups?: SidebarGroup[];
|
||||||
matchMode?: "exact" | "prefix";
|
matchMode?: "exact" | "prefix";
|
||||||
|
collapsibleGroups?: boolean;
|
||||||
actions?: ReactNode;
|
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);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -42,6 +43,8 @@ export function MobileHeader({ title, subtitle, links, groups, matchMode, action
|
|||||||
links={links}
|
links={links}
|
||||||
groups={groups}
|
groups={groups}
|
||||||
matchMode={matchMode}
|
matchMode={matchMode}
|
||||||
|
collapsibleGroups={collapsibleGroups}
|
||||||
|
railCollapsible={false}
|
||||||
onNavigate={() => setOpen(false)}
|
onNavigate={() => setOpen(false)}
|
||||||
/>
|
/>
|
||||||
</MobileDrawer>
|
</MobileDrawer>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Link from "next/link";
|
|||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { signOut } from "next-auth/react";
|
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";
|
import { useMemo, useState, type ReactNode } from "react";
|
||||||
|
|
||||||
export interface SidebarLink {
|
export interface SidebarLink {
|
||||||
@@ -27,6 +27,7 @@ interface SidebarProps {
|
|||||||
groups?: SidebarGroup[];
|
groups?: SidebarGroup[];
|
||||||
matchMode?: "exact" | "prefix";
|
matchMode?: "exact" | "prefix";
|
||||||
collapsibleGroups?: boolean;
|
collapsibleGroups?: boolean;
|
||||||
|
railCollapsible?: boolean;
|
||||||
headerAction?: ReactNode;
|
headerAction?: ReactNode;
|
||||||
onNavigate?: () => void;
|
onNavigate?: () => void;
|
||||||
}
|
}
|
||||||
@@ -38,6 +39,7 @@ export function Sidebar({
|
|||||||
groups,
|
groups,
|
||||||
matchMode = "prefix",
|
matchMode = "prefix",
|
||||||
collapsibleGroups = false,
|
collapsibleGroups = false,
|
||||||
|
railCollapsible = true,
|
||||||
headerAction,
|
headerAction,
|
||||||
onNavigate,
|
onNavigate,
|
||||||
}: SidebarProps) {
|
}: SidebarProps) {
|
||||||
@@ -45,6 +47,8 @@ export function Sidebar({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const navGroups = useMemo(() => groups ?? [{ label: "导航", links }], [groups, links]);
|
const navGroups = useMemo(() => groups ?? [{ label: "导航", links }], [groups, links]);
|
||||||
const [signingOut, setSigningOut] = useState(false);
|
const [signingOut, setSigningOut] = useState(false);
|
||||||
|
const [railCollapsed, setRailCollapsed] = useState(false);
|
||||||
|
const shouldCollapseRail = railCollapsible && railCollapsed;
|
||||||
|
|
||||||
const isActive = (href: string) =>
|
const isActive = (href: string) =>
|
||||||
matchMode === "exact" ? pathname === href : pathname === href || pathname.startsWith(`${href}/`);
|
matchMode === "exact" ? pathname === href : pathname === href || pathname.startsWith(`${href}/`);
|
||||||
@@ -71,36 +75,58 @@ export function Sidebar({
|
|||||||
}, {}),
|
}, {}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="nav-rail flex h-full w-[15rem] flex-col overflow-hidden rounded-xl text-sidebar-foreground">
|
<aside
|
||||||
<div className="border-b border-sidebar-border px-4 py-4">
|
className={cn(
|
||||||
<div className="flex items-center gap-2.5">
|
"nav-rail flex h-full flex-col overflow-hidden rounded-xl text-sidebar-foreground transition-[width] duration-200 ease-out",
|
||||||
<div className="flex size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sm font-bold text-sidebar-primary-foreground">
|
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
|
S
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
{!shouldCollapseRail && (
|
||||||
<p className="text-sm font-semibold tracking-[-0.02em]">{title}</p>
|
<div className="min-w-0 flex-1">
|
||||||
{subtitle && (
|
<p className="text-sm font-semibold tracking-[-0.02em]">{title}</p>
|
||||||
<p className="mt-0.5 truncate text-xs text-sidebar-foreground/55">{subtitle}</p>
|
{subtitle && (
|
||||||
)}
|
<p className="mt-0.5 truncate text-xs text-sidebar-foreground/55">{subtitle}</p>
|
||||||
</div>
|
)}
|
||||||
{headerAction}
|
</div>
|
||||||
|
)}
|
||||||
|
{!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>
|
||||||
</div>
|
</div>
|
||||||
<nav className="flex-1 space-y-4 overflow-y-auto px-3 py-3" aria-label={`${title} 导航`}>
|
<nav
|
||||||
{navGroups.map((group) => (
|
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">
|
<div key={group.label} className="space-y-2">
|
||||||
{(() => {
|
{(() => {
|
||||||
const hasActive = group.links.some((link) => isActive(link.href));
|
const hasActive = group.links.some((link) => isActive(link.href));
|
||||||
const isCollapsed =
|
const isCollapsed =
|
||||||
collapsibleGroups &&
|
collapsibleGroups &&
|
||||||
!hasActive &&
|
!shouldCollapseRail &&
|
||||||
(collapsedGroups[group.label] ?? Boolean(group.defaultCollapsed));
|
(collapsedGroups[group.label] ?? Boolean(group.defaultCollapsed));
|
||||||
const isOpen = !isCollapsed;
|
const isOpen = shouldCollapseRail || !isCollapsed;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{collapsibleGroups ? (
|
{collapsibleGroups && !shouldCollapseRail ? (
|
||||||
<button
|
<button
|
||||||
type="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"
|
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={() =>
|
onClick={() =>
|
||||||
setCollapsedGroups((prev) => ({
|
setCollapsedGroups((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[group.label]: !(
|
[group.label]: !(prev[group.label] ?? (!hasActive && Boolean(group.defaultCollapsed))),
|
||||||
!hasActive &&
|
|
||||||
(prev[group.label] ?? Boolean(group.defaultCollapsed))
|
|
||||||
),
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -124,11 +147,13 @@ export function Sidebar({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</button>
|
</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">
|
<p className="px-2.5 text-[0.68rem] font-medium tracking-wide text-sidebar-foreground/38">
|
||||||
{group.label}
|
{group.label}
|
||||||
</p>
|
</p>
|
||||||
)}
|
) : null}
|
||||||
<div
|
<div
|
||||||
id={`sidebar-group-${group.label}`}
|
id={`sidebar-group-${group.label}`}
|
||||||
className={cn("space-y-1", !isOpen && "hidden")}
|
className={cn("space-y-1", !isOpen && "hidden")}
|
||||||
@@ -143,11 +168,14 @@ export function Sidebar({
|
|||||||
onClick={onNavigate}
|
onClick={onNavigate}
|
||||||
aria-current={active ? "page" : undefined}
|
aria-current={active ? "page" : undefined}
|
||||||
className={cn(
|
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
|
active
|
||||||
? "bg-sidebar-primary text-sidebar-primary-foreground font-medium"
|
? "bg-sidebar-primary text-sidebar-primary-foreground font-medium"
|
||||||
: "text-sidebar-foreground/68 hover:bg-sidebar-accent hover:text-sidebar-foreground"
|
: "text-sidebar-foreground/68 hover:bg-sidebar-accent hover:text-sidebar-foreground"
|
||||||
)}
|
)}
|
||||||
|
aria-label={shouldCollapseRail ? link.label : undefined}
|
||||||
|
title={shouldCollapseRail ? link.label : undefined}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -157,8 +185,8 @@ export function Sidebar({
|
|||||||
>
|
>
|
||||||
{link.icon}
|
{link.icon}
|
||||||
</span>
|
</span>
|
||||||
<span className="min-w-0 flex-1 truncate">{link.label}</span>
|
{!shouldCollapseRail && <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 />}
|
{active && !shouldCollapseRail && <span className="size-1.5 rounded-full bg-sidebar-primary-foreground/80" aria-hidden />}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -169,14 +197,20 @@ export function Sidebar({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleSignOut}
|
onClick={handleSignOut}
|
||||||
disabled={signingOut}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
27
src/components/shared/site-footer.tsx
Normal file
27
src/components/shared/site-footer.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ export function UserMobileNav({ userName, unreadCount }: { userName: string; unr
|
|||||||
subtitle={userName}
|
subtitle={userName}
|
||||||
groups={userNavGroups}
|
groups={userNavGroups}
|
||||||
matchMode="exact"
|
matchMode="exact"
|
||||||
|
collapsibleGroups
|
||||||
actions={<NotificationPopover unreadCount={unreadCount} className="size-10" />}
|
actions={<NotificationPopover unreadCount={unreadCount} className="size-10" />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export function UserSidebar({ userName, unreadCount, onNavigate }: { userName: s
|
|||||||
subtitle={userName}
|
subtitle={userName}
|
||||||
groups={userNavGroups}
|
groups={userNavGroups}
|
||||||
matchMode="exact"
|
matchMode="exact"
|
||||||
|
collapsibleGroups
|
||||||
headerAction={<NotificationPopover unreadCount={unreadCount} />}
|
headerAction={<NotificationPopover unreadCount={unreadCount} />}
|
||||||
onNavigate={onNavigate}
|
onNavigate={onNavigate}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user