chore: 统一代码格式并配置开发工具
- 添加 ESLint 和 Prettier 配置以统一代码风格 - 配置项目级 TypeScript 设置 - 更新前后端依赖版本 - 修复代码格式问题(引号、分号、尾随逗号等) - 优化文件结构和导入路径
This commit is contained in:
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"plugins": ["prettier-plugin-tailwindcss"]
|
||||||
|
}
|
||||||
@@ -4,101 +4,87 @@
|
|||||||
|
|
||||||
## 1. 项目初始化与依赖配置
|
## 1. 项目初始化与依赖配置
|
||||||
|
|
||||||
* [ ] **检查与安装依赖**
|
- [ ] **检查与安装依赖**
|
||||||
|
- [ ] Web 端安装 `swr`, `zustand`。
|
||||||
|
|
||||||
* [ ] Web 端安装 `swr`, `zustand`。
|
- [ ] 确认 `backend` 和 `web` 的 workspace 关联,确保类型共享。
|
||||||
|
|
||||||
* [ ] 确认 `backend` 和 `web` 的 workspace 关联,确保类型共享。
|
- [ ] **类型安全配置 (Eden)**
|
||||||
|
- [ ] 在 Backend 导出 `App` 类型。
|
||||||
|
|
||||||
* [ ] **类型安全配置 (Eden)**
|
- [ ] 在 Web 端正确引入 `App` 类型以实现端到端类型安全。
|
||||||
|
|
||||||
* [ ] 在 Backend 导出 `App` 类型。
|
- [ ] **环境变量配置**
|
||||||
|
- [ ] 配置 Backend `.env` (数据库路径, Auth Secret 等)。
|
||||||
|
|
||||||
* [ ] 在 Web 端正确引入 `App` 类型以实现端到端类型安全。
|
- [ ] 配置 Web `.env` (API 地址等)。
|
||||||
|
|
||||||
* [ ] **环境变量配置**
|
|
||||||
|
|
||||||
* [ ] 配置 Backend `.env` (数据库路径, Auth Secret 等)。
|
|
||||||
|
|
||||||
* [ ] 配置 Web `.env` (API 地址等)。
|
|
||||||
|
|
||||||
## 2. 数据库与后端基础
|
## 2. 数据库与后端基础
|
||||||
|
|
||||||
* [ ] **Prisma Schema 设计**
|
- [ ] **Prisma Schema 设计**
|
||||||
|
- [ ] 完善 `User` 模型 (适配 better-auth)。
|
||||||
|
|
||||||
* [ ] 完善 `User` 模型 (适配 better-auth)。
|
- [ ] 设计 `Resource` (资源/传送门) 模型:包含标题、描述、链接、图标/图片、分类等字段。
|
||||||
|
|
||||||
* [ ] 设计 `Resource` (资源/传送门) 模型:包含标题、描述、链接、图标/图片、分类等字段。
|
- [ ] 设计 `Post` (帖子) 模型:包含标题、内容、作者、发布时间等。
|
||||||
|
|
||||||
* [ ] 设计 `Post` (帖子) 模型:包含标题、内容、作者、发布时间等。
|
- [ ] 执行 `bun prisma migrate dev` 生成数据库表。
|
||||||
|
|
||||||
* [ ] 执行 `bun prisma migrate dev` 生成数据库表。
|
- [ ] **后端核心功能**
|
||||||
|
- [ ] **Auth**: 集成 `better-auth`,实现注册、登录、注销、获取当前用户接口。
|
||||||
|
|
||||||
* [ ] **后端核心功能**
|
- [ ] **Resources**: 实现资源的增删改查 (CRUD) 接口。
|
||||||
|
|
||||||
* [ ] **Auth**: 集成 `better-auth`,实现注册、登录、注销、获取当前用户接口。
|
- [ ] **Posts**: 实现帖子的增删改查接口。
|
||||||
|
|
||||||
* [ ] **Resources**: 实现资源的增删改查 (CRUD) 接口。
|
- [ ] **API 导出**: 确保所有路由都挂载到主 App 实例。
|
||||||
|
|
||||||
* [ ] **Posts**: 实现帖子的增删改查接口。
|
|
||||||
|
|
||||||
* [ ] **API 导出**: 确保所有路由都挂载到主 App 实例。
|
|
||||||
|
|
||||||
## 3. 前端基础架构 (Web)
|
## 3. 前端基础架构 (Web)
|
||||||
|
|
||||||
* [ ] **HeroUI v3 配置**
|
- [ ] **HeroUI v3 配置**
|
||||||
|
- [ ] 确保 `tailwind.css` 和 `HeroUI` 样式正确加载。
|
||||||
|
|
||||||
* [ ] 确保 `tailwind.css` 和 `HeroUI` 样式正确加载。
|
- [ ] 配置全局 Theme (亮/暗色)。
|
||||||
|
|
||||||
* [ ] 配置全局 Theme (亮/暗色)。
|
- [ ] **状态管理 & 数据请求**
|
||||||
|
- [ ] 封装 `useClient` 或类似 Hook,结合 `eden` 和 `swr` 进行数据请求。
|
||||||
|
|
||||||
* [ ] **状态管理 & 数据请求**
|
- [ ] 使用 `zustand` 管理全局状态 (如当前用户信息、UI状态)。
|
||||||
|
|
||||||
* [ ] 封装 `useClient` 或类似 Hook,结合 `eden` 和 `swr` 进行数据请求。
|
- [ ] **路由配置**
|
||||||
|
- [ ] 使用 `react-router` 配置页面路由 (首页、登录、注册、资源页、帖子页、管理页)。
|
||||||
* [ ] 使用 `zustand` 管理全局状态 (如当前用户信息、UI状态)。
|
|
||||||
|
|
||||||
* [ ] **路由配置**
|
|
||||||
|
|
||||||
* [ ] 使用 `react-router` 配置页面路由 (首页、登录、注册、资源页、帖子页、管理页)。
|
|
||||||
|
|
||||||
## 4. 功能模块实现
|
## 4. 功能模块实现
|
||||||
|
|
||||||
* [ ] **认证模块 (Auth)**
|
- [ ] **认证模块 (Auth)**
|
||||||
|
- [ ] 实现登录页面 (Login Page)。
|
||||||
|
|
||||||
* [ ] 实现登录页面 (Login Page)。
|
- [ ] 实现注册页面 (Register Page)。
|
||||||
|
|
||||||
* [ ] 实现注册页面 (Register Page)。
|
- [ ] 实现受保护路由 (Protected Routes)。
|
||||||
|
|
||||||
* [ ] 实现受保护路由 (Protected Routes)。
|
- [ ] **资源/传送门模块 (Resources/Teleport)**
|
||||||
|
- [ ] **展示**: 使用 HeroUI `Card` 组件展示资源列表。
|
||||||
|
|
||||||
* [ ] **资源/传送门模块 (Resources/Teleport)**
|
- [ ] **编辑**: 实现“在线编辑”功能 (管理员或特定用户权限),支持添加/修改/删除资源卡片。
|
||||||
|
|
||||||
* [ ] **展示**: 使用 HeroUI `Card` 组件展示资源列表。
|
- [ ] **帖子模块 (Posts)**
|
||||||
|
- [ ] **列表**: 展示帖子列表。
|
||||||
|
|
||||||
* [ ] **编辑**: 实现“在线编辑”功能 (管理员或特定用户权限),支持添加/修改/删除资源卡片。
|
- [ ] **详情**: 展示帖子详情。
|
||||||
|
|
||||||
* [ ] **帖子模块 (Posts)**
|
- [ ] **发布**: 实现发帖编辑器和发布功能。
|
||||||
|
|
||||||
* [ ] **列表**: 展示帖子列表。
|
|
||||||
|
|
||||||
* [ ] **详情**: 展示帖子详情。
|
|
||||||
|
|
||||||
* [ ] **发布**: 实现发帖编辑器和发布功能。
|
|
||||||
|
|
||||||
## 5. 测试与优化
|
## 5. 测试与优化
|
||||||
|
|
||||||
* [ ] **功能测试**
|
- [ ] **功能测试**
|
||||||
|
- [ ] 测试完整的注册/登录流程。
|
||||||
|
|
||||||
* [ ] 测试完整的注册/登录流程。
|
- [ ] 测试资源的增删改查。
|
||||||
|
|
||||||
* [ ] 测试资源的增删改查。
|
- [ ] 测试发帖功能。
|
||||||
|
|
||||||
* [ ] 测试发帖功能。
|
- [ ] **UI/UX 优化**
|
||||||
|
- [ ] 响应式布局调整。
|
||||||
* [ ] **UI/UX 优化**
|
|
||||||
|
|
||||||
* [ ] 响应式布局调整。
|
|
||||||
|
|
||||||
* [ ] Loading 状态和错误处理。
|
|
||||||
|
|
||||||
|
- [ ] Loading 状态和错误处理。
|
||||||
|
|||||||
0
backend/dev.db
Normal file
0
backend/dev.db
Normal file
@@ -9,17 +9,24 @@
|
|||||||
"start": "NODE_ENV=production bun src/index.ts",
|
"start": "NODE_ENV=production bun src/index.ts",
|
||||||
"start:dist": "bun run dist/server",
|
"start:dist": "bun run dist/server",
|
||||||
"build": "bun build --compile --minify-whitespace --minify-syntax --target bun --outfile dist/server src/index.ts",
|
"build": "bun build --compile --minify-whitespace --minify-syntax --target bun --outfile dist/server src/index.ts",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
"typecheck": "bun --bun tsc --noEmit"
|
"typecheck": "bun --bun tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"elysia": "^1.0.0",
|
|
||||||
"prisma": "^5.0.0",
|
|
||||||
"@prisma/client": "^5.0.0",
|
|
||||||
"better-auth": "^0.7.0",
|
|
||||||
"@elysiajs/cors": "^1.0.0",
|
"@elysiajs/cors": "^1.0.0",
|
||||||
"@elysiajs/eden": "^1.0.0"
|
"@elysiajs/eden": "^1.0.0",
|
||||||
|
"@libsql/client": "^0.17.0",
|
||||||
|
"@prisma/adapter-libsql": "^7.4.2",
|
||||||
|
"@prisma/client": "^7.4.2",
|
||||||
|
"better-auth": "^1.5.4",
|
||||||
|
"dotenv": "^17.3.1",
|
||||||
|
"elysia": "^1.0.0",
|
||||||
|
"prisma": "^7.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest"
|
"@prisma/config": "^7.4.2",
|
||||||
|
"@types/bun": "latest",
|
||||||
|
"eslint": "^10.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
12
backend/prisma.config.ts
Normal file
12
backend/prisma.config.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import "dotenv/config";
|
||||||
|
import { defineConfig, env } from "@prisma/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
schema: "prisma/schema.prisma",
|
||||||
|
migrations: {
|
||||||
|
path: "prisma/migrations",
|
||||||
|
},
|
||||||
|
datasource: {
|
||||||
|
url: env("DATABASE_URL") || "file:./dev.db",
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -2,12 +2,14 @@
|
|||||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||||
|
|
||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client"
|
||||||
|
output = "../src/generated/prisma"
|
||||||
|
engineType = "client"
|
||||||
|
runtime = "bun"
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "sqlite"
|
provider = "sqlite"
|
||||||
url = "file:./dev.db"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { betterAuth } from 'better-auth';
|
import { betterAuth } from 'better-auth'
|
||||||
import { prismaAdapter } from 'better-auth/adapters/prisma';
|
import { prismaAdapter } from 'better-auth/adapters/prisma'
|
||||||
import { prisma } from './prisma';
|
import { prisma } from './prisma'
|
||||||
|
|
||||||
export const auth = betterAuth({
|
export const auth = betterAuth({
|
||||||
database: prismaAdapter(prisma, {
|
database: prismaAdapter(prisma, {
|
||||||
provider: 'sqlite'
|
provider: 'sqlite',
|
||||||
}),
|
}),
|
||||||
basePath: '/api/auth',
|
basePath: '/api/auth',
|
||||||
emailAndPassword: {
|
emailAndPassword: {
|
||||||
@@ -14,7 +14,9 @@ export const auth = betterAuth({
|
|||||||
github: {
|
github: {
|
||||||
clientId: process.env.GITHUB_CLIENT_ID || 'dummy',
|
clientId: process.env.GITHUB_CLIENT_ID || 'dummy',
|
||||||
clientSecret: process.env.GITHUB_CLIENT_SECRET || 'dummy',
|
clientSecret: process.env.GITHUB_CLIENT_SECRET || 'dummy',
|
||||||
enabled: !!(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET),
|
enabled: !!(
|
||||||
|
process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|||||||
54
backend/src/generated/prisma/browser.ts
Normal file
54
backend/src/generated/prisma/browser.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* This file should be your main import to use Prisma-related types and utilities in a browser.
|
||||||
|
* Use it to get access to models, enums, and input types.
|
||||||
|
*
|
||||||
|
* This file does not contain a `PrismaClient` class, nor several other helpers that are intended as server-side only.
|
||||||
|
* See `client.ts` for the standard, server-side entry point.
|
||||||
|
*
|
||||||
|
* 🟢 You can import this file directly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Prisma from './internal/prismaNamespaceBrowser.ts'
|
||||||
|
export { Prisma }
|
||||||
|
export * as $Enums from './enums.ts'
|
||||||
|
export * from './enums.ts';
|
||||||
|
/**
|
||||||
|
* Model User
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type User = Prisma.UserModel
|
||||||
|
/**
|
||||||
|
* Model Account
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Account = Prisma.AccountModel
|
||||||
|
/**
|
||||||
|
* Model Session
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Session = Prisma.SessionModel
|
||||||
|
/**
|
||||||
|
* Model Post
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Post = Prisma.PostModel
|
||||||
|
/**
|
||||||
|
* Model Resource
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Resource = Prisma.ResourceModel
|
||||||
|
/**
|
||||||
|
* Model Comment
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Comment = Prisma.CommentModel
|
||||||
|
/**
|
||||||
|
* Model VerificationToken
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type VerificationToken = Prisma.VerificationTokenModel
|
||||||
78
backend/src/generated/prisma/client.ts
Normal file
78
backend/src/generated/prisma/client.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* This file should be your main import to use Prisma. Through it you get access to all the models, enums, and input types.
|
||||||
|
* If you're looking for something you can import in the client-side of your application, please refer to the `browser.ts` file instead.
|
||||||
|
*
|
||||||
|
* 🟢 You can import this file directly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as process from 'node:process'
|
||||||
|
import * as path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
|
import * as runtime from "@prisma/client/runtime/client"
|
||||||
|
import * as $Enums from "./enums.ts"
|
||||||
|
import * as $Class from "./internal/class.ts"
|
||||||
|
import * as Prisma from "./internal/prismaNamespace.ts"
|
||||||
|
|
||||||
|
export * as $Enums from './enums.ts'
|
||||||
|
export * from "./enums.ts"
|
||||||
|
/**
|
||||||
|
* ## Prisma Client
|
||||||
|
*
|
||||||
|
* Type-safe database client for TypeScript
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* const prisma = new PrismaClient({
|
||||||
|
* adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL })
|
||||||
|
* })
|
||||||
|
* // Fetch zero or more Users
|
||||||
|
* const users = await prisma.user.findMany()
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Read more in our [docs](https://pris.ly/d/client).
|
||||||
|
*/
|
||||||
|
export const PrismaClient = $Class.getPrismaClientClass()
|
||||||
|
export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
||||||
|
export { Prisma }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model User
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type User = Prisma.UserModel
|
||||||
|
/**
|
||||||
|
* Model Account
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Account = Prisma.AccountModel
|
||||||
|
/**
|
||||||
|
* Model Session
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Session = Prisma.SessionModel
|
||||||
|
/**
|
||||||
|
* Model Post
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Post = Prisma.PostModel
|
||||||
|
/**
|
||||||
|
* Model Resource
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Resource = Prisma.ResourceModel
|
||||||
|
/**
|
||||||
|
* Model Comment
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Comment = Prisma.CommentModel
|
||||||
|
/**
|
||||||
|
* Model VerificationToken
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type VerificationToken = Prisma.VerificationTokenModel
|
||||||
348
backend/src/generated/prisma/commonInputTypes.ts
Normal file
348
backend/src/generated/prisma/commonInputTypes.ts
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* This file exports various common sort, input & filter types that are not directly linked to a particular model.
|
||||||
|
*
|
||||||
|
* 🟢 You can import this file directly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type * as runtime from "@prisma/client/runtime/client"
|
||||||
|
import * as $Enums from "./enums.ts"
|
||||||
|
import type * as Prisma from "./internal/prismaNamespace.ts"
|
||||||
|
|
||||||
|
|
||||||
|
export type StringFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
in?: string[]
|
||||||
|
notIn?: string[]
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StringNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: string[] | null
|
||||||
|
notIn?: string[] | null
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DateTimeNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: Date[] | string[] | null
|
||||||
|
notIn?: Date[] | string[] | null
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DateTimeFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
in?: Date[] | string[]
|
||||||
|
notIn?: Date[] | string[]
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SortOrderInput = {
|
||||||
|
sort: Prisma.SortOrder
|
||||||
|
nulls?: Prisma.NullsOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StringWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
in?: string[]
|
||||||
|
notIn?: string[]
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedStringFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: string[] | null
|
||||||
|
notIn?: string[] | null
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: Date[] | string[] | null
|
||||||
|
notIn?: Date[] | string[] | null
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
in?: Date[] | string[]
|
||||||
|
notIn?: Date[] | string[]
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IntNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: number[] | null
|
||||||
|
notIn?: number[] | null
|
||||||
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IntNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: number[] | null
|
||||||
|
notIn?: number[] | null
|
||||||
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
||||||
|
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BoolFilter<$PrismaModel = never> = {
|
||||||
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BoolWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedStringFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
in?: string[]
|
||||||
|
notIn?: string[]
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedStringNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: string[] | null
|
||||||
|
notIn?: string[] | null
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: Date[] | string[] | null
|
||||||
|
notIn?: Date[] | string[] | null
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedDateTimeFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
in?: Date[] | string[]
|
||||||
|
notIn?: Date[] | string[]
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
in?: string[]
|
||||||
|
notIn?: string[]
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedStringFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedIntFilter<$PrismaModel = never> = {
|
||||||
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
in?: number[]
|
||||||
|
notIn?: number[]
|
||||||
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: string[] | null
|
||||||
|
notIn?: string[] | null
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedIntNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: number[] | null
|
||||||
|
notIn?: number[] | null
|
||||||
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: Date[] | string[] | null
|
||||||
|
notIn?: Date[] | string[] | null
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
in?: Date[] | string[]
|
||||||
|
notIn?: Date[] | string[]
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedIntNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: number[] | null
|
||||||
|
notIn?: number[] | null
|
||||||
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
||||||
|
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedFloatNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: number[] | null
|
||||||
|
notIn?: number[] | null
|
||||||
|
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedBoolFilter<$PrismaModel = never> = {
|
||||||
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
15
backend/src/generated/prisma/enums.ts
Normal file
15
backend/src/generated/prisma/enums.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* This file exports all enum related types from the schema.
|
||||||
|
*
|
||||||
|
* 🟢 You can import this file directly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// This file is empty because there are no enums in the schema.
|
||||||
|
export {}
|
||||||
264
backend/src/generated/prisma/internal/class.ts
Normal file
264
backend/src/generated/prisma/internal/class.ts
Normal file
File diff suppressed because one or more lines are too long
1278
backend/src/generated/prisma/internal/prismaNamespace.ts
Normal file
1278
backend/src/generated/prisma/internal/prismaNamespace.ts
Normal file
File diff suppressed because it is too large
Load Diff
181
backend/src/generated/prisma/internal/prismaNamespaceBrowser.ts
Normal file
181
backend/src/generated/prisma/internal/prismaNamespaceBrowser.ts
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* WARNING: This is an internal file that is subject to change!
|
||||||
|
*
|
||||||
|
* 🛑 Under no circumstances should you import this file directly! 🛑
|
||||||
|
*
|
||||||
|
* All exports from this file are wrapped under a `Prisma` namespace object in the browser.ts file.
|
||||||
|
* While this enables partial backward compatibility, it is not part of the stable public API.
|
||||||
|
*
|
||||||
|
* If you are looking for your Models, Enums, and Input Types, please import them from the respective
|
||||||
|
* model files in the `model` directory!
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as runtime from "@prisma/client/runtime/index-browser"
|
||||||
|
|
||||||
|
export type * from '../models.ts'
|
||||||
|
export type * from './prismaNamespace.ts'
|
||||||
|
|
||||||
|
export const Decimal = runtime.Decimal
|
||||||
|
|
||||||
|
|
||||||
|
export const NullTypes = {
|
||||||
|
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
|
||||||
|
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
|
||||||
|
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
|
||||||
|
*
|
||||||
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
|
*/
|
||||||
|
export const DbNull = runtime.DbNull
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
|
||||||
|
*
|
||||||
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
|
*/
|
||||||
|
export const JsonNull = runtime.JsonNull
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
|
||||||
|
*
|
||||||
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
|
*/
|
||||||
|
export const AnyNull = runtime.AnyNull
|
||||||
|
|
||||||
|
|
||||||
|
export const ModelName = {
|
||||||
|
User: 'User',
|
||||||
|
Account: 'Account',
|
||||||
|
Session: 'Session',
|
||||||
|
Post: 'Post',
|
||||||
|
Resource: 'Resource',
|
||||||
|
Comment: 'Comment',
|
||||||
|
VerificationToken: 'VerificationToken'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Enums
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const TransactionIsolationLevel = runtime.makeStrictEnum({
|
||||||
|
Serializable: 'Serializable'
|
||||||
|
} as const)
|
||||||
|
|
||||||
|
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
|
||||||
|
|
||||||
|
|
||||||
|
export const UserScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
name: 'name',
|
||||||
|
email: 'email',
|
||||||
|
emailVerified: 'emailVerified',
|
||||||
|
phone: 'phone',
|
||||||
|
phoneVerified: 'phoneVerified',
|
||||||
|
image: 'image',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const AccountScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
userId: 'userId',
|
||||||
|
type: 'type',
|
||||||
|
provider: 'provider',
|
||||||
|
providerAccountId: 'providerAccountId',
|
||||||
|
refresh_token: 'refresh_token',
|
||||||
|
access_token: 'access_token',
|
||||||
|
expires_at: 'expires_at',
|
||||||
|
token_type: 'token_type',
|
||||||
|
scope: 'scope',
|
||||||
|
id_token: 'id_token',
|
||||||
|
session_state: 'session_state'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type AccountScalarFieldEnum = (typeof AccountScalarFieldEnum)[keyof typeof AccountScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const SessionScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
sessionToken: 'sessionToken',
|
||||||
|
userId: 'userId',
|
||||||
|
expires: 'expires'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type SessionScalarFieldEnum = (typeof SessionScalarFieldEnum)[keyof typeof SessionScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const PostScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
title: 'title',
|
||||||
|
content: 'content',
|
||||||
|
userId: 'userId',
|
||||||
|
published: 'published',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type PostScalarFieldEnum = (typeof PostScalarFieldEnum)[keyof typeof PostScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const ResourceScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
title: 'title',
|
||||||
|
description: 'description',
|
||||||
|
url: 'url',
|
||||||
|
icon: 'icon',
|
||||||
|
category: 'category',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type ResourceScalarFieldEnum = (typeof ResourceScalarFieldEnum)[keyof typeof ResourceScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const CommentScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
content: 'content',
|
||||||
|
userId: 'userId',
|
||||||
|
postId: 'postId',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type CommentScalarFieldEnum = (typeof CommentScalarFieldEnum)[keyof typeof CommentScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const VerificationTokenScalarFieldEnum = {
|
||||||
|
identifier: 'identifier',
|
||||||
|
token: 'token',
|
||||||
|
expires: 'expires'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type VerificationTokenScalarFieldEnum = (typeof VerificationTokenScalarFieldEnum)[keyof typeof VerificationTokenScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const SortOrder = {
|
||||||
|
asc: 'asc',
|
||||||
|
desc: 'desc'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
|
||||||
|
|
||||||
|
|
||||||
|
export const NullsOrder = {
|
||||||
|
first: 'first',
|
||||||
|
last: 'last'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
|
||||||
|
|
||||||
18
backend/src/generated/prisma/models.ts
Normal file
18
backend/src/generated/prisma/models.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* This is a barrel export file for all models and their related types.
|
||||||
|
*
|
||||||
|
* 🟢 You can import this file directly.
|
||||||
|
*/
|
||||||
|
export type * from './models/User.ts'
|
||||||
|
export type * from './models/Account.ts'
|
||||||
|
export type * from './models/Session.ts'
|
||||||
|
export type * from './models/Post.ts'
|
||||||
|
export type * from './models/Resource.ts'
|
||||||
|
export type * from './models/Comment.ts'
|
||||||
|
export type * from './models/VerificationToken.ts'
|
||||||
|
export type * from './commonInputTypes.ts'
|
||||||
1640
backend/src/generated/prisma/models/Account.ts
Normal file
1640
backend/src/generated/prisma/models/Account.ts
Normal file
File diff suppressed because it is too large
Load Diff
1497
backend/src/generated/prisma/models/Comment.ts
Normal file
1497
backend/src/generated/prisma/models/Comment.ts
Normal file
File diff suppressed because it is too large
Load Diff
1556
backend/src/generated/prisma/models/Post.ts
Normal file
1556
backend/src/generated/prisma/models/Post.ts
Normal file
File diff suppressed because it is too large
Load Diff
1226
backend/src/generated/prisma/models/Resource.ts
Normal file
1226
backend/src/generated/prisma/models/Resource.ts
Normal file
File diff suppressed because it is too large
Load Diff
1302
backend/src/generated/prisma/models/Session.ts
Normal file
1302
backend/src/generated/prisma/models/Session.ts
Normal file
File diff suppressed because it is too large
Load Diff
1878
backend/src/generated/prisma/models/User.ts
Normal file
1878
backend/src/generated/prisma/models/User.ts
Normal file
File diff suppressed because it is too large
Load Diff
1092
backend/src/generated/prisma/models/VerificationToken.ts
Normal file
1092
backend/src/generated/prisma/models/VerificationToken.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
|||||||
import { Elysia } from 'elysia';
|
import { Elysia } from 'elysia'
|
||||||
import { cors } from '@elysiajs/cors';
|
import { cors } from '@elysiajs/cors'
|
||||||
import { auth } from './auth';
|
import { auth } from './auth'
|
||||||
import { prisma } from './prisma';
|
import { prisma } from './prisma'
|
||||||
import { resources } from './resources';
|
import { resources } from './resources'
|
||||||
import { posts } from './posts';
|
import { posts } from './posts'
|
||||||
|
|
||||||
const app = new Elysia()
|
const app = new Elysia()
|
||||||
.use(
|
.use(
|
||||||
@@ -11,8 +11,8 @@ const app = new Elysia()
|
|||||||
origin: 'http://localhost:5173',
|
origin: 'http://localhost:5173',
|
||||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||||
credentials: true,
|
credentials: true,
|
||||||
allowedHeaders: ['Content-Type', 'Authorization']
|
allowedHeaders: ['Content-Type', 'Authorization'],
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.mount(auth.handler)
|
.mount(auth.handler)
|
||||||
.get('/health', () => 'OK')
|
.get('/health', () => 'OK')
|
||||||
@@ -21,15 +21,15 @@ const app = new Elysia()
|
|||||||
.get('/user/:id', async ({ params }) => {
|
.get('/user/:id', async ({ params }) => {
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: params.id
|
id: params.id,
|
||||||
}
|
},
|
||||||
});
|
})
|
||||||
return user;
|
return user
|
||||||
});
|
})
|
||||||
|
|
||||||
app.listen(3001, () => {
|
app.listen(3001, () => {
|
||||||
console.log('Server running on port 3001');
|
console.log('Server running on port 3001')
|
||||||
});
|
})
|
||||||
|
|
||||||
export default app;
|
export default app
|
||||||
export type App = typeof app;
|
export type App = typeof app
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Elysia, t } from 'elysia';
|
import { Elysia, t } from 'elysia'
|
||||||
import { prisma } from './prisma';
|
import { prisma } from './prisma'
|
||||||
|
|
||||||
export const posts = new Elysia({ prefix: '/posts' })
|
export const posts = new Elysia({ prefix: '/posts' })
|
||||||
.get('/', async () => {
|
.get('/', async () => {
|
||||||
return await prisma.post.findMany({
|
return await prisma.post.findMany({
|
||||||
include: { user: true },
|
include: { user: true },
|
||||||
orderBy: { createdAt: 'desc' }
|
orderBy: { createdAt: 'desc' },
|
||||||
});
|
})
|
||||||
})
|
})
|
||||||
.get('/:id', async ({ params: { id } }) => {
|
.get('/:id', async ({ params: { id } }) => {
|
||||||
return await prisma.post.findUnique({
|
return await prisma.post.findUnique({
|
||||||
@@ -15,24 +15,28 @@ export const posts = new Elysia({ prefix: '/posts' })
|
|||||||
user: true,
|
user: true,
|
||||||
comments: {
|
comments: {
|
||||||
include: {
|
include: {
|
||||||
user: true
|
user: true,
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'desc'
|
createdAt: 'desc',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
|
||||||
})
|
|
||||||
.post('/', async ({ body }) => {
|
|
||||||
return await prisma.post.create({
|
|
||||||
data: body
|
|
||||||
});
|
|
||||||
}, {
|
|
||||||
body: t.Object({
|
|
||||||
title: t.String(),
|
|
||||||
content: t.String(),
|
|
||||||
userId: t.String(),
|
|
||||||
published: t.Optional(t.Boolean())
|
|
||||||
})
|
})
|
||||||
});
|
})
|
||||||
|
.post(
|
||||||
|
'/',
|
||||||
|
async ({ body }) => {
|
||||||
|
return await prisma.post.create({
|
||||||
|
data: body,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
body: t.Object({
|
||||||
|
title: t.String(),
|
||||||
|
content: t.String(),
|
||||||
|
userId: t.String(),
|
||||||
|
published: t.Optional(t.Boolean()),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from "./generated/prisma/client";
|
||||||
|
import { PrismaLibSql } from "@prisma/adapter-libsql";
|
||||||
|
import { createClient } from "@libsql/client";
|
||||||
|
|
||||||
export const prisma = new PrismaClient();
|
const libsql = createClient({
|
||||||
|
url: process.env.DATABASE_URL || "file:./prisma/dev.db",
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const adapter = new PrismaLibSql(libsql as any);
|
||||||
|
|
||||||
|
export const prisma = new PrismaClient({ adapter });
|
||||||
|
|||||||
@@ -1,41 +1,49 @@
|
|||||||
import { Elysia, t } from 'elysia';
|
import { Elysia, t } from 'elysia'
|
||||||
import { prisma } from './prisma';
|
import { prisma } from './prisma'
|
||||||
|
|
||||||
export const resources = new Elysia({ prefix: '/resources' })
|
export const resources = new Elysia({ prefix: '/resources' })
|
||||||
.get('/', async () => {
|
.get('/', async () => {
|
||||||
return await prisma.resource.findMany({
|
return await prisma.resource.findMany({
|
||||||
orderBy: { createdAt: 'desc' }
|
orderBy: { createdAt: 'desc' },
|
||||||
});
|
|
||||||
})
|
|
||||||
.post('/', async ({ body }) => {
|
|
||||||
return await prisma.resource.create({
|
|
||||||
data: body
|
|
||||||
});
|
|
||||||
}, {
|
|
||||||
body: t.Object({
|
|
||||||
title: t.String(),
|
|
||||||
description: t.Optional(t.String()),
|
|
||||||
url: t.String(),
|
|
||||||
icon: t.Optional(t.String()),
|
|
||||||
category: t.Optional(t.String())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.put('/:id', async ({ params: { id }, body }) => {
|
|
||||||
return await prisma.resource.update({
|
|
||||||
where: { id },
|
|
||||||
data: body
|
|
||||||
});
|
|
||||||
}, {
|
|
||||||
body: t.Object({
|
|
||||||
title: t.Optional(t.String()),
|
|
||||||
description: t.Optional(t.String()),
|
|
||||||
url: t.Optional(t.String()),
|
|
||||||
icon: t.Optional(t.String()),
|
|
||||||
category: t.Optional(t.String())
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
.post(
|
||||||
|
'/',
|
||||||
|
async ({ body }) => {
|
||||||
|
return await prisma.resource.create({
|
||||||
|
data: body,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
body: t.Object({
|
||||||
|
title: t.String(),
|
||||||
|
description: t.Optional(t.String()),
|
||||||
|
url: t.String(),
|
||||||
|
icon: t.Optional(t.String()),
|
||||||
|
category: t.Optional(t.String()),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.put(
|
||||||
|
'/:id',
|
||||||
|
async ({ params: { id }, body }) => {
|
||||||
|
return await prisma.resource.update({
|
||||||
|
where: { id },
|
||||||
|
data: body,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
body: t.Object({
|
||||||
|
title: t.Optional(t.String()),
|
||||||
|
description: t.Optional(t.String()),
|
||||||
|
url: t.Optional(t.String()),
|
||||||
|
icon: t.Optional(t.String()),
|
||||||
|
category: t.Optional(t.String()),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
)
|
||||||
.delete('/:id', async ({ params: { id } }) => {
|
.delete('/:id', async ({ params: { id } }) => {
|
||||||
return await prisma.resource.delete({
|
return await prisma.resource.delete({
|
||||||
where: { id }
|
where: { id },
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|||||||
8
backend/tsconfig.json
Normal file
8
backend/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["bun"]
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
54
eslint.config.js
Normal file
54
eslint.config.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
import react from 'eslint-plugin-react'
|
||||||
|
import prettier from 'eslint-config-prettier'
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
'dist',
|
||||||
|
'node_modules',
|
||||||
|
'.cursor',
|
||||||
|
'.trae',
|
||||||
|
'backend/prisma/migrations',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
prettier,
|
||||||
|
],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node,
|
||||||
|
...globals.es2020,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
react,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
'react/jsx-uses-react': 'off',
|
||||||
|
'react/react-in-jsx-scope': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{ argsIgnorePattern: '^_' },
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-explicit-any': 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
22
package.json
22
package.json
@@ -15,10 +15,28 @@
|
|||||||
"build:web": "bun run --filter=web build",
|
"build:web": "bun run --filter=web build",
|
||||||
"build:backend": "bun run --filter=backend build",
|
"build:backend": "bun run --filter=backend build",
|
||||||
"build:seq": "bun run build:backend && bun run build:web",
|
"build:seq": "bun run build:backend && bun run build:web",
|
||||||
"lint": "concurrently -n 'backend,web' 'cd backend && bun run typecheck' 'cd web && bun run lint'"
|
"lint": "concurrently -n 'backend,web' 'cd backend && bun run lint' 'cd web && bun run lint'",
|
||||||
|
"lint:fix": "concurrently -n 'backend,web' 'cd backend && bun run lint:fix' 'cd web && bun run lint:fix'",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"typecheck": "concurrently -n 'backend,web' 'cd backend && bun run typecheck' 'cd web && bun run typecheck'"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^10.0.1",
|
||||||
|
"@types/bun": "^1.3.10",
|
||||||
|
"@types/node": "^25.4.0",
|
||||||
|
"@types/react": "^19.2.14",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"bun-plugin-tailwind": "^0.1.2",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"typescript": "~5.9.3"
|
"eslint": "^10.0.3",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
|
"globals": "^17.4.0",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"typescript-eslint": "^8.57.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["web/src", "backend/src"],
|
||||||
|
"exclude": ["node_modules", "dist", "web/dist", "backend/dist"]
|
||||||
|
}
|
||||||
155
web/build.ts
155
web/build.ts
@@ -1,10 +1,11 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
import plugin from "bun-plugin-tailwind";
|
|
||||||
import { existsSync } from "fs";
|
|
||||||
import { rm } from "fs/promises";
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
import plugin from 'bun-plugin-tailwind'
|
||||||
|
import { existsSync } from 'fs'
|
||||||
|
import { rm } from 'fs/promises'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
||||||
console.log(`
|
console.log(`
|
||||||
🏗️ Bun Build Script
|
🏗️ Bun Build Script
|
||||||
|
|
||||||
@@ -29,121 +30,129 @@ Common Options:
|
|||||||
|
|
||||||
Example:
|
Example:
|
||||||
bun run build.ts --outdir=dist --minify --sourcemap=linked --external=react,react-dom
|
bun run build.ts --outdir=dist --minify --sourcemap=linked --external=react,react-dom
|
||||||
`);
|
`)
|
||||||
process.exit(0);
|
process.exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
const toCamelCase = (str: string): string => str.replace(/-([a-z])/g, g => g[1].toUpperCase());
|
const toCamelCase = (str: string): string =>
|
||||||
|
str.replace(/-([a-z])/g, (g) => g[1].toUpperCase())
|
||||||
|
|
||||||
const parseValue = (value: string): any => {
|
const parseValue = (value: string): string | number | boolean | string[] => {
|
||||||
if (value === "true") return true;
|
if (value === 'true') return true
|
||||||
if (value === "false") return false;
|
if (value === 'false') return false
|
||||||
|
|
||||||
if (/^\d+$/.test(value)) return parseInt(value, 10);
|
if (/^\d+$/.test(value)) return parseInt(value, 10)
|
||||||
if (/^\d*\.\d+$/.test(value)) return parseFloat(value);
|
if (/^\d*\.\d+$/.test(value)) return parseFloat(value)
|
||||||
|
|
||||||
if (value.includes(",")) return value.split(",").map(v => v.trim());
|
if (value.includes(',')) return value.split(',').map((v) => v.trim())
|
||||||
|
|
||||||
return value;
|
return value
|
||||||
};
|
}
|
||||||
|
|
||||||
function parseArgs(): Partial<Bun.BuildConfig> {
|
function parseArgs(): Partial<Bun.BuildConfig> {
|
||||||
const config: Partial<Bun.BuildConfig> = {};
|
const config: Record<string, unknown> = {}
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2)
|
||||||
|
|
||||||
for (let i = 0; i < args.length; i++) {
|
for (let i = 0; i < args.length; i++) {
|
||||||
const arg = args[i];
|
const arg = args[i]
|
||||||
if (arg === undefined) continue;
|
if (arg === undefined) continue
|
||||||
if (!arg.startsWith("--")) continue;
|
if (!arg.startsWith('--')) continue
|
||||||
|
|
||||||
if (arg.startsWith("--no-")) {
|
if (arg.startsWith('--no-')) {
|
||||||
const key = toCamelCase(arg.slice(5));
|
const key = toCamelCase(arg.slice(5))
|
||||||
config[key] = false;
|
config[key] = false
|
||||||
continue;
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!arg.includes("=") && (i === args.length - 1 || args[i + 1]?.startsWith("--"))) {
|
if (
|
||||||
const key = toCamelCase(arg.slice(2));
|
!arg.includes('=') &&
|
||||||
config[key] = true;
|
(i === args.length - 1 || args[i + 1]?.startsWith('--'))
|
||||||
continue;
|
) {
|
||||||
|
const key = toCamelCase(arg.slice(2))
|
||||||
|
config[key] = true
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
let key: string;
|
let key: string
|
||||||
let value: string;
|
let value: string
|
||||||
|
|
||||||
if (arg.includes("=")) {
|
if (arg.includes('=')) {
|
||||||
[key, value] = arg.slice(2).split("=", 2) as [string, string];
|
;[key, value] = arg.slice(2).split('=', 2) as [string, string]
|
||||||
} else {
|
} else {
|
||||||
key = arg.slice(2);
|
key = arg.slice(2)
|
||||||
value = args[++i] ?? "";
|
value = args[++i] ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
key = toCamelCase(key);
|
key = toCamelCase(key)
|
||||||
|
|
||||||
if (key.includes(".")) {
|
if (key.includes('.')) {
|
||||||
const [parentKey, childKey] = key.split(".");
|
const [parentKey, childKey] = key.split('.')
|
||||||
config[parentKey] = config[parentKey] || {};
|
if (parentKey && childKey) {
|
||||||
config[parentKey][childKey] = parseValue(value);
|
config[parentKey] = config[parentKey] || {}
|
||||||
|
config[parentKey][childKey] = parseValue(value)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
config[key] = parseValue(value);
|
config[key] = parseValue(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
return config as Partial<Bun.BuildConfig>
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatFileSize = (bytes: number): string => {
|
const formatFileSize = (bytes: number): string => {
|
||||||
const units = ["B", "KB", "MB", "GB"];
|
const units = ['B', 'KB', 'MB', 'GB']
|
||||||
let size = bytes;
|
let size = bytes
|
||||||
let unitIndex = 0;
|
let unitIndex = 0
|
||||||
|
|
||||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||||
size /= 1024;
|
size /= 1024
|
||||||
unitIndex++;
|
unitIndex++
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
return `${size.toFixed(2)} ${units[unitIndex]}`
|
||||||
};
|
|
||||||
|
|
||||||
console.log("\n🚀 Starting build process...\n");
|
|
||||||
|
|
||||||
const cliConfig = parseArgs();
|
|
||||||
const outdir = cliConfig.outdir || path.join(process.cwd(), "dist");
|
|
||||||
|
|
||||||
if (existsSync(outdir)) {
|
|
||||||
console.log(`🗑️ Cleaning previous build at ${outdir}`);
|
|
||||||
await rm(outdir, { recursive: true, force: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = performance.now();
|
console.log('\n🚀 Starting build process...\n')
|
||||||
|
|
||||||
const entrypoints = [...new Bun.Glob("**.html").scanSync("src")]
|
const cliConfig = parseArgs()
|
||||||
.map(a => path.resolve("src", a))
|
const outdir = cliConfig.outdir || path.join(process.cwd(), 'dist')
|
||||||
.filter(dir => !dir.includes("node_modules"));
|
|
||||||
console.log(`📄 Found ${entrypoints.length} HTML ${entrypoints.length === 1 ? "file" : "files"} to process\n`);
|
if (existsSync(outdir)) {
|
||||||
|
console.log(`🗑️ Cleaning previous build at ${outdir}`)
|
||||||
|
await rm(outdir, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = performance.now()
|
||||||
|
|
||||||
|
const entrypoints = [...new Bun.Glob('**.html').scanSync('src')]
|
||||||
|
.map((a) => path.resolve('src', a))
|
||||||
|
.filter((dir) => !dir.includes('node_modules'))
|
||||||
|
console.log(
|
||||||
|
`📄 Found ${entrypoints.length} HTML ${entrypoints.length === 1 ? 'file' : 'files'} to process\n`,
|
||||||
|
)
|
||||||
|
|
||||||
const result = await Bun.build({
|
const result = await Bun.build({
|
||||||
entrypoints,
|
entrypoints,
|
||||||
outdir,
|
outdir,
|
||||||
plugins: [plugin],
|
plugins: [plugin],
|
||||||
minify: true,
|
minify: true,
|
||||||
target: "browser",
|
target: 'browser',
|
||||||
sourcemap: "linked",
|
sourcemap: 'linked',
|
||||||
define: {
|
define: {
|
||||||
"process.env.NODE_ENV": JSON.stringify("production"),
|
'process.env.NODE_ENV': JSON.stringify('production'),
|
||||||
},
|
},
|
||||||
...cliConfig,
|
...cliConfig,
|
||||||
});
|
})
|
||||||
|
|
||||||
const end = performance.now();
|
const end = performance.now()
|
||||||
|
|
||||||
const outputTable = result.outputs.map(output => ({
|
const outputTable = result.outputs.map((output) => ({
|
||||||
File: path.relative(process.cwd(), output.path),
|
File: path.relative(process.cwd(), output.path),
|
||||||
Type: output.kind,
|
Type: output.kind,
|
||||||
Size: formatFileSize(output.size),
|
Size: formatFileSize(output.size),
|
||||||
}));
|
}))
|
||||||
|
|
||||||
console.table(outputTable);
|
console.table(outputTable)
|
||||||
const buildTime = (end - start).toFixed(2);
|
const buildTime = (end - start).toFixed(2)
|
||||||
|
|
||||||
console.log(`\n✅ Build completed in ${buildTime}ms\n`);
|
console.log(`\n✅ Build completed in ${buildTime}ms\n`)
|
||||||
|
|||||||
12
web/bun-env.d.ts
vendored
12
web/bun-env.d.ts
vendored
@@ -1,17 +1,17 @@
|
|||||||
// Generated by `bun init`
|
// Generated by `bun init`
|
||||||
|
|
||||||
declare module "*.svg" {
|
declare module '*.svg' {
|
||||||
/**
|
/**
|
||||||
* A path to the SVG file
|
* A path to the SVG file
|
||||||
*/
|
*/
|
||||||
const path: `${string}.svg`;
|
const path: `${string}.svg`
|
||||||
export = path;
|
export = path
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "*.module.css" {
|
declare module '*.module.css' {
|
||||||
/**
|
/**
|
||||||
* A record of class names to their corresponding CSS module classes
|
* A record of class names to their corresponding CSS module classes
|
||||||
*/
|
*/
|
||||||
const classes: { readonly [key: string]: string };
|
const classes: { readonly [key: string]: string }
|
||||||
export = classes;
|
export = classes
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>HLAE中文站</title>
|
<title>HLAE中文站</title>
|
||||||
<script>
|
<script>
|
||||||
const theme = localStorage.getItem('theme') || 'light';
|
const theme = localStorage.getItem('theme') || 'light'
|
||||||
document.documentElement.classList.add(theme);
|
document.documentElement.classList.add(theme)
|
||||||
document.documentElement.setAttribute('data-theme', theme);
|
document.documentElement.setAttribute('data-theme', theme)
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -28,10 +30,11 @@
|
|||||||
"zustand": "^5.0.11"
|
"zustand": "^5.0.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"backend": "workspace:*",
|
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"vite": "^5.0.0",
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
"@vitejs/plugin-react": "^4.0.0"
|
"backend": "workspace:*",
|
||||||
|
"eslint": "^10.0.3",
|
||||||
|
"vite": "^5.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
"@tailwindcss/postcss": {},
|
'@tailwindcss/postcss': {},
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { edenTreaty } from '@elysiajs/eden';
|
import { edenTreaty } from '@elysiajs/eden'
|
||||||
import type { App } from 'backend';
|
import type { App } from 'backend'
|
||||||
|
|
||||||
// 创建 Eden 客户端,自动推断类型
|
// 创建 Eden 客户端,自动推断类型
|
||||||
export const api = edenTreaty<App>('http://localhost:3001');
|
export const api = edenTreaty<App>('http://localhost:3001')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { Button, Card } from "@heroui/react";
|
import { Button, Card } from '@heroui/react'
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
export default function AboutPage() {
|
export default function AboutPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-6 py-12 max-w-4xl">
|
<div className="container mx-auto max-w-4xl px-6 py-12">
|
||||||
<Card>
|
<Card>
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<Card.Title className="text-3xl font-bold">关于 HLAE 中文站</Card.Title>
|
<Card.Title className="text-3xl font-bold">
|
||||||
|
关于 HLAE 中文站
|
||||||
|
</Card.Title>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content className="prose dark:prose-invert">
|
<Card.Content className="prose dark:prose-invert">
|
||||||
<p className="text-default-500 mb-6 leading-relaxed">
|
<p className="text-default-500 mb-6 leading-relaxed">
|
||||||
@@ -17,11 +19,9 @@ export default function AboutPage() {
|
|||||||
</p>
|
</p>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
<Card.Footer>
|
<Card.Footer>
|
||||||
<Button onPress={() => navigate("/")}>
|
<Button onPress={() => navigate('/')}>返回首页</Button>
|
||||||
返回首页
|
|
||||||
</Button>
|
|
||||||
</Card.Footer>
|
</Card.Footer>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { Button, Card } from "@heroui/react";
|
import { Button, Card } from '@heroui/react'
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
export default function DemoPage() {
|
export default function DemoPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-6 py-12 max-w-4xl">
|
<div className="container mx-auto max-w-4xl px-6 py-12">
|
||||||
<Card>
|
<Card>
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<Card.Title className="text-3xl font-bold">击杀生成 (Kill Generation)</Card.Title>
|
<Card.Title className="text-3xl font-bold">
|
||||||
|
击杀生成 (Kill Generation)
|
||||||
|
</Card.Title>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<p className="text-default-500 mb-4">
|
<p className="text-default-500 mb-4">
|
||||||
@@ -16,11 +18,9 @@ export default function DemoPage() {
|
|||||||
</p>
|
</p>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
<Card.Footer>
|
<Card.Footer>
|
||||||
<Button onPress={() => navigate("/")}>
|
<Button onPress={() => navigate('/')}>返回首页</Button>
|
||||||
返回首页
|
|
||||||
</Button>
|
|
||||||
</Card.Footer>
|
</Card.Footer>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react'
|
||||||
import { useResources } from '../hooks/useApi';
|
import { useResources, Resource } from '../hooks/useApi'
|
||||||
import { CustomCard } from '../components/CustomCard';
|
import { CustomCard } from '../components/CustomCard'
|
||||||
import { Hero } from '../components/Hero';
|
import { Hero } from '../components/Hero'
|
||||||
import { Button, Link, Chip } from '@heroui/react';
|
import { Button, Chip } from '@heroui/react'
|
||||||
import { buttonVariants, linkVariants } from '@heroui/styles';
|
import { buttonVariants } from '@heroui/styles'
|
||||||
import { CreateResourceModal } from '../components/CreateResourceModal';
|
import { CreateResourceModal } from '../components/CreateResourceModal'
|
||||||
import { EditResourceModal } from '../components/EditResourceModal';
|
import { EditResourceModal } from '../components/EditResourceModal'
|
||||||
import { useSession } from '../lib/auth-client';
|
import { useSession } from '../lib/auth-client'
|
||||||
import {
|
import {
|
||||||
ChatBubbleLeftEllipsisIcon,
|
ChatBubbleLeftEllipsisIcon,
|
||||||
BookOpenIcon,
|
BookOpenIcon,
|
||||||
@@ -15,9 +15,9 @@ import {
|
|||||||
WrenchScrewdriverIcon,
|
WrenchScrewdriverIcon,
|
||||||
ListBulletIcon,
|
ListBulletIcon,
|
||||||
VideoCameraIcon,
|
VideoCameraIcon,
|
||||||
UserGroupIcon
|
UserGroupIcon,
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline'
|
||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom'
|
||||||
|
|
||||||
const PORTALS = [
|
const PORTALS = [
|
||||||
{
|
{
|
||||||
@@ -68,14 +68,16 @@ const PORTALS = [
|
|||||||
icon: VideoCameraIcon,
|
icon: VideoCameraIcon,
|
||||||
url: 'https://www.hltv.org',
|
url: 'https://www.hltv.org',
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const { resources, isLoading } = useResources();
|
const { resources, isLoading } = useResources()
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false)
|
||||||
const [selectedResource, setSelectedResource] = useState<any>(null);
|
const [selectedResource, setSelectedResource] = useState<Resource | null>(
|
||||||
const { data: session } = useSession();
|
null,
|
||||||
|
)
|
||||||
|
const { data: session } = useSession()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pb-20">
|
<div className="pb-20">
|
||||||
@@ -83,23 +85,25 @@ export default function HomePage() {
|
|||||||
<Hero />
|
<Hero />
|
||||||
{/* Portals Section */}
|
{/* Portals Section */}
|
||||||
<section className="mb-20">
|
<section className="mb-20">
|
||||||
<h2 className="text-3xl font-bold text-center mb-12">传送门</h2>
|
<h2 className="mb-12 text-center text-3xl font-bold">传送门</h2>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
{PORTALS.map((portal) => {
|
{PORTALS.map((portal) => {
|
||||||
const isExternal = portal.url.startsWith('http');
|
const isExternal = portal.url.startsWith('http')
|
||||||
const Content = (
|
const Content = (
|
||||||
<CustomCard className="group cursor-pointer p-6 h-full">
|
<CustomCard className="group h-full cursor-pointer p-6">
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div className="p-3 rounded-2xl bg-default-100 text-default-600 group-hover:bg-primary-50 group-hover:text-primary transition-colors">
|
<div className="bg-default-100 text-default-600 group-hover:bg-primary-50 group-hover:text-primary rounded-2xl p-3 transition-colors">
|
||||||
<portal.icon className="w-6 h-6" />
|
<portal.icon className="h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h3 className="text-lg font-bold">{portal.title}</h3>
|
<h3 className="text-lg font-bold">{portal.title}</h3>
|
||||||
<p className="text-sm text-default-500 line-clamp-1">{portal.description}</p>
|
<p className="text-default-500 line-clamp-1 text-sm">
|
||||||
|
{portal.description}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CustomCard>
|
</CustomCard>
|
||||||
);
|
)
|
||||||
|
|
||||||
return isExternal ? (
|
return isExternal ? (
|
||||||
<a
|
<a
|
||||||
@@ -107,7 +111,7 @@ export default function HomePage() {
|
|||||||
href={portal.url}
|
href={portal.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="no-underline text-inherit block"
|
className="block text-inherit no-underline"
|
||||||
>
|
>
|
||||||
{Content}
|
{Content}
|
||||||
</a>
|
</a>
|
||||||
@@ -115,18 +119,18 @@ export default function HomePage() {
|
|||||||
<RouterLink
|
<RouterLink
|
||||||
key={portal.title}
|
key={portal.title}
|
||||||
to={portal.url}
|
to={portal.url}
|
||||||
className="no-underline text-inherit block"
|
className="block text-inherit no-underline"
|
||||||
>
|
>
|
||||||
{Content}
|
{Content}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Resources Section */}
|
{/* Resources Section */}
|
||||||
<section>
|
<section>
|
||||||
<div className="flex justify-center items-center mb-12 relative">
|
<div className="relative mb-12 flex items-center justify-center">
|
||||||
<h2 className="text-3xl font-bold">资源下载</h2>
|
<h2 className="text-3xl font-bold">资源下载</h2>
|
||||||
{session && (
|
{session && (
|
||||||
<Button
|
<Button
|
||||||
@@ -139,7 +143,10 @@ export default function HomePage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CreateResourceModal isOpen={isModalOpen} onOpenChange={setIsModalOpen} />
|
<CreateResourceModal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onOpenChange={setIsModalOpen}
|
||||||
|
/>
|
||||||
<EditResourceModal
|
<EditResourceModal
|
||||||
isOpen={isEditModalOpen}
|
isOpen={isEditModalOpen}
|
||||||
onOpenChange={setIsEditModalOpen}
|
onOpenChange={setIsEditModalOpen}
|
||||||
@@ -149,42 +156,54 @@ export default function HomePage() {
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex justify-center py-20">加载中...</div>
|
<div className="flex justify-center py-20">加载中...</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{resources?.map((res) => (
|
{resources?.map((res) => (
|
||||||
<CustomCard key={res.id} className="p-6">
|
<CustomCard key={res.id} className="p-6">
|
||||||
<div className="flex gap-4 mb-4">
|
<div className="mb-4 flex gap-4">
|
||||||
<div className="w-16 h-16 rounded-2xl bg-default-100 overflow-hidden flex-shrink-0 flex items-center justify-center">
|
<div className="bg-default-100 flex h-16 w-16 flex-shrink-0 items-center justify-center overflow-hidden rounded-2xl">
|
||||||
{res.icon ? (
|
{res.icon ? (
|
||||||
<img
|
<img
|
||||||
alt={res.title}
|
alt={res.title}
|
||||||
src={res.icon}
|
src={res.icon}
|
||||||
className="w-10 h-10 object-contain"
|
className="h-10 w-10 object-contain"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-2xl font-bold text-default-400">
|
<div className="text-default-400 text-2xl font-bold">
|
||||||
{res.title[0]}
|
{res.title[0]}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1 flex-1 min-w-0">
|
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="text-lg font-bold truncate">{res.title}</h3>
|
<h3 className="truncate text-lg font-bold">
|
||||||
|
{res.title}
|
||||||
|
</h3>
|
||||||
{res.category && (
|
{res.category && (
|
||||||
<Chip size="sm" variant="soft" className="text-[10px] h-5">
|
<Chip
|
||||||
|
size="sm"
|
||||||
|
variant="soft"
|
||||||
|
className="h-5 text-[10px]"
|
||||||
|
>
|
||||||
{res.category}
|
{res.category}
|
||||||
</Chip>
|
</Chip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-default-500 line-clamp-2">{res.description}</p>
|
<p className="text-default-500 line-clamp-2 text-sm">
|
||||||
|
{res.description}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-3 mt-auto">
|
<div className="mt-auto flex gap-3">
|
||||||
<a
|
<a
|
||||||
href={res.url}
|
href={res.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className={buttonVariants({ fullWidth: true, className: "bg-[#C14B4B] text-white font-medium hover:opacity-90 flex items-center justify-center h-10 px-4 rounded-3xl" })}
|
className={buttonVariants({
|
||||||
|
fullWidth: true,
|
||||||
|
className:
|
||||||
|
'flex h-10 items-center justify-center rounded-3xl bg-[#C14B4B] px-4 font-medium text-white hover:opacity-90',
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
加速下载
|
加速下载
|
||||||
</a>
|
</a>
|
||||||
@@ -192,7 +211,12 @@ export default function HomePage() {
|
|||||||
href={res.url}
|
href={res.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className={buttonVariants({ fullWidth: true, variant: "tertiary", className: "bg-default-200 text-default-600 font-medium hover:bg-default-300 flex items-center justify-center h-10 px-4 rounded-3xl" })}
|
className={buttonVariants({
|
||||||
|
fullWidth: true,
|
||||||
|
variant: 'tertiary',
|
||||||
|
className:
|
||||||
|
'bg-default-200 text-default-600 hover:bg-default-300 flex h-10 items-center justify-center rounded-3xl px-4 font-medium',
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
原始下载
|
原始下载
|
||||||
</a>
|
</a>
|
||||||
@@ -201,26 +225,26 @@ export default function HomePage() {
|
|||||||
isIconOnly
|
isIconOnly
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setSelectedResource(res);
|
setSelectedResource(res)
|
||||||
setIsEditModalOpen(true);
|
setIsEditModalOpen(true)
|
||||||
}}
|
}}
|
||||||
className="flex-shrink-0"
|
className="flex-shrink-0"
|
||||||
>
|
>
|
||||||
<WrenchScrewdriverIcon className="w-4 h-4" />
|
<WrenchScrewdriverIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CustomCard>
|
</CustomCard>
|
||||||
))}
|
))}
|
||||||
{!resources?.length && (
|
{!resources?.length && (
|
||||||
<div className="col-span-full text-center text-default-400 py-20">
|
<div className="text-default-400 col-span-full py-20 text-center">
|
||||||
暂无资源
|
暂无资源
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,56 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react'
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link as RouterLink } from 'react-router-dom'
|
||||||
import { Card, Input, Button, Form, toast, TextField, Label, FieldError } from '@heroui/react';
|
import {
|
||||||
import { signIn } from '../../lib/auth-client';
|
Card,
|
||||||
|
Input,
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
toast,
|
||||||
|
TextField,
|
||||||
|
Label,
|
||||||
|
FieldError,
|
||||||
|
} from '@heroui/react'
|
||||||
|
import { buttonVariants } from '@heroui/styles'
|
||||||
|
import { signIn } from '../../lib/auth-client'
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('')
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('')
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false)
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const handleLogin = async (e: React.FormEvent<HTMLFormElement>) => {
|
const handleLogin = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
setLoading(true);
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
await signIn.email({
|
await signIn.email(
|
||||||
email,
|
{
|
||||||
password,
|
email,
|
||||||
}, {
|
password,
|
||||||
onSuccess: () => {
|
|
||||||
navigate('/');
|
|
||||||
},
|
},
|
||||||
onError: (ctx) => {
|
{
|
||||||
toast.danger(ctx.error.message);
|
onSuccess: () => {
|
||||||
}
|
navigate('/')
|
||||||
});
|
},
|
||||||
|
onError: (ctx) => {
|
||||||
|
toast.danger(ctx.error.message)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-[calc(100vh-64px)] p-4">
|
<div className="flex min-h-[calc(100vh-64px)] flex-col items-center justify-center p-4">
|
||||||
<Card className="w-full max-w-md">
|
<Card className="w-full max-w-md">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<Card.Title className="text-2xl font-bold text-center w-full">登录 HLAE 中文站</Card.Title>
|
<Card.Title className="w-full text-center text-2xl font-bold">
|
||||||
|
登录 HLAE 中文站
|
||||||
|
</Card.Title>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Form validationBehavior="native" onSubmit={handleLogin}>
|
<Form validationBehavior="native" onSubmit={handleLogin}>
|
||||||
<Card.Content className="flex flex-col gap-6">
|
<Card.Content className="flex flex-col gap-6">
|
||||||
@@ -63,21 +78,42 @@ export default function LoginPage() {
|
|||||||
<FieldError />
|
<FieldError />
|
||||||
</TextField>
|
</TextField>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
<Card.Footer className="flex flex-col gap-3 mt-2">
|
<Card.Footer className="mt-2 flex flex-col gap-3">
|
||||||
<Button type="submit" isPending={loading} fullWidth className="font-bold">
|
<Button
|
||||||
|
type="submit"
|
||||||
|
isPending={loading}
|
||||||
|
fullWidth
|
||||||
|
className="font-bold"
|
||||||
|
>
|
||||||
登录
|
登录
|
||||||
</Button>
|
</Button>
|
||||||
<Button as={Link} to="/register" variant="tertiary" fullWidth className="font-bold">
|
<RouterLink
|
||||||
|
to="/register"
|
||||||
|
className={buttonVariants({
|
||||||
|
variant: 'tertiary',
|
||||||
|
fullWidth: true,
|
||||||
|
className: 'font-bold',
|
||||||
|
})}
|
||||||
|
>
|
||||||
注册账号
|
注册账号
|
||||||
</Button>
|
</RouterLink>
|
||||||
<div className="flex justify-center mt-2">
|
<div className="mt-2 flex justify-center">
|
||||||
<Link to="/" className="text-sm text-default-500 hover:text-primary transition-colors flex items-center gap-1">
|
<RouterLink
|
||||||
<span>← 返回首页</span>
|
to="/"
|
||||||
</Link>
|
className="text-default-500 hover:text-foreground text-sm"
|
||||||
|
>
|
||||||
|
返回首页
|
||||||
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
</Card.Footer>
|
</Card.Footer>
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
|
<p className="mt-4 text-sm text-gray-500">
|
||||||
|
没有账号?{' '}
|
||||||
|
<RouterLink to="/register" className="text-primary hover:underline">
|
||||||
|
去注册
|
||||||
|
</RouterLink>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,63 @@
|
|||||||
import { useParams, Link as RouterLink } from 'react-router-dom';
|
import { useParams, Link as RouterLink } from 'react-router-dom'
|
||||||
import { usePost } from '../../../hooks/useApi';
|
import { usePost, Comment } from '../../../hooks/useApi'
|
||||||
import { Card, Button, Avatar, Separator, Skeleton } from '@heroui/react';
|
import { Card, Avatar, Separator, Skeleton } from '@heroui/react'
|
||||||
import { buttonVariants } from '@heroui/styles';
|
import { buttonVariants } from '@heroui/styles'
|
||||||
|
|
||||||
export default function PostDetailPage() {
|
export default function PostDetailPage() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>()
|
||||||
const { post, isLoading, isError } = usePost(id || '');
|
const { post, isLoading, isError } = usePost(id || '')
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="pb-20">
|
<div className="pb-20">
|
||||||
<main className="container mx-auto px-4 py-8">
|
<main className="container mx-auto px-4 py-8">
|
||||||
<Card className="p-6 max-w-4xl mx-auto space-y-4">
|
<Card className="mx-auto max-w-4xl space-y-4 p-6">
|
||||||
<Skeleton className="h-8 w-3/4 rounded-lg" />
|
<Skeleton className="h-8 w-3/4 rounded-lg" />
|
||||||
<Skeleton className="h-4 w-1/4 rounded-lg" />
|
<Skeleton className="h-4 w-1/4 rounded-lg" />
|
||||||
<Skeleton className="h-40 w-full rounded-lg" />
|
<Skeleton className="h-40 w-full rounded-lg" />
|
||||||
</Card>
|
</Card>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError || !post) {
|
if (isError || !post) {
|
||||||
return (
|
return (
|
||||||
<div className="pb-20">
|
<div className="pb-20">
|
||||||
<main className="container mx-auto px-4 py-8 text-center">
|
<main className="container mx-auto px-4 py-8 text-center">
|
||||||
<h1 className="text-2xl font-bold mb-4">帖子未找到</h1>
|
<h1 className="mb-4 text-2xl font-bold">帖子未找到</h1>
|
||||||
<RouterLink to="/posts" className={buttonVariants({ variant: "secondary" })}>
|
<RouterLink
|
||||||
|
to="/posts"
|
||||||
|
className={buttonVariants({ variant: 'secondary' })}
|
||||||
|
>
|
||||||
返回帖子列表
|
返回帖子列表
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pb-20">
|
<div className="pb-20">
|
||||||
<main className="container mx-auto px-4 py-8">
|
<main className="container mx-auto px-4 py-8">
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="mx-auto max-w-4xl">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<RouterLink to="/posts" className="text-default-500 hover:text-foreground">
|
<RouterLink
|
||||||
|
to="/posts"
|
||||||
|
className="text-default-500 hover:text-foreground"
|
||||||
|
>
|
||||||
← 返回帖子列表
|
← 返回帖子列表
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="p-6 mb-8">
|
<Card className="mb-8 p-6">
|
||||||
<Card.Header className="flex flex-col items-start gap-2">
|
<Card.Header className="flex flex-col items-start gap-2">
|
||||||
<h1 className="text-3xl font-bold">{post.title}</h1>
|
<h1 className="text-3xl font-bold">{post.title}</h1>
|
||||||
<div className="flex items-center gap-2 text-small text-default-500">
|
<div className="text-small text-default-500 flex items-center gap-2">
|
||||||
<Avatar className="w-6 h-6">
|
<Avatar className="h-6 w-6">
|
||||||
<Avatar.Fallback>
|
<Avatar.Fallback>
|
||||||
{(post.user?.name || 'U').slice(0, 2).toUpperCase()}
|
{(post.user?.name || 'U').slice(0, 2).toUpperCase()}
|
||||||
</Avatar.Fallback>
|
</Avatar.Fallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<span>{post.user?.name || '匿名用户'}</span>
|
<span>{post.user?.name || '匿名用户'}</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
@@ -60,7 +66,7 @@ export default function PostDetailPage() {
|
|||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Separator className="my-4" />
|
<Separator className="my-4" />
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<div className="whitespace-pre-wrap text-lg leading-relaxed">
|
<div className="text-lg leading-relaxed whitespace-pre-wrap">
|
||||||
{post.content}
|
{post.content}
|
||||||
</div>
|
</div>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
@@ -68,13 +74,19 @@ export default function PostDetailPage() {
|
|||||||
|
|
||||||
{post.comments && post.comments.length > 0 && (
|
{post.comments && post.comments.length > 0 && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-xl font-bold">评论 ({post.comments.length})</h3>
|
<h3 className="text-xl font-bold">
|
||||||
{post.comments.map((comment) => (
|
评论 ({post.comments.length})
|
||||||
|
</h3>
|
||||||
|
{post.comments.map((comment: Comment) => (
|
||||||
<Card key={comment.id} className="p-4">
|
<Card key={comment.id} className="p-4">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex items-center gap-2 text-small text-default-500">
|
<div className="text-small text-default-500 flex items-center gap-2">
|
||||||
<span className="font-semibold text-foreground">{comment.user?.name || '匿名用户'}</span>
|
<span className="text-foreground font-semibold">
|
||||||
<span>{new Date(comment.createdAt).toLocaleString()}</span>
|
{comment.user?.name || '匿名用户'}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{new Date(comment.createdAt).toLocaleString()}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p>{comment.content}</p>
|
<p>{comment.content}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,5 +97,5 @@ export default function PostDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,76 +1,98 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react'
|
||||||
import { usePosts } from '../../hooks/useApi';
|
import { usePosts, Post } from '../../hooks/useApi'
|
||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom'
|
||||||
import { Card, Button, Input, TextArea, Form, Link, toast, TextField, Label, FieldError } from '@heroui/react';
|
import {
|
||||||
import { useSession } from '../../lib/auth-client';
|
Card,
|
||||||
import { api } from '../../api/client';
|
Button,
|
||||||
|
Input,
|
||||||
|
TextArea,
|
||||||
|
Form,
|
||||||
|
toast,
|
||||||
|
TextField,
|
||||||
|
Label,
|
||||||
|
FieldError,
|
||||||
|
} from '@heroui/react'
|
||||||
|
import { useSession } from '../../lib/auth-client'
|
||||||
|
import { api } from '../../api/client'
|
||||||
|
|
||||||
export default function PostsPage() {
|
export default function PostsPage() {
|
||||||
const { posts, isLoading, mutate } = usePosts();
|
const { posts, isLoading, mutate } = usePosts()
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession()
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('')
|
||||||
const [content, setContent] = useState('');
|
const [content, setContent] = useState('')
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
if (!session) return;
|
if (!session) return
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true)
|
||||||
try {
|
try {
|
||||||
await api.posts.post({
|
await api.posts.post({
|
||||||
title,
|
title,
|
||||||
content,
|
content,
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
published: true
|
published: true,
|
||||||
});
|
})
|
||||||
setTitle('');
|
setTitle('')
|
||||||
setContent('');
|
setContent('')
|
||||||
mutate(); // Refresh list
|
mutate() // Refresh list
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err)
|
||||||
toast.danger('发布失败');
|
toast.danger('发布失败')
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pb-20">
|
<div className="pb-20">
|
||||||
<main className="container mx-auto px-4 py-8">
|
<main className="container mx-auto px-4 py-8">
|
||||||
<div className="flex flex-col md:flex-row gap-8">
|
<div className="flex flex-col gap-8 md:flex-row">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h2 className="text-2xl font-bold mb-6">最新帖子</h2>
|
<h2 className="mb-6 text-2xl font-bold">最新帖子</h2>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div>加载中...</div>
|
<div>加载中...</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{posts?.map((post: any) => (
|
{posts?.map((post: Post) => (
|
||||||
<Card key={post.id} className="p-4">
|
<Card key={post.id} className="p-4">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<RouterLink to={`/posts/${post.id}`} className="text-md font-bold text-foreground hover:text-primary">
|
<RouterLink
|
||||||
{post.title}
|
to={`/posts/${post.id}`}
|
||||||
</RouterLink>
|
className="text-md text-foreground hover:text-primary font-bold"
|
||||||
<p className="text-small text-default-500">
|
>
|
||||||
{post.user?.name || '匿名用户'} - {new Date(post.createdAt).toLocaleDateString()}
|
{post.title}
|
||||||
</p>
|
</RouterLink>
|
||||||
</div>
|
<p className="text-small text-default-500">
|
||||||
|
{post.user?.name || '匿名用户'} -{' '}
|
||||||
|
{new Date(post.createdAt).toLocaleDateString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<p className="whitespace-pre-wrap line-clamp-3">{post.content}</p>
|
<p className="line-clamp-3 whitespace-pre-wrap">
|
||||||
|
{post.content}
|
||||||
|
</p>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
{!posts?.length && <div className="text-center text-gray-500">暂无帖子</div>}
|
{!posts?.length && (
|
||||||
|
<div className="text-center text-gray-500">暂无帖子</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{session && (
|
{session && (
|
||||||
<div className="w-full md:w-80">
|
<div className="w-full md:w-80">
|
||||||
<Card className="p-4 sticky top-4">
|
<Card className="sticky top-4 p-4">
|
||||||
<h3 className="text-xl font-bold mb-4">发布新帖</h3>
|
<h3 className="mb-4 text-xl font-bold">发布新帖</h3>
|
||||||
<Form validationBehavior="native" onSubmit={handleSubmit} className="flex flex-col gap-4">
|
<Form
|
||||||
|
validationBehavior="native"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="flex flex-col gap-4"
|
||||||
|
>
|
||||||
<TextField
|
<TextField
|
||||||
isRequired
|
isRequired
|
||||||
name="title"
|
name="title"
|
||||||
@@ -103,5 +125,5 @@ export default function PostsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,57 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react'
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link as RouterLink } from 'react-router-dom'
|
||||||
import { Card, Input, Button, Form, toast, TextField, Label, FieldError } from '@heroui/react';
|
import {
|
||||||
import { signUp } from '../../lib/auth-client';
|
Card,
|
||||||
|
Input,
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
toast,
|
||||||
|
TextField,
|
||||||
|
Label,
|
||||||
|
FieldError,
|
||||||
|
} from '@heroui/react'
|
||||||
|
import { buttonVariants } from '@heroui/styles'
|
||||||
|
import { signUp } from '../../lib/auth-client'
|
||||||
|
|
||||||
export default function RegisterPage() {
|
export default function RegisterPage() {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('')
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('')
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false)
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const handleRegister = async (e: React.FormEvent<HTMLFormElement>) => {
|
const handleRegister = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
setLoading(true);
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
await signUp.email({
|
await signUp.email(
|
||||||
email,
|
{
|
||||||
password,
|
email,
|
||||||
name: email.split('@')[0], // Default name
|
password,
|
||||||
}, {
|
name: email.split('@')[0], // Default name
|
||||||
onSuccess: () => {
|
|
||||||
navigate('/');
|
|
||||||
},
|
},
|
||||||
onError: (ctx) => {
|
{
|
||||||
toast.danger(ctx.error.message);
|
onSuccess: () => {
|
||||||
}
|
navigate('/')
|
||||||
});
|
},
|
||||||
|
onError: (ctx) => {
|
||||||
|
toast.danger(ctx.error.message)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-[calc(100vh-64px)] p-4">
|
<div className="flex min-h-[calc(100vh-64px)] flex-col items-center justify-center p-4">
|
||||||
<Card className="w-full max-w-md">
|
<Card className="w-full max-w-md">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<Card.Title className="text-2xl font-bold text-center w-full">注册 HLAE 中文站</Card.Title>
|
<Card.Title className="w-full text-center text-2xl font-bold">
|
||||||
|
注册 HLAE 中文站
|
||||||
|
</Card.Title>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Form validationBehavior="native" onSubmit={handleRegister}>
|
<Form validationBehavior="native" onSubmit={handleRegister}>
|
||||||
<Card.Content className="flex flex-col gap-6">
|
<Card.Content className="flex flex-col gap-6">
|
||||||
@@ -64,21 +79,42 @@ export default function RegisterPage() {
|
|||||||
<FieldError />
|
<FieldError />
|
||||||
</TextField>
|
</TextField>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
<Card.Footer className="flex flex-col gap-3 mt-2">
|
<Card.Footer className="mt-2 flex flex-col gap-3">
|
||||||
<Button type="submit" isPending={loading} fullWidth className="font-bold">
|
<Button
|
||||||
|
type="submit"
|
||||||
|
isPending={loading}
|
||||||
|
fullWidth
|
||||||
|
className="font-bold"
|
||||||
|
>
|
||||||
注册
|
注册
|
||||||
</Button>
|
</Button>
|
||||||
<Button as={Link} to="/login" variant="tertiary" fullWidth className="font-bold">
|
<RouterLink
|
||||||
|
to="/login"
|
||||||
|
className={buttonVariants({
|
||||||
|
variant: 'tertiary',
|
||||||
|
fullWidth: true,
|
||||||
|
className: 'font-bold',
|
||||||
|
})}
|
||||||
|
>
|
||||||
已有账号?登录
|
已有账号?登录
|
||||||
</Button>
|
</RouterLink>
|
||||||
<div className="flex justify-center mt-2">
|
<div className="mt-2 flex justify-center">
|
||||||
<Link to="/" className="text-sm text-default-500 hover:text-primary transition-colors flex items-center gap-1">
|
<RouterLink
|
||||||
<span>← 返回首页</span>
|
to="/"
|
||||||
</Link>
|
className="text-default-500 hover:text-foreground text-sm"
|
||||||
|
>
|
||||||
|
返回首页
|
||||||
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
</Card.Footer>
|
</Card.Footer>
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
|
<p className="mt-4 text-sm text-gray-500">
|
||||||
|
已有账号?{' '}
|
||||||
|
<RouterLink to="/login" className="text-primary hover:underline">
|
||||||
|
去登录
|
||||||
|
</RouterLink>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,57 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react'
|
||||||
import { Modal, Button, Input, Form, toast, TextField, Label, FieldError } from "@heroui/react";
|
import {
|
||||||
import { api } from '../api/client';
|
Modal,
|
||||||
import { useSWRConfig } from 'swr';
|
Button,
|
||||||
|
Input,
|
||||||
|
Form,
|
||||||
|
toast,
|
||||||
|
TextField,
|
||||||
|
Label,
|
||||||
|
FieldError,
|
||||||
|
} from '@heroui/react'
|
||||||
|
import { api } from '../api/client'
|
||||||
|
import { useSWRConfig } from 'swr'
|
||||||
|
|
||||||
export function CreateResourceModal({ isOpen, onOpenChange }: { isOpen: boolean, onOpenChange: (isOpen: boolean) => void }) {
|
export function CreateResourceModal({
|
||||||
const [title, setTitle] = useState('');
|
isOpen,
|
||||||
const [description, setDescription] = useState('');
|
onOpenChange,
|
||||||
const [url, setUrl] = useState('');
|
}: {
|
||||||
const [icon, setIcon] = useState('');
|
isOpen: boolean
|
||||||
const [category, setCategory] = useState('');
|
onOpenChange: (isOpen: boolean) => void
|
||||||
const [loading, setLoading] = useState(false);
|
}) {
|
||||||
const { mutate } = useSWRConfig();
|
const [title, setTitle] = useState('')
|
||||||
|
const [description, setDescription] = useState('')
|
||||||
|
const [url, setUrl] = useState('')
|
||||||
|
const [icon, setIcon] = useState('')
|
||||||
|
const [category, setCategory] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const { mutate } = useSWRConfig()
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
setLoading(true);
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
await api.resources.post({
|
await api.resources.post({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
url,
|
url,
|
||||||
icon,
|
icon,
|
||||||
category
|
category,
|
||||||
});
|
})
|
||||||
mutate('/resources');
|
mutate('/resources')
|
||||||
onOpenChange(false);
|
onOpenChange(false)
|
||||||
setTitle('');
|
setTitle('')
|
||||||
setDescription('');
|
setDescription('')
|
||||||
setUrl('');
|
setUrl('')
|
||||||
setIcon('');
|
setIcon('')
|
||||||
setCategory('');
|
setCategory('')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err)
|
||||||
toast.danger('创建失败');
|
toast.danger('创建失败')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
@@ -48,7 +63,11 @@ export function CreateResourceModal({ isOpen, onOpenChange }: { isOpen: boolean,
|
|||||||
<Modal.CloseTrigger />
|
<Modal.CloseTrigger />
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<Form validationBehavior="native" onSubmit={handleSubmit} className="flex flex-col gap-4">
|
<Form
|
||||||
|
validationBehavior="native"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="flex flex-col gap-4"
|
||||||
|
>
|
||||||
<TextField
|
<TextField
|
||||||
isRequired
|
isRequired
|
||||||
name="title"
|
name="title"
|
||||||
@@ -70,22 +89,13 @@ export function CreateResourceModal({ isOpen, onOpenChange }: { isOpen: boolean,
|
|||||||
<FieldError />
|
<FieldError />
|
||||||
</TextField>
|
</TextField>
|
||||||
|
|
||||||
<TextField
|
<TextField isRequired name="url" value={url} onChange={setUrl}>
|
||||||
isRequired
|
|
||||||
name="url"
|
|
||||||
value={url}
|
|
||||||
onChange={setUrl}
|
|
||||||
>
|
|
||||||
<Label>链接</Label>
|
<Label>链接</Label>
|
||||||
<Input placeholder="https://..." variant="secondary" />
|
<Input placeholder="https://..." variant="secondary" />
|
||||||
<FieldError />
|
<FieldError />
|
||||||
</TextField>
|
</TextField>
|
||||||
|
|
||||||
<TextField
|
<TextField name="icon" value={icon} onChange={setIcon}>
|
||||||
name="icon"
|
|
||||||
value={icon}
|
|
||||||
onChange={setIcon}
|
|
||||||
>
|
|
||||||
<Label>图标 URL</Label>
|
<Label>图标 URL</Label>
|
||||||
<Input placeholder="如: /icon/hlae.png" variant="secondary" />
|
<Input placeholder="如: /icon/hlae.png" variant="secondary" />
|
||||||
<FieldError />
|
<FieldError />
|
||||||
@@ -115,5 +125,5 @@ export function CreateResourceModal({ isOpen, onOpenChange }: { isOpen: boolean,
|
|||||||
</Modal.Container>
|
</Modal.Container>
|
||||||
</Modal.Backdrop>
|
</Modal.Backdrop>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,37 @@
|
|||||||
import { Card } from "@heroui/react";
|
import { Card } from '@heroui/react'
|
||||||
import { tv, type VariantProps } from "tailwind-variants";
|
import { tv, type VariantProps } from 'tailwind-variants'
|
||||||
import React from "react";
|
import React from 'react'
|
||||||
|
|
||||||
// Define custom styles
|
// Define custom styles
|
||||||
export const customStyles = tv({
|
const customStyles = tv({
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
glass: "bg-white/70 backdrop-blur-md border-white/20 shadow-xl dark:bg-black/70 dark:border-white/10",
|
glass:
|
||||||
neon: "border border-blue-500 shadow-[0_0_15px_rgba(59,130,246,0.5)] bg-gray-900/90 text-white",
|
'bg-white/70 backdrop-blur-md border-white/20 shadow-xl dark:bg-black/70 dark:border-white/10',
|
||||||
minimal: "border-none shadow-none bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors",
|
neon: 'border border-blue-500 shadow-[0_0_15px_rgba(59,130,246,0.5)] bg-gray-900/90 text-white',
|
||||||
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",
|
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: {
|
defaultVariants: {
|
||||||
variant: "modern",
|
variant: 'modern',
|
||||||
}
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
type CustomCardProps = React.ComponentProps<typeof Card> & VariantProps<typeof customStyles>;
|
type CustomCardProps = React.ComponentProps<typeof Card> &
|
||||||
|
VariantProps<typeof customStyles>
|
||||||
|
|
||||||
// Create the custom component
|
// Create the custom component
|
||||||
export function CustomCard({ className, variant, ...props }: CustomCardProps) {
|
export function CustomCard({ className, variant, ...props }: CustomCardProps) {
|
||||||
// Use custom styles and merge with className
|
// Use custom styles and merge with className
|
||||||
return <Card className={customStyles({ variant, className })} {...props} />;
|
return <Card className={customStyles({ variant, className })} {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach subcomponents to the custom component for easier usage
|
// Attach subcomponents to the custom component for easier usage
|
||||||
CustomCard.Header = Card.Header;
|
CustomCard.Header = Card.Header
|
||||||
CustomCard.Content = Card.Content;
|
CustomCard.Content = Card.Content
|
||||||
CustomCard.Footer = Card.Footer;
|
CustomCard.Footer = Card.Footer
|
||||||
CustomCard.Title = Card.Title;
|
CustomCard.Title = Card.Title
|
||||||
CustomCard.Description = Card.Description;
|
CustomCard.Description = Card.Description
|
||||||
|
|||||||
@@ -1,72 +1,83 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react'
|
||||||
import { Modal, Button, Input, Form, toast, TextField, Label, FieldError } from "@heroui/react";
|
import {
|
||||||
import { api } from '../api/client';
|
Modal,
|
||||||
import { useSWRConfig } from 'swr';
|
Button,
|
||||||
import { Resource } from '../hooks/useApi';
|
Input,
|
||||||
|
Form,
|
||||||
|
toast,
|
||||||
|
TextField,
|
||||||
|
Label,
|
||||||
|
FieldError,
|
||||||
|
} from '@heroui/react'
|
||||||
|
import { api } from '../api/client'
|
||||||
|
import { useSWRConfig } from 'swr'
|
||||||
|
import { Resource } from '../hooks/useApi'
|
||||||
|
|
||||||
export function EditResourceModal({
|
export function EditResourceModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
resource
|
resource,
|
||||||
}: {
|
}: {
|
||||||
isOpen: boolean,
|
isOpen: boolean
|
||||||
onOpenChange: (isOpen: boolean) => void,
|
onOpenChange: (isOpen: boolean) => void
|
||||||
resource: Resource | null
|
resource: Resource | null
|
||||||
}) {
|
}) {
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('')
|
||||||
const [description, setDescription] = useState('');
|
const [description, setDescription] = useState('')
|
||||||
const [url, setUrl] = useState('');
|
const [url, setUrl] = useState('')
|
||||||
const [icon, setIcon] = useState('');
|
const [icon, setIcon] = useState('')
|
||||||
const [category, setCategory] = useState('');
|
const [category, setCategory] = useState('')
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false)
|
||||||
const { mutate } = useSWRConfig();
|
const { mutate } = useSWRConfig()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (resource) {
|
if (resource) {
|
||||||
setTitle(resource.title);
|
setTitle(resource.title)
|
||||||
setDescription(resource.description || '');
|
setDescription(resource.description || '')
|
||||||
setUrl(resource.url);
|
setUrl(resource.url)
|
||||||
setIcon(resource.icon || '');
|
setIcon(resource.icon || '')
|
||||||
setCategory(resource.category || '');
|
setCategory(resource.category || '')
|
||||||
}
|
}
|
||||||
}, [resource]);
|
}, [resource])
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
if (!resource) return;
|
if (!resource) return
|
||||||
setLoading(true);
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
await api.resources({ id: resource.id }).put({
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
await (api.resources as any)[resource.id].put({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
url,
|
url,
|
||||||
icon,
|
icon,
|
||||||
category
|
category,
|
||||||
});
|
})
|
||||||
mutate('/resources');
|
mutate('/resources')
|
||||||
onOpenChange(false);
|
onOpenChange(false)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err)
|
||||||
toast.danger('更新失败');
|
toast.danger('更新失败')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!resource || !confirm('确定要删除这个资源吗?')) return;
|
if (!resource || !confirm('确定要删除这个资源吗?')) return
|
||||||
setLoading(true);
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
await api.resources({ id: resource.id }).delete();
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
mutate('/resources');
|
await (api.resources as any)[resource.id].delete()
|
||||||
onOpenChange(false);
|
mutate('/resources')
|
||||||
|
onOpenChange(false)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err)
|
||||||
toast.danger('删除失败');
|
toast.danger('删除失败')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
@@ -78,7 +89,11 @@ export function EditResourceModal({
|
|||||||
<Modal.CloseTrigger />
|
<Modal.CloseTrigger />
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<Form validationBehavior="native" onSubmit={handleSubmit} className="flex flex-col gap-4">
|
<Form
|
||||||
|
validationBehavior="native"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="flex flex-col gap-4"
|
||||||
|
>
|
||||||
<TextField
|
<TextField
|
||||||
isRequired
|
isRequired
|
||||||
name="title"
|
name="title"
|
||||||
@@ -100,22 +115,13 @@ export function EditResourceModal({
|
|||||||
<FieldError />
|
<FieldError />
|
||||||
</TextField>
|
</TextField>
|
||||||
|
|
||||||
<TextField
|
<TextField isRequired name="url" value={url} onChange={setUrl}>
|
||||||
isRequired
|
|
||||||
name="url"
|
|
||||||
value={url}
|
|
||||||
onChange={setUrl}
|
|
||||||
>
|
|
||||||
<Label>链接</Label>
|
<Label>链接</Label>
|
||||||
<Input placeholder="https://..." variant="secondary" />
|
<Input placeholder="https://..." variant="secondary" />
|
||||||
<FieldError />
|
<FieldError />
|
||||||
</TextField>
|
</TextField>
|
||||||
|
|
||||||
<TextField
|
<TextField name="icon" value={icon} onChange={setIcon}>
|
||||||
name="icon"
|
|
||||||
value={icon}
|
|
||||||
onChange={setIcon}
|
|
||||||
>
|
|
||||||
<Label>图标 URL</Label>
|
<Label>图标 URL</Label>
|
||||||
<Input placeholder="如: /icon/hlae.png" variant="secondary" />
|
<Input placeholder="如: /icon/hlae.png" variant="secondary" />
|
||||||
<FieldError />
|
<FieldError />
|
||||||
@@ -131,8 +137,12 @@ export function EditResourceModal({
|
|||||||
<FieldError />
|
<FieldError />
|
||||||
</TextField>
|
</TextField>
|
||||||
|
|
||||||
<div className="flex justify-between gap-2 mt-4">
|
<div className="mt-4 flex justify-between gap-2">
|
||||||
<Button variant="danger" onPress={handleDelete} isPending={loading}>
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
onPress={handleDelete}
|
||||||
|
isPending={loading}
|
||||||
|
>
|
||||||
删除资源
|
删除资源
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -150,5 +160,5 @@ export function EditResourceModal({
|
|||||||
</Modal.Container>
|
</Modal.Container>
|
||||||
</Modal.Backdrop>
|
</Modal.Backdrop>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,54 @@
|
|||||||
import { Button } from '@heroui/react';
|
import { WrenchScrewdriverIcon } from '@heroicons/react/24/outline'
|
||||||
import { WrenchScrewdriverIcon } from '@heroicons/react/24/outline';
|
import { buttonVariants } from '@heroui/styles'
|
||||||
|
|
||||||
export const Hero = () => {
|
export const Hero = () => {
|
||||||
return (
|
return (
|
||||||
<section className="flex flex-col items-center justify-center py-20 text-center">
|
<section className="flex flex-col items-center justify-center py-20 text-center">
|
||||||
<h1 className="text-6xl font-extrabold tracking-tight mb-8">
|
<h1 className="mb-8 text-6xl font-extrabold tracking-tight">
|
||||||
HLAE中文站
|
HLAE中文站
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-2xl text-default-600 mb-12 max-w-3xl leading-relaxed">
|
<p className="text-default-600 mb-12 max-w-3xl text-2xl leading-relaxed">
|
||||||
<span className="text-[#FF6B00] font-bold">CS</span>等起源引擎游戏的影片制作工具
|
<span className="font-bold text-[#FF6B00]">CS</span>
|
||||||
<span className="text-brand font-bold mx-1">HLAE</span>的中文门户网站
|
等起源引擎游戏的影片制作工具
|
||||||
|
<span className="text-brand mx-1 font-bold">HLAE</span>的中文门户网站
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-6">
|
<div className="flex gap-6">
|
||||||
<Button
|
<a
|
||||||
as="a"
|
|
||||||
href="https://www.advancedfx.org/"
|
href="https://www.advancedfx.org/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="bg-brand text-white font-medium px-10 h-14 rounded-full text-lg shadow-lg shadow-brand/20 hover:opacity-90 transition-opacity"
|
className={buttonVariants({
|
||||||
startContent={<WrenchScrewdriverIcon className="w-6 h-6" />}
|
className:
|
||||||
|
'bg-brand shadow-brand/20 flex h-14 items-center gap-2 rounded-full px-10 text-lg font-medium text-white shadow-lg transition-opacity hover:opacity-90',
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
|
<WrenchScrewdriverIcon className="h-6 w-6" />
|
||||||
官方网站
|
官方网站
|
||||||
</Button>
|
</a>
|
||||||
<Button
|
<a
|
||||||
as="a"
|
|
||||||
href="https://github.com/purp1e/hlae-site"
|
href="https://github.com/purp1e/hlae-site"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="bg-default-100 text-default-900 font-medium px-10 h-14 rounded-full text-lg hover:bg-default-200 transition-colors"
|
className={buttonVariants({
|
||||||
startContent={
|
className:
|
||||||
<svg
|
'bg-default-100 text-default-900 hover:bg-default-200 flex h-14 items-center gap-2 rounded-full px-10 text-lg font-medium transition-colors',
|
||||||
className="w-6 h-6"
|
})}
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
|
<svg
|
||||||
|
className="h-6 w-6"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
本项目
|
本项目
|
||||||
</Button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,51 +1,63 @@
|
|||||||
import {
|
import { Button, Avatar, Dropdown, Label } from '@heroui/react'
|
||||||
Link,
|
import { buttonVariants } from '@heroui/styles'
|
||||||
Button,
|
import { useEffect, useState } from 'react'
|
||||||
Avatar,
|
import { useSession, signOut } from '../lib/auth-client'
|
||||||
Dropdown,
|
import { SunIcon, MoonIcon } from '@heroicons/react/24/outline'
|
||||||
Label
|
import { Link as RouterLink } from 'react-router-dom'
|
||||||
} from "@heroui/react";
|
|
||||||
import { linkVariants, buttonVariants } from "@heroui/styles";
|
|
||||||
import { useEffect, 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";
|
|
||||||
|
|
||||||
export function SiteNavbar() {
|
export function SiteNavbar() {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession()
|
||||||
const [theme, setTheme] = useState<"light" | "dark">("light");
|
const [theme, setTheme] = useState<'light' | 'dark'>(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
return document.documentElement.classList.contains('dark')
|
||||||
|
? 'dark'
|
||||||
|
: 'light'
|
||||||
|
}
|
||||||
|
return 'light'
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentTheme = document.documentElement.classList.contains("dark") ? "dark" : "light";
|
// Sync theme on mount if needed, but the state is already initialized
|
||||||
setTheme(currentTheme);
|
}, [])
|
||||||
}, []);
|
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
const nextTheme = theme === "dark" ? "light" : "dark";
|
const nextTheme = theme === 'dark' ? 'light' : 'dark'
|
||||||
document.documentElement.classList.remove("light", "dark");
|
document.documentElement.classList.remove('light', 'dark')
|
||||||
document.documentElement.classList.add(nextTheme);
|
document.documentElement.classList.add(nextTheme)
|
||||||
document.documentElement.setAttribute("data-theme", nextTheme);
|
document.documentElement.setAttribute('data-theme', nextTheme)
|
||||||
localStorage.setItem("theme", nextTheme);
|
localStorage.setItem('theme', nextTheme)
|
||||||
setTheme(nextTheme);
|
setTheme(nextTheme)
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="flex items-center justify-between px-8 py-4 bg-background/70 backdrop-blur-md border-b border-default-100 sticky top-0 z-50 w-full">
|
<nav className="bg-background/70 border-default-100 sticky top-0 z-50 flex w-full items-center justify-between border-b px-8 py-4 backdrop-blur-md">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<RouterLink to="/" className="font-bold text-inherit text-xl no-underline text-foreground">
|
<RouterLink
|
||||||
|
to="/"
|
||||||
|
className="text-foreground text-xl font-bold text-inherit no-underline"
|
||||||
|
>
|
||||||
HLAE中文站
|
HLAE中文站
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-8">
|
<div className="flex items-center gap-8">
|
||||||
<div className="hidden sm:flex gap-8 items-center">
|
<div className="hidden items-center gap-8 sm:flex">
|
||||||
<RouterLink to="/" className="text-foreground/80 hover:text-foreground font-medium transition-colors">
|
<RouterLink
|
||||||
|
to="/"
|
||||||
|
className="text-foreground/80 hover:text-foreground font-medium transition-colors"
|
||||||
|
>
|
||||||
主页
|
主页
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<RouterLink to="/demo" className="text-foreground/80 hover:text-foreground font-medium transition-colors">
|
<RouterLink
|
||||||
|
to="/demo"
|
||||||
|
className="text-foreground/80 hover:text-foreground font-medium transition-colors"
|
||||||
|
>
|
||||||
击杀生成
|
击杀生成
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<RouterLink to="/about" className="text-foreground/80 hover:text-foreground font-medium transition-colors">
|
<RouterLink
|
||||||
|
to="/about"
|
||||||
|
className="text-foreground/80 hover:text-foreground font-medium transition-colors"
|
||||||
|
>
|
||||||
关于
|
关于
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,14 +65,14 @@ export function SiteNavbar() {
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
variant="light"
|
variant="ghost"
|
||||||
onPress={toggleTheme}
|
onPress={toggleTheme}
|
||||||
className="text-foreground/80"
|
className="text-foreground/80"
|
||||||
>
|
>
|
||||||
{theme === "dark" ? (
|
{theme === 'dark' ? (
|
||||||
<SunIcon className="w-5 h-5" />
|
<SunIcon className="h-5 w-5" />
|
||||||
) : (
|
) : (
|
||||||
<MoonIcon className="w-5 h-5" />
|
<MoonIcon className="h-5 w-5" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -70,11 +82,11 @@ export function SiteNavbar() {
|
|||||||
<Button isIconOnly variant="tertiary" className="rounded-full">
|
<Button isIconOnly variant="tertiary" className="rounded-full">
|
||||||
<Avatar size="sm" color="accent">
|
<Avatar size="sm" color="accent">
|
||||||
<Avatar.Image
|
<Avatar.Image
|
||||||
alt={session.user.name || "用户头像"}
|
alt={session.user.name || '用户头像'}
|
||||||
src={session.user.image || ""}
|
src={session.user.image || ''}
|
||||||
/>
|
/>
|
||||||
<Avatar.Fallback>
|
<Avatar.Fallback>
|
||||||
{(session.user.name || "U").slice(0, 2).toUpperCase()}
|
{(session.user.name || 'U').slice(0, 2).toUpperCase()}
|
||||||
</Avatar.Fallback>
|
</Avatar.Fallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -82,17 +94,30 @@ export function SiteNavbar() {
|
|||||||
<Dropdown.Popover>
|
<Dropdown.Popover>
|
||||||
<Dropdown.Menu aria-label="Profile Actions">
|
<Dropdown.Menu aria-label="Profile Actions">
|
||||||
<Dropdown.Item id="profile" textValue="Profile">
|
<Dropdown.Item id="profile" textValue="Profile">
|
||||||
<Label className="font-semibold">登录为 {session.user.email}</Label>
|
<Label className="font-semibold">
|
||||||
|
登录为 {session.user.email}
|
||||||
|
</Label>
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
<Dropdown.Item id="logout" textValue="Logout" onPress={() => signOut()}>
|
<Dropdown.Item
|
||||||
|
id="logout"
|
||||||
|
textValue="Logout"
|
||||||
|
variant="danger"
|
||||||
|
onPress={() => signOut()}
|
||||||
|
>
|
||||||
<Label className="text-danger">退出登录</Label>
|
<Label className="text-danger">退出登录</Label>
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
</Dropdown.Menu>
|
</Dropdown.Menu>
|
||||||
</Dropdown.Popover>
|
</Dropdown.Popover>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex items-center gap-4">
|
||||||
<RouterLink to="/login" className={buttonVariants({ variant: "light", className: "text-foreground font-medium" })}>
|
<RouterLink
|
||||||
|
to="/login"
|
||||||
|
className={buttonVariants({
|
||||||
|
variant: 'ghost',
|
||||||
|
className: 'text-foreground font-medium',
|
||||||
|
})}
|
||||||
|
>
|
||||||
登录
|
登录
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,5 +125,5 @@ export function SiteNavbar() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
@import "tailwindcss";
|
@import 'tailwindcss';
|
||||||
@import "@heroui/styles";
|
@import '@heroui/styles';
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--color-brand: #C14B4B;
|
--color-brand: #c14b4b;
|
||||||
--color-brand-foreground: #FFFFFF;
|
--color-brand-foreground: #ffffff;
|
||||||
|
|
||||||
--radius-2xl: 1.25rem;
|
--radius-2xl: 1.25rem;
|
||||||
--radius-3xl: 1.5rem;
|
--radius-3xl: 1.5rem;
|
||||||
@@ -22,6 +22,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground antialiased selection:bg-brand/20 selection:text-brand;
|
@apply bg-background text-foreground selection:bg-brand/20 selection:text-brand antialiased;
|
||||||
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
|
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,89 @@
|
|||||||
import useSWR from 'swr';
|
import useSWR from 'swr'
|
||||||
import { api } from '../api/client';
|
import { api } from '../api/client'
|
||||||
|
|
||||||
|
export interface Resource {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
description: string | null
|
||||||
|
url: string
|
||||||
|
icon: string | null
|
||||||
|
category: string | null
|
||||||
|
createdAt: string | Date
|
||||||
|
updatedAt: string | Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Comment {
|
||||||
|
id: string
|
||||||
|
content: string
|
||||||
|
userId: string
|
||||||
|
postId: string
|
||||||
|
createdAt: string | Date
|
||||||
|
updatedAt: string | Date
|
||||||
|
user?: {
|
||||||
|
name: string | null
|
||||||
|
image?: string | null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Post {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
userId: string
|
||||||
|
createdAt: string | Date
|
||||||
|
updatedAt: string | Date
|
||||||
|
user?: {
|
||||||
|
name: string | null
|
||||||
|
image?: string | null
|
||||||
|
}
|
||||||
|
comments?: Comment[]
|
||||||
|
}
|
||||||
|
|
||||||
export function useResources() {
|
export function useResources() {
|
||||||
const { data, error, isLoading, mutate } = useSWR('/resources', async () => {
|
const { data, error, isLoading, mutate } = useSWR('/resources', async () => {
|
||||||
const { data, error } = await api.resources.get();
|
const { data, error } = await api.resources.get()
|
||||||
if (error) throw error;
|
if (error) throw error
|
||||||
return data;
|
return data
|
||||||
});
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
resources: data,
|
resources: data,
|
||||||
isLoading,
|
isLoading,
|
||||||
isError: error,
|
isError: error,
|
||||||
mutate
|
mutate,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePosts() {
|
export function usePosts() {
|
||||||
const { data, error, isLoading, mutate } = useSWR('/posts', async () => {
|
const { data, error, isLoading, mutate } = useSWR('/posts', async () => {
|
||||||
const { data, error } = await api.posts.get();
|
const { data, error } = await api.posts.get()
|
||||||
if (error) throw error;
|
if (error) throw error
|
||||||
return data;
|
return data
|
||||||
});
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
posts: data,
|
posts: data,
|
||||||
isLoading,
|
isLoading,
|
||||||
isError: error,
|
isError: error,
|
||||||
mutate
|
mutate,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePost(id: string) {
|
export function usePost(id: string) {
|
||||||
const { data, error, isLoading, mutate } = useSWR(id ? `/posts/${id}` : null, async () => {
|
const { data, error, isLoading, mutate } = useSWR(
|
||||||
const { data, error } = await api.posts({ id }).get();
|
id ? `/posts/${id}` : null,
|
||||||
if (error) throw error;
|
async () => {
|
||||||
return data;
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
});
|
const { data, error } = await (api.posts as any)[id].get()
|
||||||
|
if (error) throw error
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
post: data,
|
post: data,
|
||||||
isLoading,
|
isLoading,
|
||||||
isError: error,
|
isError: error,
|
||||||
mutate
|
mutate,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { Toast } from "@heroui/react";
|
import { Toast } from '@heroui/react'
|
||||||
import "./globals.css";
|
import './globals.css'
|
||||||
import { SiteNavbar } from "./components/SiteNavbar";
|
import { SiteNavbar } from './components/SiteNavbar'
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background text-foreground">
|
<div className="bg-background text-foreground min-h-screen">
|
||||||
<Toast.Provider />
|
<Toast.Provider />
|
||||||
<SiteNavbar />
|
<SiteNavbar />
|
||||||
<main className="min-h-screen h-full">{children}</main>
|
<main className="h-full min-h-screen">{children}</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createAuthClient } from 'better-auth/react';
|
import { createAuthClient } from 'better-auth/react'
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
export const authClient = createAuthClient({
|
||||||
baseURL: import.meta.env.VITE_API_URL,
|
baseURL: import.meta.env.VITE_API_URL,
|
||||||
});
|
})
|
||||||
|
|
||||||
export const { useSession, signIn, signUp, signOut } = authClient;
|
export const { useSession, signIn, signUp, signOut } = authClient
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
export function Providers( { children }: { children: React.ReactNode } ) {
|
export function Providers({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return <>{children}</>
|
||||||
<>
|
|
||||||
{children}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,32 @@
|
|||||||
import { StrictMode, Suspense, useEffect } from "react";
|
import { StrictMode, Suspense, useEffect } from 'react'
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from 'react-dom/client'
|
||||||
import { BrowserRouter, useRoutes } from "react-router-dom";
|
import { BrowserRouter, useRoutes } from 'react-router-dom'
|
||||||
import { Providers } from "./providers";
|
import { Providers } from './providers'
|
||||||
import RootLayout from "./layout";
|
import RootLayout from './layout'
|
||||||
import routes from "~react-pages";
|
import routes from '~react-pages'
|
||||||
|
|
||||||
function App() {
|
export function App() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const storedTheme = localStorage.getItem("theme");
|
const storedTheme = localStorage.getItem('theme')
|
||||||
const systemPrefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
const systemPrefersDark = window.matchMedia(
|
||||||
const theme = storedTheme === "dark" || storedTheme === "light"
|
'(prefers-color-scheme: dark)',
|
||||||
? storedTheme
|
).matches
|
||||||
: systemPrefersDark
|
const theme =
|
||||||
? "dark"
|
storedTheme === 'dark' || storedTheme === 'light'
|
||||||
: "light";
|
? storedTheme
|
||||||
|
: systemPrefersDark
|
||||||
|
? 'dark'
|
||||||
|
: 'light'
|
||||||
|
|
||||||
document.documentElement.classList.remove("light", "dark");
|
document.documentElement.classList.remove('light', 'dark')
|
||||||
document.documentElement.classList.add(theme);
|
document.documentElement.classList.add(theme)
|
||||||
document.documentElement.setAttribute("data-theme", theme);
|
document.documentElement.setAttribute('data-theme', theme)
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
return <Suspense fallback={<p>Loading...</p>}>{useRoutes(routes)}</Suspense>;
|
return <Suspense fallback={<p>Loading...</p>}>{useRoutes(routes)}</Suspense>
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = createRoot(document.getElementById("root")!);
|
const app = createRoot(document.getElementById('root')!)
|
||||||
|
|
||||||
app.render(
|
app.render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
@@ -35,4 +38,4 @@ app.render(
|
|||||||
</Providers>
|
</Providers>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
);
|
)
|
||||||
|
|||||||
11
web/tsconfig.json
Normal file
11
web/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src", "vite-env.d.ts", "bun-env.d.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -1,35 +1,32 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from 'vite'
|
||||||
import path from "path";
|
import path from 'path'
|
||||||
import react from "@vitejs/plugin-react";
|
import react from '@vitejs/plugin-react'
|
||||||
import Pages from "vite-plugin-pages";
|
import Pages from 'vite-plugin-pages'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
root: __dirname,
|
root: __dirname,
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": path.resolve(__dirname, "src"),
|
'@': path.resolve(__dirname, 'src'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [react(), Pages({ dirs: ['src/app'] })],
|
||||||
react(),
|
|
||||||
Pages({ dirs: ["src/app"] }),
|
|
||||||
],
|
|
||||||
build: {
|
build: {
|
||||||
outDir: "dist",
|
outDir: 'dist',
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
manualChunks: {
|
manualChunks: {
|
||||||
// React core
|
// React core
|
||||||
"react-vendor": [
|
'react-vendor': [
|
||||||
"react",
|
'react',
|
||||||
"react-dom",
|
'react-dom',
|
||||||
"react-router",
|
'react-router',
|
||||||
"react-router-dom",
|
'react-router-dom',
|
||||||
],
|
],
|
||||||
// UI libraries
|
// UI libraries
|
||||||
"ui-vendor": ["@heroui/react"],
|
'ui-vendor': ['@heroui/react'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user