diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..0194d3c --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,10 @@ +# Database +DATABASE_URL="postgresql://user:password@localhost:5432/hlae_site" + +# Better Auth +BETTER_AUTH_SECRET="your-secret-here" +BETTER_AUTH_URL="http://localhost:3001" + +# GitHub OAuth (Optional) +GITHUB_CLIENT_ID="" +GITHUB_CLIENT_SECRET="" diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 25e5f9f..e548f88 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -11,49 +11,54 @@ datasource db { model User { id String @id @default(cuid()) - name String? - email String? @unique - emailVerified DateTime? - phone String? @unique - phoneVerified DateTime? + name String + email String @unique + emailVerified Boolean image String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - accounts Account[] + createdAt DateTime + updatedAt DateTime sessions Session[] + accounts Account[] posts Post[] comments Comment[] - - @@map("users") -} - -model Account { - id String @id @default(cuid()) - userId String @map("user_id") - type String - provider String - providerAccountId String @map("provider_account_id") - refresh_token String? - access_token String? - expires_at Int? - token_type String? - scope String? - id_token String? - session_state String? - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@unique([provider, providerAccountId]) - @@map("accounts") } model Session { - id String @id @default(cuid()) - sessionToken String @unique @map("session_token") - userId String @map("user_id") - expires DateTime - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + expiresAt DateTime + token String @unique + createdAt DateTime + updatedAt DateTime + ipAddress String? + userAgent String? + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} - @@map("sessions") +model Account { + id String @id @default(cuid()) + accountId String + providerId String + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + accessToken String? + refreshToken String? + idToken String? + accessTokenExpiresAt DateTime? + refreshTokenExpiresAt DateTime? + scope String? + password String? + createdAt DateTime + updatedAt DateTime +} + +model Verification { + id String @id @default(cuid()) + identifier String + value String + expiresAt DateTime + createdAt DateTime? + updatedAt DateTime? } model Post { @@ -66,21 +71,36 @@ model Post { updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) comments Comment[] - - @@map("posts") } model Resource { + id String @id @default(cuid()) + title String + description String? + url String + icon String? + image String? + category String? + githubRepo String? + downloadCdn String? + downloadOriginal String? + version String? + background Boolean @default(false) + order Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Portal { id String @id @default(cuid()) title String - description String? url String + description String? icon String? - category String? + background Boolean @default(false) + order Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - - @@map("resources") } model Comment { @@ -92,15 +112,4 @@ model Comment { updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) post Post @relation(fields: [postId], references: [id], onDelete: Cascade) - - @@map("comments") -} - -model VerificationToken { - identifier String - token String @unique - expires DateTime - - @@unique([identifier, token]) - @@map("verification_tokens") } diff --git a/backend/prisma/seed.ts b/backend/prisma/seed.ts new file mode 100644 index 0000000..fef3668 --- /dev/null +++ b/backend/prisma/seed.ts @@ -0,0 +1,246 @@ +import { PrismaClient } from '@prisma/client' +import { PrismaPg } from "@prisma/adapter-pg"; +import pg from "pg"; + +const connectionString = process.env.DATABASE_URL; +const pool = new pg.Pool({ connectionString }); +const adapter = new PrismaPg(pool); + +const prisma = new PrismaClient({ adapter }); + +const portalData = [ + { + title: '中文论坛', + url: 'https://forum.hlae.site', + description: 'HLAE中文交流社区', + icon: 'pi pi-comments', + background: true, + }, + { + title: '官方Wiki', + url: 'https://github.com/advancedfx/advancedfx/wiki', + description: '权威,但是英文 orz', + icon: 'pi pi-book', + background: true, + }, + { + title: '新版文档', + url: 'https://doc.hlae.site', + description: '新版 advancedfx 文档,建设中', + icon: 'pi pi-bookmark', + background: true, + }, + { + title: '官方Discord', + url: 'https://discord.gg/NGp8qhN', + description: '和开发者近距离交流', + icon: 'pi pi-discord', + background: true, + }, + { + title: '问题与建议提交', + url: 'https://github.com/advancedfx/advancedfx/issues', + description: 'tnnd 为什么不更新', + icon: 'pi pi-question-circle', + background: true, + }, + { + title: 'HUD生成器', + url: 'https://hud.hlae.site', + description: '击杀信息和准星生成工具', + icon: 'pi pi-wrench', + background: true, + }, + { + title: '击杀信息生成', + url: '/hud/deathmsg', + description: 'CS2 · CS 击杀信息生成工具(测试)', + icon: 'pi pi-sliders-h', + background: true, + }, + { + title: 'HLTV', + url: 'https://hltv.org/', + description: 'CSGO新闻、数据、录像', + icon: 'pi pi-video', + background: true, + }, +] + +const resourceData = [ + { + title: 'HLAE', + githubRepo: 'advancedfx/advancedfx', + url: 'https://github.com/advancedfx/advancedfx', + description: '起源游戏影片制作工具', + background: false, + downloadCdn: 'https://api.upup.cool/get/hlae', + downloadOriginal: 'https://github.com/advancedfx/advancedfx/releases/latest', + image: '/icon/hlae.png', + }, + { + title: 'FFmpeg', + githubRepo: 'GyanD/codexffmpeg', + url: 'https://ffmpeg.org', + description: '免费开源的全能媒体转码工具', + background: false, + downloadCdn: 'https://api.upup.cool/get/ffmpeg', + downloadOriginal: 'https://github.com/GyanD/codexffmpeg/releases/latest', + image: '/icon/ffmpeg.webp', + }, + { + title: 'CFG预设 For CS2', + githubRepo: 'Purple-CSGO/CS2-Config-Presets', + url: 'https://cfg.upup.cool/v2', + description: '适用于CS2各场景的Config预设', + background: true, + downloadCdn: 'https://api.upup.cool/get/cs2-cfg', + downloadOriginal: 'https://github.com/Purple-CSGO/CS2-Config-Presets/releases/latest', + icon: 'pi pi-github', + }, + { + title: 'CS Demo Manager', + githubRepo: 'akiver/CS-Demo-Manager', + url: 'https://cs-demo-manager.com', + description: 'CS录像分析观看工具', + background: true, + downloadCdn: 'https://api.upup.cool/get/csdm', + downloadOriginal: 'https://github.com/akiver/CS-Demo-Manager/releases/latest', + image: '/icon/csdm.svg', + }, + { + title: 'CS工具箱', + githubRepo: 'plsgo/cstb', + url: 'https://cstb.upup.cool', + description: '一个为CS游戏各个方面带来便利的工具集合', + background: true, + downloadCdn: 'https://api.upup.cool/get/cstb', + downloadOriginal: 'https://github.com/plsgo/cstb/releases/latest', + image: '/icon/cstb.png', + }, + { + title: 'HLAE Studio', + githubRepo: 'One-Studio/HLAE-Studio', + url: 'https://github.com/One-Studio/HLAE-Studio', + description: 'HLAE环境配置更新工具', + version: 'v1.1.0', + background: true, + downloadCdn: 'https://api.upup.cool/get/hlae-studio', + downloadOriginal: 'https://github.com/One-Studio/HLAE-Studio/releases/latest', + image: '/icon/hlae-studio.png', + }, + { + title: 'CFG预设', + githubRepo: 'Purple-CSGO/CSGO-Config-Presets', + url: 'https://cfg.upup.cool', + description: '适用于CSGO各场景的Config预设', + background: true, + downloadCdn: 'https://api.upup.cool/get/csgo-cfg', + downloadOriginal: 'https://github.com/Purple-CSGO/CSGO-Config-Presets/releases/latest', + icon: 'pi pi-github', + }, + { + title: 'nskinz', + githubRepo: 'advancedfx/nSkinz', + url: 'https://github.com/advancedfx/nskinz', + description: 'CSGO皮肤修改替换插件 advancedfx维护版本', + background: true, + downloadCdn: 'https://api.upup.cool/repo/advancedfx/nSkinz/&&&&.zip', + downloadOriginal: 'https://github.com/advancedfx/nSkinz/releases/latest', + icon: 'pi pi-github', + }, + { + title: 'MIGI', + url: 'https://zoolsmith.github.io/MIGI3', + description: 'CSGO资源修改工具', + background: false, + downloadCdn: 'https://cdn.upup.cool/https://github.com/ZooLSmith/MIGI3/blob/main/migi.exe', + downloadOriginal: 'https://github.com/ZooLSmith/MIGI3/blob/main/migi.exe', + image: '/icon/migi.png', + }, + { + title: 'Voukoder', + url: 'https://www.voukoder.org', + description: 'PR、AE、Vegas等软件的编码导出插件', + background: true, + downloadOriginal: 'https://www.voukoder.org/forum/thread/783-downloads-instructions/', + image: '/icon/voukoder.png', + }, + { + title: 'ReShade AFX', + githubRepo: 'advancedfx/ReShade_advancedfx', + url: 'https://github.com/advancedfx/ReShade_advancedfx', + description: '连接HLAE录制的ReShade插件', + background: false, + downloadCdn: 'https://api.upup.cool/get/reshade-afx', + downloadOriginal: 'https://github.com/advancedfx/ReShade_advancedfx/releases/latest', + image: '/icon/reshade.png', + }, + { + title: 'ReShade', + url: 'https://reshade.me', + description: '重塑你的游戏画面', + background: false, + downloadOriginal: 'https://reshade.me', + image: '/icon/reshade.png', + }, + { + title: 'ShanaEncoder', + url: 'https://shana.pe.kr/shanaencoder_summary', + description: '方便好用的转码压制图形工具', + background: true, + downloadOriginal: 'https://shana.pe.kr/shanaencoder_download', + icon: 'pi pi-github', + }, + { + title: 'SRadar', + githubRepo: 'zuoshipinSHEKL/SRadar', + url: 'https://sdr.hlae.cn', + description: 'Shekl制作的简易雷达', + background: true, + downloadCdn: 'https://api.upup.cool/get/sradar', + downloadOriginal: 'https://github.com/zuoshipinSHEKL/SRadar/releases/latest', + icon: 'pi pi-map', + }, +] + +async function main() { + console.log('Start seeding ...') + + await prisma.portal.deleteMany() + await prisma.resource.deleteMany() + + for (let i = 0; i < portalData.length; i++) { + const portal = portalData[i] + const p = await prisma.portal.create({ + data: { + ...portal, + order: i, + }, + }) + console.log(`Created portal with id: ${p.id}`) + } + + for (let i = 0; i < resourceData.length; i++) { + const resource = resourceData[i] + const r = await prisma.resource.create({ + data: { + ...resource, + order: i, + }, + }) + console.log(`Created resource with id: ${r.id}`) + } + + console.log('Seeding finished.') +} + +main() + .then(async () => { + await prisma.$disconnect() + }) + .catch(async (e) => { + console.error(e) + await prisma.$disconnect() + process.exit(1) + }) diff --git a/backend/src/auth.ts b/backend/src/auth.ts index 97cf32b..b9f0650 100644 --- a/backend/src/auth.ts +++ b/backend/src/auth.ts @@ -7,6 +7,8 @@ export const auth = betterAuth({ provider: 'postgresql', }), basePath: '/api/auth', + baseURL: process.env.BETTER_AUTH_URL || 'http://localhost:3001', + trustedOrigins: ['http://localhost:5173'], emailAndPassword: { enabled: true, }, diff --git a/backend/src/index.ts b/backend/src/index.ts index 9326380..d408a03 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -4,6 +4,7 @@ import { auth } from './auth' import { prisma } from './prisma' import { resources } from './resources' import { posts } from './posts' +import { portals } from './portals' const app = new Elysia() .use( @@ -17,6 +18,7 @@ const app = new Elysia() .mount(auth.handler) .get('/health', () => 'OK') .use(resources) + .use(portals) .use(posts) .get('/user/:id', async ({ params }) => { const user = await prisma.user.findUnique({ diff --git a/backend/src/portals.ts b/backend/src/portals.ts new file mode 100644 index 0000000..f70a06f --- /dev/null +++ b/backend/src/portals.ts @@ -0,0 +1,49 @@ +import { Elysia, t } from 'elysia' +import { prisma } from './prisma' + +export const portals = new Elysia({ prefix: '/portals' }) + .get('/', async () => { + return await prisma.portal.findMany({ + orderBy: { order: 'asc' }, + }) + }) + .post( + '/', + async ({ body }) => { + return await prisma.portal.create({ + data: body, + }) + }, + { + body: t.Object({ + title: t.String(), + url: t.String(), + description: t.Optional(t.String()), + icon: t.Optional(t.String()), + background: t.Optional(t.Boolean()), + }), + }, + ) + .put( + '/:id', + async ({ params: { id }, body }) => { + return await prisma.portal.update({ + where: { id }, + data: body, + }) + }, + { + body: t.Object({ + title: t.Optional(t.String()), + url: t.Optional(t.String()), + description: t.Optional(t.String()), + icon: t.Optional(t.String()), + background: t.Optional(t.Boolean()), + }), + }, + ) + .delete('/:id', async ({ params: { id } }) => { + return await prisma.portal.delete({ + where: { id }, + }) + }) diff --git a/backend/src/resources.ts b/backend/src/resources.ts index 1f869ee..7b3fd55 100644 --- a/backend/src/resources.ts +++ b/backend/src/resources.ts @@ -4,7 +4,7 @@ import { prisma } from './prisma' export const resources = new Elysia({ prefix: '/resources' }) .get('/', async () => { return await prisma.resource.findMany({ - orderBy: { createdAt: 'desc' }, + orderBy: { order: 'asc' }, }) }) .post( @@ -21,6 +21,12 @@ export const resources = new Elysia({ prefix: '/resources' }) url: t.String(), icon: t.Optional(t.String()), category: t.Optional(t.String()), + githubRepo: t.Optional(t.String()), + downloadCdn: t.Optional(t.String()), + downloadOriginal: t.Optional(t.String()), + version: t.Optional(t.String()), + background: t.Optional(t.Boolean()), + image: t.Optional(t.String()), }), }, ) @@ -39,6 +45,12 @@ export const resources = new Elysia({ prefix: '/resources' }) url: t.Optional(t.String()), icon: t.Optional(t.String()), category: t.Optional(t.String()), + githubRepo: t.Optional(t.String()), + downloadCdn: t.Optional(t.String()), + downloadOriginal: t.Optional(t.String()), + version: t.Optional(t.String()), + background: t.Optional(t.Boolean()), + image: t.Optional(t.String()), }), }, ) diff --git a/bun.lock b/bun.lock index b04b5c1..8f742aa 100644 --- a/bun.lock +++ b/bun.lock @@ -49,30 +49,34 @@ "name": "web", "version": "0.1.0", "dependencies": { - "@elysiajs/eden": "^1.0.0", + "@elysiajs/eden": "^1.4.8", "@heroicons/react": "^2.2.0", "@heroui/react": "^3.0.0-beta.8", "@heroui/styles": "^3.0.0-beta.8", "@tailwindcss/postcss": "^4.2.1", + "ahooks": "^3.9.6", "better-auth": "^1.5.4", + "lucide-react": "^0.577.0", "postcss": "^8.5.8", - "react": "^19", - "react-dom": "^19", - "react-router": "^7.0.0", - "react-router-dom": "^7.0.0", + "primeicons": "^7.0.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-router": "^7.13.1", + "react-router-dom": "^7.13.1", "swr": "^2.4.1", + "tailwind-merge": "^3.5.0", "tailwind-variants": "^3.2.2", "tailwindcss": "^4.2.1", - "vite-plugin-pages": "^0.32.0", + "vite-plugin-pages": "^0.32.5", "zustand": "^5.0.11", }, "devDependencies": { - "@types/react": "^19", - "@types/react-dom": "^19", - "@vitejs/plugin-react": "^4.0.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^4.7.0", "backend": "workspace:*", "eslint": "^10.0.3", - "vite": "^5.0.0", + "vite": "^5.4.21", }, }, }, @@ -111,6 +115,8 @@ "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], + "@babel/runtime": ["@babel/runtime@7.28.6", "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.6.tgz", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + "@babel/template": ["@babel/template@7.28.6", "https://registry.npmmirror.com/@babel/template/-/template-7.28.6.tgz", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], "@babel/traverse": ["@babel/traverse@7.29.0", "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.0.tgz", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], @@ -693,6 +699,8 @@ "@types/estree": ["@types/estree@1.0.8", "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/js-cookie": ["@types/js-cookie@3.0.6", "https://registry.npmmirror.com/@types/js-cookie/-/js-cookie-3.0.6.tgz", {}, "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/ms": ["@types/ms@2.1.0", "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], @@ -755,6 +763,8 @@ "acorn-jsx": ["acorn-jsx@5.3.2", "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + "ahooks": ["ahooks@3.9.6", "https://registry.npmmirror.com/ahooks/-/ahooks-3.9.6.tgz", { "dependencies": { "@babel/runtime": "^7.21.0", "@types/js-cookie": "^3.0.6", "dayjs": "^1.9.1", "intersection-observer": "^0.12.0", "js-cookie": "^3.0.5", "lodash": "^4.17.21", "react-fast-compare": "^3.2.2", "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.0.0", "tslib": "^2.4.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Mr7f05swd5SmKlR9SZo5U6M0LsL4ErweLzpdgXjA1JPmnZ78Vr6wzx0jUtvoxrcqGKYnX0Yjc02iEASVxHFPjQ=="], + "ajv": ["ajv@6.14.0", "https://registry.npmmirror.com/ajv/-/ajv-6.14.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], "ansi-regex": ["ansi-regex@5.0.1", "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -859,6 +869,8 @@ "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "https://registry.npmmirror.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + "dayjs": ["dayjs@1.11.19", "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="], + "debug": ["debug@4.4.3", "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decimal.js": ["decimal.js@10.6.0", "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], @@ -1071,6 +1083,8 @@ "internal-slot": ["internal-slot@1.1.0", "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + "intersection-observer": ["intersection-observer@0.12.2", "https://registry.npmmirror.com/intersection-observer/-/intersection-observer-0.12.2.tgz", {}, "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg=="], + "intl-messageformat": ["intl-messageformat@10.7.18", "https://registry.npmmirror.com/intl-messageformat/-/intl-messageformat-10.7.18.tgz", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "@formatjs/fast-memoize": "2.2.7", "@formatjs/icu-messageformat-parser": "2.11.4", "tslib": "^2.8.0" } }, "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g=="], "is-array-buffer": ["is-array-buffer@3.0.5", "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], @@ -1139,6 +1153,8 @@ "js-base64": ["js-base64@3.7.8", "https://registry.npmmirror.com/js-base64/-/js-base64-3.7.8.tgz", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="], + "js-cookie": ["js-cookie@3.0.5", "https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.5.tgz", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="], + "js-tokens": ["js-tokens@4.0.0", "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "jsesc": ["jsesc@3.1.0", "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], @@ -1201,6 +1217,8 @@ "lru.min": ["lru.min@1.1.4", "https://registry.npmmirror.com/lru.min/-/lru.min-1.1.4.tgz", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], + "lucide-react": ["lucide-react@0.577.0", "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.577.0.tgz", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A=="], + "magic-string": ["magic-string@0.30.21", "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "math-intrinsics": ["math-intrinsics@1.1.0", "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -1325,6 +1343,8 @@ "prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.7.2", "https://registry.npmmirror.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.7.2.tgz", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-svelte"] }, "sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA=="], + "primeicons": ["primeicons@7.0.0", "https://registry.npmmirror.com/primeicons/-/primeicons-7.0.0.tgz", {}, "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw=="], + "prisma": ["prisma@7.4.2", "https://registry.npmmirror.com/prisma/-/prisma-7.4.2.tgz", { "dependencies": { "@prisma/config": "7.4.2", "@prisma/dev": "0.20.0", "@prisma/engines": "7.4.2", "@prisma/studio-core": "0.13.1", "mysql2": "3.15.3", "postgres": "3.4.7" }, "peerDependencies": { "better-sqlite3": ">=9.0.0", "typescript": ">=5.4.0" }, "optionalPeers": ["better-sqlite3", "typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-2bP8Ruww3Q95Z2eH4Yqh4KAENRsj/SxbdknIVBfd6DmjPwmpsC4OVFMLOeHt6tM3Amh8ebjvstrUz3V/hOe1dA=="], "promise-limit": ["promise-limit@2.7.0", "https://registry.npmmirror.com/promise-limit/-/promise-limit-2.7.0.tgz", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="], @@ -1351,6 +1371,8 @@ "react-dom": ["react-dom@19.2.4", "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.4.tgz", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + "react-fast-compare": ["react-fast-compare@3.2.2", "https://registry.npmmirror.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="], + "react-is": ["react-is@16.13.1", "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "react-refresh": ["react-refresh@0.17.0", "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], @@ -1373,6 +1395,8 @@ "require-directory": ["require-directory@2.1.1", "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "resize-observer-polyfill": ["resize-observer-polyfill@1.5.1", "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", {}, "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="], + "resolve": ["resolve@2.0.0-next.6", "https://registry.npmmirror.com/resolve/-/resolve-2.0.0-next.6.tgz", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="], "retry": ["retry@0.12.0", "https://registry.npmmirror.com/retry/-/retry-0.12.0.tgz", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], @@ -1397,6 +1421,8 @@ "scheduler": ["scheduler@0.27.0", "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "screenfull": ["screenfull@5.2.0", "https://registry.npmmirror.com/screenfull/-/screenfull-5.2.0.tgz", {}, "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA=="], + "semver": ["semver@6.3.1", "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "seq-queue": ["seq-queue@0.0.5", "https://registry.npmmirror.com/seq-queue/-/seq-queue-0.0.5.tgz", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="], @@ -1465,7 +1491,7 @@ "swr": ["swr@2.4.1", "https://registry.npmmirror.com/swr/-/swr-2.4.1.tgz", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA=="], - "tailwind-merge": ["tailwind-merge@3.4.0", "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-3.4.0.tgz", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], + "tailwind-merge": ["tailwind-merge@3.5.0", "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-3.5.0.tgz", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="], "tailwind-variants": ["tailwind-variants@3.2.2", "https://registry.npmmirror.com/tailwind-variants/-/tailwind-variants-3.2.2.tgz", { "peerDependencies": { "tailwind-merge": ">=3.0.0", "tailwindcss": "*" }, "optionalPeers": ["tailwind-merge"] }, "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg=="], @@ -1575,6 +1601,8 @@ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@heroui/react/tailwind-merge": ["tailwind-merge@3.4.0", "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-3.4.0.tgz", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], + "@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.4.2", "https://registry.npmmirror.com/@prisma/get-platform/-/get-platform-7.4.2.tgz", { "dependencies": { "@prisma/debug": "7.4.2" } }, "sha512-UTnChXRwiauzl/8wT4hhe7Xmixja9WE28oCnGpBtRejaHhvekx5kudr3R4Y9mLSA0kqGnAMeyTiKwDVMjaEVsw=="], "@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.4.2", "https://registry.npmmirror.com/@prisma/get-platform/-/get-platform-7.4.2.tgz", { "dependencies": { "@prisma/debug": "7.4.2" } }, "sha512-UTnChXRwiauzl/8wT4hhe7Xmixja9WE28oCnGpBtRejaHhvekx5kudr3R4Y9mLSA0kqGnAMeyTiKwDVMjaEVsw=="], diff --git a/web/.env.example b/web/.env.example new file mode 100644 index 0000000..ad1cb51 --- /dev/null +++ b/web/.env.example @@ -0,0 +1 @@ +VITE_API_URL="http://localhost:3001" diff --git a/web/package.json b/web/package.json index 3f79db7..d8afc6d 100644 --- a/web/package.json +++ b/web/package.json @@ -12,29 +12,33 @@ "preview": "vite preview" }, "dependencies": { - "@elysiajs/eden": "^1.0.0", + "@elysiajs/eden": "^1.4.8", "@heroicons/react": "^2.2.0", "@heroui/react": "^3.0.0-beta.8", "@heroui/styles": "^3.0.0-beta.8", "@tailwindcss/postcss": "^4.2.1", + "ahooks": "^3.9.6", "better-auth": "^1.5.4", + "lucide-react": "^0.577.0", "postcss": "^8.5.8", - "react": "^19", - "react-dom": "^19", - "react-router": "^7.0.0", - "react-router-dom": "^7.0.0", + "primeicons": "^7.0.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-router": "^7.13.1", + "react-router-dom": "^7.13.1", "swr": "^2.4.1", + "tailwind-merge": "^3.5.0", "tailwind-variants": "^3.2.2", "tailwindcss": "^4.2.1", - "vite-plugin-pages": "^0.32.0", + "vite-plugin-pages": "^0.32.5", "zustand": "^5.0.11" }, "devDependencies": { - "@types/react": "^19", - "@types/react-dom": "^19", - "@vitejs/plugin-react": "^4.0.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^4.7.0", "backend": "workspace:*", "eslint": "^10.0.3", - "vite": "^5.0.0" + "vite": "^5.4.21" } } diff --git a/web/src/api/client.ts b/web/src/api/client.ts index d84aaef..9e0a148 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -2,4 +2,4 @@ import { edenTreaty } from '@elysiajs/eden' import type { App } from 'backend' // 创建 Eden 客户端,自动推断类型 -export const api = edenTreaty('http://localhost:3001') +export const api = edenTreaty(import.meta.env.VITE_API_URL || 'http://localhost:3001') diff --git a/web/src/app/index.tsx b/web/src/app/index.tsx index a96ab99..2d337dc 100644 --- a/web/src/app/index.tsx +++ b/web/src/app/index.tsx @@ -1,77 +1,15 @@ import { useState } from 'react' -import { useResources, Resource } from '../hooks/useApi' -import { CustomCard } from '../components/CustomCard' +import { Resource, usePortals } from '../hooks/useApi' import { Hero } from '../components/Hero' -import { Button, Chip } from '@heroui/react' -import { buttonVariants } from '@heroui/styles' +import { Button } from '@heroui/react' import { CreateResourceModal } from '../components/CreateResourceModal' import { EditResourceModal } from '../components/EditResourceModal' import { useSession } from '../lib/auth-client' -import { - ChatBubbleLeftEllipsisIcon, - BookOpenIcon, - BookmarkIcon, - QuestionMarkCircleIcon, - WrenchScrewdriverIcon, - ListBulletIcon, - VideoCameraIcon, - UserGroupIcon, -} from '@heroicons/react/24/outline' -import { Link as RouterLink } from 'react-router-dom' - -const PORTALS = [ - { - title: '中文论坛', - description: 'HLAE中文交流社区', - icon: ChatBubbleLeftEllipsisIcon, - url: 'https://bbs.hlae.site', - }, - { - title: '官方Wiki', - description: '权威,但是英文 orz', - icon: BookOpenIcon, - url: 'https://github.com/advancedfx/advancedfx/wiki', - }, - { - title: '新版文档', - description: '新版 advancedfx 文档,建设中', - icon: BookmarkIcon, - url: 'https://docs.hlae.site', - }, - { - title: '官方Discord', - description: '和开发者近距离交流', - icon: UserGroupIcon, - url: 'https://discord.gg/advancedfx', - }, - { - title: '问题与建议提交', - description: 'tnnd 为什么不更新', - icon: QuestionMarkCircleIcon, - url: 'https://github.com/advancedfx/advancedfx/issues', - }, - { - title: 'HUD生成器', - description: '击杀信息和准星生成工具', - icon: WrenchScrewdriverIcon, - url: '/hud-generator', - }, - { - title: '击杀信息生成', - description: 'CS2 · CS 击杀信息生成工具(测试)', - icon: ListBulletIcon, - url: '/demo', - }, - { - title: 'HLTV', - description: 'CSGO新闻、数据、录像', - icon: VideoCameraIcon, - url: 'https://www.hltv.org', - }, -] +import { Card } from '../components/Card' +import { ResourceCardList } from '../components/ResourceCardList' export default function HomePage() { - const { resources, isLoading } = useResources() + const { portals, isLoading: isPortalsLoading } = usePortals() const [isModalOpen, setIsModalOpen] = useState(false) const [isEditModalOpen, setIsEditModalOpen] = useState(false) const [selectedResource, setSelectedResource] = useState( @@ -86,46 +24,22 @@ export default function HomePage() { {/* Portals Section */}

传送门

-
- {PORTALS.map((portal) => { - const isExternal = portal.url.startsWith('http') - const Content = ( - -
-
- -
-
-

{portal.title}

-

- {portal.description} -

-
-
-
- ) - - return isExternal ? ( - - {Content} - - ) : ( - - {Content} - - ) - })} -
+ {isPortalsLoading ? ( +
加载中...
+ ) : ( +
    + {portals?.map((portal) => ( + + ))} +
+ )}
{/* Resources Section */} @@ -135,7 +49,7 @@ export default function HomePage() { {session && ( - )} - - - ))} - {!resources?.length && ( -
- 暂无资源 -
- )} - - )} + { + setSelectedResource(resource) + setIsEditModalOpen(true) + }} + /> diff --git a/web/src/app/login/index.tsx b/web/src/app/login/index.tsx index 1e4095c..c53cdfe 100644 --- a/web/src/app/login/index.tsx +++ b/web/src/app/login/index.tsx @@ -87,6 +87,30 @@ export default function LoginPage() { > 登录 +
+
+ 或者 +
+
+ { + if (session?.user) { + setName(session.user.name || '') + } else if (!isPending && !session) { + navigate('/login') + } + }, [session, isPending, navigate]) + + if (isPending || !session) { + return ( +
+ 加载中... +
+ ) + } + + const handleUpdateProfile = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + try { + await user.update({ + name, + }) + toast.success('个人信息更新成功') + setIsEditing(false) + } catch (err) { + console.error(err) + toast.danger('更新失败') + } finally { + setLoading(false) + } + } + + return ( +
+ + + 个人资料 + + +
+ + + + {(session.user.name || 'U').slice(0, 2).toUpperCase()} + + + +
+ +
+
+
+ + + + + + + + + + + + + + + +
+ +
+ {isEditing ? ( + <> + + + + ) : ( + + )} +
+
+
+
+
+
+ ) +} diff --git a/web/src/app/register/index.tsx b/web/src/app/register/index.tsx index fe64bf1..f5e4af8 100644 --- a/web/src/app/register/index.tsx +++ b/web/src/app/register/index.tsx @@ -88,6 +88,31 @@ export default function RegisterPage() { > 注册 +
+
+ 或者 +
+
+ void + action?: React.ReactNode +} + +const H4 = ({ + className, + children, +}: { + className?: string + children: React.ReactNode +}) =>

{children}

+ +const LinkComponent = ({ + children, + className, + url, +}: { + children: React.ReactNode + className?: string + url: string +}) => { + const isExternal = url.startsWith('http') + if (isExternal) { + return ( + + {children} + + ) + } + return ( + + {children} + + ) +} + +export function Card({ + title, + url, + description, + icon, + version, + background, + downloadCdn, + downloadOriginal, + image, + className, + children, + onVersionClick, + action, +}: CardProps) { + return ( +
  • +
    + +
    + {icon && } + {image && !icon && ( + {title} + )} +
    +
    +
    + +

    + {title} +

    +
    + {version && ( + +
    + {version} +
    +
    + )} +
    +
    + +

    + {description} +

    + +
    + {downloadCdn && ( + + 加速下载 + + )} + {downloadOriginal && ( + + 原始下载 + + )} + {children} +
    + {action} +
  • + ) +} diff --git a/web/src/components/CustomCard.tsx b/web/src/components/CustomCard.tsx deleted file mode 100644 index f888e1c..0000000 --- a/web/src/components/CustomCard.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Card } from '@heroui/react' -import { tv, type VariantProps } from 'tailwind-variants' -import React from 'react' - -// Define custom styles -const customStyles = tv({ - variants: { - variant: { - glass: - 'bg-white/70 backdrop-blur-md border-white/20 shadow-xl dark:bg-black/70 dark:border-white/10', - neon: 'border border-blue-500 shadow-[0_0_15px_rgba(59,130,246,0.5)] bg-gray-900/90 text-white', - minimal: - 'border-none shadow-none bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors', - modern: - 'bg-default-50/40 hover:bg-default-100/60 transition-all duration-500 border-none shadow-sm hover:shadow-xl hover:-translate-y-1 rounded-3xl', - }, - }, - defaultVariants: { - variant: 'modern', - }, -}) - -type CustomCardProps = React.ComponentProps & - VariantProps - -// Create the custom component -export function CustomCard({ className, variant, ...props }: CustomCardProps) { - // Use custom styles and merge with className - return -} - -// Attach subcomponents to the custom component for easier usage -CustomCard.Header = Card.Header -CustomCard.Content = Card.Content -CustomCard.Footer = Card.Footer -CustomCard.Title = Card.Title -CustomCard.Description = Card.Description diff --git a/web/src/components/Footer.tsx b/web/src/components/Footer.tsx new file mode 100644 index 0000000..b09cbe4 --- /dev/null +++ b/web/src/components/Footer.tsx @@ -0,0 +1,37 @@ +export default function Footer() { + return ( +
    +
    + + Presented by{' '} + + Purple-CSGO + {' '} + ©{new Date().getFullYear()} + + + +
    +
    + ) +} + +type BeianProps = { + record?: string + icp?: string +} + +function Beian({ record, icp }: BeianProps) { + return ( + + ) +} diff --git a/web/src/components/Hero.tsx b/web/src/components/Hero.tsx index 7cdd6fb..da84c8e 100644 --- a/web/src/components/Hero.tsx +++ b/web/src/components/Hero.tsx @@ -1,41 +1,31 @@ -import { WrenchScrewdriverIcon } from '@heroicons/react/24/outline' -import { buttonVariants } from '@heroui/styles' - export const Hero = () => { return ( -
    -

    - HLAE中文站 -

    -

    - CS +

    +

    HLAE中文站

    + +

    + CS 等起源引擎游戏的影片制作工具 - HLAE的中文门户网站 + HLAE + 的中文门户网站

    -
    + +
    - + HLAE Logo 官方网站
    加载中...
    + } + + if (!resources?.length) { + return
    暂无资源
    + } + + return ( +
      + {resources.map((item) => { + const releaseInfo = item.githubRepo + ? repoReleaseMap.get(item.githubRepo) + : undefined + + const cardProps: CardProps = { + title: item.title, + url: item.url, + description: item.description, + icon: item.icon, + background: item.background, + githubRepo: item.githubRepo, + downloadCdn: item.downloadCdn, + downloadOriginal: item.downloadOriginal, + image: item.image, + version: releaseInfo?.latest_version || item.version, + onVersionClick: releaseInfo + ? (e) => { + e.preventDefault() + window.open( + `https://github.com/${item.githubRepo}/releases/tag/${releaseInfo.latest_version}`, + '_blank', + ) + } + : undefined, + action: + session && onEdit ? ( + + ) : undefined, + } + + return + })} +
    + ) +} diff --git a/web/src/components/SiteNavbar.tsx b/web/src/components/SiteNavbar.tsx index a546026..f44b2c9 100644 --- a/web/src/components/SiteNavbar.tsx +++ b/web/src/components/SiteNavbar.tsx @@ -1,25 +1,31 @@ -import { Button, Avatar, Dropdown, Label } from '@heroui/react' -import { buttonVariants } from '@heroui/styles' -import { useEffect, useState } from 'react' +import { Link, useLocation } from 'react-router-dom' +import { useScroll } from 'ahooks' +import { twMerge } from 'tailwind-merge' +import { Moon, Sun, Menu, X } from 'lucide-react' +import { Button, Avatar, Dropdown } from '@heroui/react' +import { useState } from 'react' import { useSession, signOut } from '../lib/auth-client' -import { SunIcon, MoonIcon } from '@heroicons/react/24/outline' -import { Link as RouterLink } from 'react-router-dom' + +function isBrowser() { + return !!(typeof window !== 'undefined' && window.document && window.document.createElement) +} export function SiteNavbar() { + const scroll = useScroll(isBrowser() ? document : null) + const location = useLocation() + const pathname = location.pathname const { data: session } = useSession() + const [isMenuOpen, setIsMenuOpen] = useState(false) + const [theme, setTheme] = useState<'light' | 'dark'>(() => { if (typeof window !== 'undefined') { - return document.documentElement.classList.contains('dark') - ? 'dark' - : 'light' + const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null + if (savedTheme) return savedTheme + return document.documentElement.classList.contains('dark') ? 'dark' : 'light' } return 'light' }) - useEffect(() => { - // Sync theme on mount if needed, but the state is already initialized - }, []) - const toggleTheme = () => { const nextTheme = theme === 'dark' ? 'light' : 'dark' document.documentElement.classList.remove('light', 'dark') @@ -29,101 +35,168 @@ export function SiteNavbar() { setTheme(nextTheme) } + const menuItems = [ + { name: '主页', href: '/' }, + { name: '击杀生成', href: '/demo' }, + { name: '关于', href: '/about' }, + ] + + const shouldShow = pathname !== '/' || (scroll?.top || 0) > 140 + return ( - + + {/* Mobile Menu */} + <> +
    setIsMenuOpen(false)} + aria-hidden={!isMenuOpen} + /> +
    +
    +
    +

    菜单

    + +
    + +
    +
    + + ) } diff --git a/web/src/hooks/useApi.ts b/web/src/hooks/useApi.ts index b4001c8..acd3def 100644 --- a/web/src/hooks/useApi.ts +++ b/web/src/hooks/useApi.ts @@ -8,6 +8,25 @@ export interface Resource { url: string icon: string | null category: string | null + githubRepo: string | null + downloadCdn: string | null + downloadOriginal: string | null + version: string | null + background: boolean + image: string | null + order: number + createdAt: string | Date + updatedAt: string | Date +} + +export interface Portal { + id: string + title: string + url: string + description: string | null + icon: string | null + background: boolean + order: number createdAt: string | Date updatedAt: string | Date } @@ -54,6 +73,21 @@ export function useResources() { } } +export function usePortals() { + const { data, error, isLoading, mutate } = useSWR('/portals', async () => { + const { data, error } = await api.portals.get() + if (error) throw error + return data + }) + + return { + portals: data, + isLoading, + isError: error, + mutate, + } +} + export function usePosts() { const { data, error, isLoading, mutate } = useSWR('/posts', async () => { const { data, error } = await api.posts.get() diff --git a/web/src/layout.tsx b/web/src/layout.tsx index 1bb2a0e..8c0c3bf 100644 --- a/web/src/layout.tsx +++ b/web/src/layout.tsx @@ -1,6 +1,8 @@ import { Toast } from '@heroui/react' +import 'primeicons/primeicons.css' import './globals.css' import { SiteNavbar } from './components/SiteNavbar' +import Footer from './components/Footer' export default function RootLayout({ children, @@ -11,7 +13,8 @@ export default function RootLayout({
    -
    {children}
    +
    {children}
    +
    ) } diff --git a/web/src/lib/auth-client.ts b/web/src/lib/auth-client.ts index 2c191d1..530eecf 100644 --- a/web/src/lib/auth-client.ts +++ b/web/src/lib/auth-client.ts @@ -5,3 +5,4 @@ export const authClient = createAuthClient({ }) export const { useSession, signIn, signUp, signOut } = authClient +export const user = authClient.user diff --git a/web/src/lib/github.ts b/web/src/lib/github.ts new file mode 100644 index 0000000..efc7fdc --- /dev/null +++ b/web/src/lib/github.ts @@ -0,0 +1,37 @@ +export interface LatestRelease { + repo: string + latest_version: string + changelog: string + published_at: string + attachments: string[] +} + +export interface ApiResponse { + results: Array<{ + repo: string + success: boolean + latest_release?: LatestRelease + error?: string + }> +} + +export async function fetchResourceReleaseData( + repos: string[], +): Promise { + const response = await fetch('https://gh-info.okk.cool/repos/batch', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + repos: repos, + fields: ['latest_release'], + }), + }) + + if (!response.ok) { + throw new Error(`API 请求失败: ${response.status}`) + } + + return await response.json() +}