Compare commits
2 Commits
b0fa1a4201
...
0881846717
| Author | SHA1 | Date | |
|---|---|---|---|
| 0881846717 | |||
| d7149366e9 |
27
AGENTS.md
27
AGENTS.md
@@ -93,6 +93,17 @@ if (!session?.user?.id) return { success: false, message: "未授权" };
|
|||||||
// 变更前检查所有权
|
// 变更前检查所有权
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 日志
|
||||||
|
```typescript
|
||||||
|
import { createLogger } from "@/lib/logger";
|
||||||
|
|
||||||
|
const log = createLogger("folder-repository");
|
||||||
|
|
||||||
|
log.debug("Fetching public folders");
|
||||||
|
log.info("Fetched folders", { count: folders.length });
|
||||||
|
log.error("Failed to fetch folders", { error });
|
||||||
|
```
|
||||||
|
|
||||||
## 反模式 (本项目)
|
## 反模式 (本项目)
|
||||||
|
|
||||||
- ❌ `index.ts` barrel exports
|
- ❌ `index.ts` barrel exports
|
||||||
@@ -100,7 +111,7 @@ if (!session?.user?.id) return { success: false, message: "未授权" };
|
|||||||
- ❌ 用 API routes 做数据操作 (使用 Server Actions)
|
- ❌ 用 API routes 做数据操作 (使用 Server Actions)
|
||||||
- ❌ Server Component 可行时用 Client Component
|
- ❌ Server Component 可行时用 Client Component
|
||||||
- ❌ npm 或 yarn (使用 pnpm)
|
- ❌ npm 或 yarn (使用 pnpm)
|
||||||
- ❌ 生产代码中使用 `console.log`
|
- ❌ 生产代码中使用 `console.log` (使用 winston logger)
|
||||||
|
|
||||||
## 独特风格
|
## 独特风格
|
||||||
|
|
||||||
@@ -132,6 +143,20 @@ pnpm lint # ESLint
|
|||||||
pnpm prisma studio # 数据库 GUI
|
pnpm prisma studio # 数据库 GUI
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 数据库迁移
|
||||||
|
|
||||||
|
**必须使用 `prisma migrate dev`,禁止使用 `db push`:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 修改 schema 后创建迁移
|
||||||
|
DATABASE_URL=your_db_url pnpm prisma migrate dev --name your_migration_name
|
||||||
|
|
||||||
|
# 生成 Prisma Client
|
||||||
|
DATABASE_URL=your_db_url pnpm prisma generate
|
||||||
|
```
|
||||||
|
|
||||||
|
`db push` 会绕过迁移历史,导致生产环境无法正确迁移。
|
||||||
|
|
||||||
## 备注
|
## 备注
|
||||||
|
|
||||||
- Tailwind CSS v4 (无 tailwind.config.ts)
|
- Tailwind CSS v4 (无 tailwind.config.ts)
|
||||||
|
|||||||
@@ -159,7 +159,9 @@
|
|||||||
"sourceCode": "GitHub",
|
"sourceCode": "GitHub",
|
||||||
"sign_in": "Sign In",
|
"sign_in": "Sign In",
|
||||||
"profile": "Profile",
|
"profile": "Profile",
|
||||||
"folders": "Folders"
|
"folders": "Folders",
|
||||||
|
"explore": "Explore",
|
||||||
|
"favorites": "Favorites"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"myProfile": "My Profile",
|
"myProfile": "My Profile",
|
||||||
@@ -255,6 +257,26 @@
|
|||||||
"savedToFolder": "Saved to folder: {folderName}",
|
"savedToFolder": "Saved to folder: {folderName}",
|
||||||
"saveFailed": "Save failed, please try again later"
|
"saveFailed": "Save failed, please try again later"
|
||||||
},
|
},
|
||||||
|
"explore": {
|
||||||
|
"title": "Explore",
|
||||||
|
"subtitle": "Discover public folders",
|
||||||
|
"searchPlaceholder": "Search public folders...",
|
||||||
|
"loading": "Loading...",
|
||||||
|
"noFolders": "No public folders found",
|
||||||
|
"folderInfo": "{userName} • {totalPairs} pairs",
|
||||||
|
"unknownUser": "Unknown User",
|
||||||
|
"favorite": "Favorite",
|
||||||
|
"unfavorite": "Unfavorite",
|
||||||
|
"pleaseLogin": "Please login first"
|
||||||
|
},
|
||||||
|
"favorites": {
|
||||||
|
"title": "My Favorites",
|
||||||
|
"subtitle": "Folders you've favorited",
|
||||||
|
"loading": "Loading...",
|
||||||
|
"noFavorites": "No favorites yet",
|
||||||
|
"folderInfo": "{userName} • {totalPairs} pairs",
|
||||||
|
"unknownUser": "Unknown User"
|
||||||
|
},
|
||||||
"user_profile": {
|
"user_profile": {
|
||||||
"anonymous": "Anonymous",
|
"anonymous": "Anonymous",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
|||||||
@@ -159,7 +159,9 @@
|
|||||||
"sourceCode": "源码",
|
"sourceCode": "源码",
|
||||||
"sign_in": "登录",
|
"sign_in": "登录",
|
||||||
"profile": "个人资料",
|
"profile": "个人资料",
|
||||||
"folders": "文件夹"
|
"folders": "文件夹",
|
||||||
|
"explore": "探索",
|
||||||
|
"favorites": "收藏"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"myProfile": "我的个人资料",
|
"myProfile": "我的个人资料",
|
||||||
@@ -255,6 +257,34 @@
|
|||||||
"savedToFolder": "已保存到文件夹:{folderName}",
|
"savedToFolder": "已保存到文件夹:{folderName}",
|
||||||
"saveFailed": "保存失败,请稍后重试"
|
"saveFailed": "保存失败,请稍后重试"
|
||||||
},
|
},
|
||||||
|
"explore": {
|
||||||
|
"title": "探索",
|
||||||
|
"subtitle": "发现公开文件夹",
|
||||||
|
"searchPlaceholder": "搜索公开文件夹...",
|
||||||
|
"loading": "加载中...",
|
||||||
|
"noFolders": "没有找到公开文件夹",
|
||||||
|
"folderInfo": "{userName} • {totalPairs} 个文本对",
|
||||||
|
"unknownUser": "未知用户",
|
||||||
|
"favorite": "收藏",
|
||||||
|
"unfavorite": "取消收藏",
|
||||||
|
"pleaseLogin": "请先登录"
|
||||||
|
},
|
||||||
|
"favorites": {
|
||||||
|
"title": "收藏",
|
||||||
|
"subtitle": "我收藏的文件夹",
|
||||||
|
"loading": "加载中...",
|
||||||
|
"noFavorites": "还没有收藏",
|
||||||
|
"folderInfo": "{userName} • {totalPairs} 个文本对",
|
||||||
|
"unknownUser": "未知用户"
|
||||||
|
},
|
||||||
|
"favorites": {
|
||||||
|
"title": "我的收藏",
|
||||||
|
"subtitle": "收藏的公开文件夹",
|
||||||
|
"loading": "加载中...",
|
||||||
|
"noFavorites": "还没有收藏任何文件夹",
|
||||||
|
"folderInfo": "{userName} • {totalPairs} 个文本对",
|
||||||
|
"unknownUser": "未知用户"
|
||||||
|
},
|
||||||
"user_profile": {
|
"user_profile": {
|
||||||
"anonymous": "匿名",
|
"anonymous": "匿名",
|
||||||
"email": "邮箱",
|
"email": "邮箱",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"unstorage": "^1.17.3",
|
"unstorage": "^1.17.3",
|
||||||
|
"winston": "^3.19.0",
|
||||||
"zod": "^4.3.5",
|
"zod": "^4.3.5",
|
||||||
"zustand": "^5.0.11"
|
"zustand": "^5.0.11"
|
||||||
},
|
},
|
||||||
|
|||||||
163
pnpm-lock.yaml
generated
163
pnpm-lock.yaml
generated
@@ -60,6 +60,9 @@ importers:
|
|||||||
unstorage:
|
unstorage:
|
||||||
specifier: ^1.17.3
|
specifier: ^1.17.3
|
||||||
version: 1.17.3
|
version: 1.17.3
|
||||||
|
winston:
|
||||||
|
specifier: ^3.19.0
|
||||||
|
version: 3.19.0
|
||||||
zod:
|
zod:
|
||||||
specifier: ^4.3.5
|
specifier: ^4.3.5
|
||||||
version: 4.3.5
|
version: 4.3.5
|
||||||
@@ -321,6 +324,13 @@ packages:
|
|||||||
'@clack/prompts@0.11.0':
|
'@clack/prompts@0.11.0':
|
||||||
resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==}
|
resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==}
|
||||||
|
|
||||||
|
'@colors/colors@1.6.0':
|
||||||
|
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
|
||||||
|
engines: {node: '>=0.1.90'}
|
||||||
|
|
||||||
|
'@dabh/diagnostics@2.0.8':
|
||||||
|
resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==}
|
||||||
|
|
||||||
'@electric-sql/pglite-socket@0.0.20':
|
'@electric-sql/pglite-socket@0.0.20':
|
||||||
resolution: {integrity: sha512-J5nLGsicnD9wJHnno9r+DGxfcZWh+YJMCe0q/aCgtG6XOm9Z7fKeite8IZSNXgZeGltSigM9U/vAWZQWdgcSFg==}
|
resolution: {integrity: sha512-J5nLGsicnD9wJHnno9r+DGxfcZWh+YJMCe0q/aCgtG6XOm9Z7fKeite8IZSNXgZeGltSigM9U/vAWZQWdgcSFg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -841,6 +851,9 @@ packages:
|
|||||||
'@schummar/icu-type-parser@1.21.5':
|
'@schummar/icu-type-parser@1.21.5':
|
||||||
resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==}
|
resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==}
|
||||||
|
|
||||||
|
'@so-ric/colorspace@1.1.6':
|
||||||
|
resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==}
|
||||||
|
|
||||||
'@standard-schema/spec@1.0.0':
|
'@standard-schema/spec@1.0.0':
|
||||||
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
|
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
|
||||||
|
|
||||||
@@ -1047,6 +1060,9 @@ packages:
|
|||||||
'@types/react@19.2.7':
|
'@types/react@19.2.7':
|
||||||
resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==}
|
resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==}
|
||||||
|
|
||||||
|
'@types/triple-beam@1.3.5':
|
||||||
|
resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.48.1':
|
'@typescript-eslint/eslint-plugin@8.48.1':
|
||||||
resolution: {integrity: sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==}
|
resolution: {integrity: sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
@@ -1345,6 +1361,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
|
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
async@3.2.6:
|
||||||
|
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
|
||||||
|
|
||||||
available-typed-arrays@1.0.7:
|
available-typed-arrays@1.0.7:
|
||||||
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1547,9 +1566,25 @@ packages:
|
|||||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
engines: {node: '>=7.0.0'}
|
engines: {node: '>=7.0.0'}
|
||||||
|
|
||||||
|
color-convert@3.1.3:
|
||||||
|
resolution: {integrity: sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==}
|
||||||
|
engines: {node: '>=14.6'}
|
||||||
|
|
||||||
color-name@1.1.4:
|
color-name@1.1.4:
|
||||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
|
|
||||||
|
color-name@2.1.0:
|
||||||
|
resolution: {integrity: sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==}
|
||||||
|
engines: {node: '>=12.20'}
|
||||||
|
|
||||||
|
color-string@2.1.4:
|
||||||
|
resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
color@5.0.3:
|
||||||
|
resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
commander@12.1.0:
|
commander@12.1.0:
|
||||||
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
|
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -1790,6 +1825,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==}
|
resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
enabled@2.0.0:
|
||||||
|
resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==}
|
||||||
|
|
||||||
end-of-stream@1.4.5:
|
end-of-stream@1.4.5:
|
||||||
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
|
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
|
||||||
|
|
||||||
@@ -1989,6 +2027,9 @@ packages:
|
|||||||
picomatch:
|
picomatch:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
fecha@4.2.3:
|
||||||
|
resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==}
|
||||||
|
|
||||||
file-entry-cache@8.0.0:
|
file-entry-cache@8.0.0:
|
||||||
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
@@ -2011,6 +2052,9 @@ packages:
|
|||||||
flatted@3.3.3:
|
flatted@3.3.3:
|
||||||
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
|
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
|
||||||
|
|
||||||
|
fn.name@1.1.0:
|
||||||
|
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
|
||||||
|
|
||||||
for-each@0.3.5:
|
for-each@0.3.5:
|
||||||
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -2278,6 +2322,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==}
|
resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
is-stream@2.0.1:
|
||||||
|
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
is-string@1.1.1:
|
is-string@1.1.1:
|
||||||
resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
|
resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -2364,6 +2412,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
|
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
kuler@2.0.0:
|
||||||
|
resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==}
|
||||||
|
|
||||||
kysely@0.28.8:
|
kysely@0.28.8:
|
||||||
resolution: {integrity: sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA==}
|
resolution: {integrity: sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -2467,6 +2518,10 @@ packages:
|
|||||||
lodash@4.17.21:
|
lodash@4.17.21:
|
||||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||||
|
|
||||||
|
logform@2.7.0:
|
||||||
|
resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
long@5.3.2:
|
long@5.3.2:
|
||||||
resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
|
resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
|
||||||
|
|
||||||
@@ -2656,6 +2711,9 @@ packages:
|
|||||||
once@1.4.0:
|
once@1.4.0:
|
||||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||||
|
|
||||||
|
one-time@1.0.0:
|
||||||
|
resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==}
|
||||||
|
|
||||||
open@10.2.0:
|
open@10.2.0:
|
||||||
resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==}
|
resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -2930,6 +2988,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
|
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
safe-stable-stringify@2.5.0:
|
||||||
|
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
safer-buffer@2.1.2:
|
safer-buffer@2.1.2:
|
||||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||||
|
|
||||||
@@ -3028,6 +3090,9 @@ packages:
|
|||||||
stable-hash@0.0.5:
|
stable-hash@0.0.5:
|
||||||
resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
|
resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
|
||||||
|
|
||||||
|
stack-trace@0.0.10:
|
||||||
|
resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==}
|
||||||
|
|
||||||
std-env@3.10.0:
|
std-env@3.10.0:
|
||||||
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
|
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
|
||||||
|
|
||||||
@@ -3111,6 +3176,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
text-hex@1.0.0:
|
||||||
|
resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==}
|
||||||
|
|
||||||
tinyexec@1.0.2:
|
tinyexec@1.0.2:
|
||||||
resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
|
resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -3123,6 +3191,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||||
engines: {node: '>=8.0'}
|
engines: {node: '>=8.0'}
|
||||||
|
|
||||||
|
triple-beam@1.4.1:
|
||||||
|
resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==}
|
||||||
|
engines: {node: '>= 14.0.0'}
|
||||||
|
|
||||||
ts-api-utils@2.1.0:
|
ts-api-utils@2.1.0:
|
||||||
resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
|
resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
|
||||||
engines: {node: '>=18.12'}
|
engines: {node: '>=18.12'}
|
||||||
@@ -3300,6 +3372,14 @@ packages:
|
|||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
winston-transport@4.9.0:
|
||||||
|
resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
|
winston@3.19.0:
|
||||||
|
resolution: {integrity: sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
word-wrap@1.2.5:
|
word-wrap@1.2.5:
|
||||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -3708,6 +3788,14 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
sisteransi: 1.0.5
|
sisteransi: 1.0.5
|
||||||
|
|
||||||
|
'@colors/colors@1.6.0': {}
|
||||||
|
|
||||||
|
'@dabh/diagnostics@2.0.8':
|
||||||
|
dependencies:
|
||||||
|
'@so-ric/colorspace': 1.1.6
|
||||||
|
enabled: 2.0.0
|
||||||
|
kuler: 2.0.0
|
||||||
|
|
||||||
'@electric-sql/pglite-socket@0.0.20(@electric-sql/pglite@0.3.15)':
|
'@electric-sql/pglite-socket@0.0.20(@electric-sql/pglite@0.3.15)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@electric-sql/pglite': 0.3.15
|
'@electric-sql/pglite': 0.3.15
|
||||||
@@ -4156,6 +4244,11 @@ snapshots:
|
|||||||
|
|
||||||
'@schummar/icu-type-parser@1.21.5': {}
|
'@schummar/icu-type-parser@1.21.5': {}
|
||||||
|
|
||||||
|
'@so-ric/colorspace@1.1.6':
|
||||||
|
dependencies:
|
||||||
|
color: 5.0.3
|
||||||
|
text-hex: 1.0.0
|
||||||
|
|
||||||
'@standard-schema/spec@1.0.0': {}
|
'@standard-schema/spec@1.0.0': {}
|
||||||
|
|
||||||
'@standard-schema/spec@1.1.0': {}
|
'@standard-schema/spec@1.1.0': {}
|
||||||
@@ -4314,6 +4407,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
csstype: 3.2.3
|
csstype: 3.2.3
|
||||||
|
|
||||||
|
'@types/triple-beam@1.3.5': {}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
'@typescript-eslint/eslint-plugin@8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.12.2
|
'@eslint-community/regexpp': 4.12.2
|
||||||
@@ -4659,6 +4754,8 @@ snapshots:
|
|||||||
|
|
||||||
async-function@1.0.0: {}
|
async-function@1.0.0: {}
|
||||||
|
|
||||||
|
async@3.2.6: {}
|
||||||
|
|
||||||
available-typed-arrays@1.0.7:
|
available-typed-arrays@1.0.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
possible-typed-array-names: 1.1.0
|
possible-typed-array-names: 1.1.0
|
||||||
@@ -4874,8 +4971,23 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
color-name: 1.1.4
|
color-name: 1.1.4
|
||||||
|
|
||||||
|
color-convert@3.1.3:
|
||||||
|
dependencies:
|
||||||
|
color-name: 2.1.0
|
||||||
|
|
||||||
color-name@1.1.4: {}
|
color-name@1.1.4: {}
|
||||||
|
|
||||||
|
color-name@2.1.0: {}
|
||||||
|
|
||||||
|
color-string@2.1.4:
|
||||||
|
dependencies:
|
||||||
|
color-name: 2.1.0
|
||||||
|
|
||||||
|
color@5.0.3:
|
||||||
|
dependencies:
|
||||||
|
color-convert: 3.1.3
|
||||||
|
color-string: 2.1.4
|
||||||
|
|
||||||
commander@12.1.0: {}
|
commander@12.1.0: {}
|
||||||
|
|
||||||
concat-map@0.0.1: {}
|
concat-map@0.0.1: {}
|
||||||
@@ -5012,6 +5124,8 @@ snapshots:
|
|||||||
|
|
||||||
empathic@2.0.0: {}
|
empathic@2.0.0: {}
|
||||||
|
|
||||||
|
enabled@2.0.0: {}
|
||||||
|
|
||||||
end-of-stream@1.4.5:
|
end-of-stream@1.4.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
@@ -5359,6 +5473,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
|
|
||||||
|
fecha@4.2.3: {}
|
||||||
|
|
||||||
file-entry-cache@8.0.0:
|
file-entry-cache@8.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
flat-cache: 4.0.1
|
flat-cache: 4.0.1
|
||||||
@@ -5381,6 +5497,8 @@ snapshots:
|
|||||||
|
|
||||||
flatted@3.3.3: {}
|
flatted@3.3.3: {}
|
||||||
|
|
||||||
|
fn.name@1.1.0: {}
|
||||||
|
|
||||||
for-each@0.3.5:
|
for-each@0.3.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-callable: 1.2.7
|
is-callable: 1.2.7
|
||||||
@@ -5655,6 +5773,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
call-bound: 1.0.4
|
call-bound: 1.0.4
|
||||||
|
|
||||||
|
is-stream@2.0.1: {}
|
||||||
|
|
||||||
is-string@1.1.1:
|
is-string@1.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bound: 1.0.4
|
call-bound: 1.0.4
|
||||||
@@ -5735,6 +5855,8 @@ snapshots:
|
|||||||
|
|
||||||
kleur@3.0.3: {}
|
kleur@3.0.3: {}
|
||||||
|
|
||||||
|
kuler@2.0.0: {}
|
||||||
|
|
||||||
kysely@0.28.8: {}
|
kysely@0.28.8: {}
|
||||||
|
|
||||||
language-subtag-registry@0.3.23: {}
|
language-subtag-registry@0.3.23: {}
|
||||||
@@ -5807,6 +5929,15 @@ snapshots:
|
|||||||
|
|
||||||
lodash@4.17.21: {}
|
lodash@4.17.21: {}
|
||||||
|
|
||||||
|
logform@2.7.0:
|
||||||
|
dependencies:
|
||||||
|
'@colors/colors': 1.6.0
|
||||||
|
'@types/triple-beam': 1.3.5
|
||||||
|
fecha: 4.2.3
|
||||||
|
ms: 2.1.3
|
||||||
|
safe-stable-stringify: 2.5.0
|
||||||
|
triple-beam: 1.4.1
|
||||||
|
|
||||||
long@5.3.2: {}
|
long@5.3.2: {}
|
||||||
|
|
||||||
loose-envify@1.4.0:
|
loose-envify@1.4.0:
|
||||||
@@ -6001,6 +6132,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
wrappy: 1.0.2
|
wrappy: 1.0.2
|
||||||
|
|
||||||
|
one-time@1.0.0:
|
||||||
|
dependencies:
|
||||||
|
fn.name: 1.1.0
|
||||||
|
|
||||||
open@10.2.0:
|
open@10.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
default-browser: 5.4.0
|
default-browser: 5.4.0
|
||||||
@@ -6292,6 +6427,8 @@ snapshots:
|
|||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
is-regex: 1.2.1
|
is-regex: 1.2.1
|
||||||
|
|
||||||
|
safe-stable-stringify@2.5.0: {}
|
||||||
|
|
||||||
safer-buffer@2.1.2: {}
|
safer-buffer@2.1.2: {}
|
||||||
|
|
||||||
scheduler@0.27.0: {}
|
scheduler@0.27.0: {}
|
||||||
@@ -6419,6 +6556,8 @@ snapshots:
|
|||||||
|
|
||||||
stable-hash@0.0.5: {}
|
stable-hash@0.0.5: {}
|
||||||
|
|
||||||
|
stack-trace@0.0.10: {}
|
||||||
|
|
||||||
std-env@3.10.0: {}
|
std-env@3.10.0: {}
|
||||||
|
|
||||||
stop-iteration-iterator@1.1.0:
|
stop-iteration-iterator@1.1.0:
|
||||||
@@ -6520,6 +6659,8 @@ snapshots:
|
|||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
readable-stream: 3.6.2
|
readable-stream: 3.6.2
|
||||||
|
|
||||||
|
text-hex@1.0.0: {}
|
||||||
|
|
||||||
tinyexec@1.0.2: {}
|
tinyexec@1.0.2: {}
|
||||||
|
|
||||||
tinyglobby@0.2.15:
|
tinyglobby@0.2.15:
|
||||||
@@ -6531,6 +6672,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-number: 7.0.0
|
is-number: 7.0.0
|
||||||
|
|
||||||
|
triple-beam@1.4.1: {}
|
||||||
|
|
||||||
ts-api-utils@2.1.0(typescript@5.9.3):
|
ts-api-utils@2.1.0(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
@@ -6718,6 +6861,26 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
isexe: 2.0.0
|
isexe: 2.0.0
|
||||||
|
|
||||||
|
winston-transport@4.9.0:
|
||||||
|
dependencies:
|
||||||
|
logform: 2.7.0
|
||||||
|
readable-stream: 3.6.2
|
||||||
|
triple-beam: 1.4.1
|
||||||
|
|
||||||
|
winston@3.19.0:
|
||||||
|
dependencies:
|
||||||
|
'@colors/colors': 1.6.0
|
||||||
|
'@dabh/diagnostics': 2.0.8
|
||||||
|
async: 3.2.6
|
||||||
|
is-stream: 2.0.1
|
||||||
|
logform: 2.7.0
|
||||||
|
one-time: 1.0.0
|
||||||
|
readable-stream: 3.6.2
|
||||||
|
safe-stable-stringify: 2.5.0
|
||||||
|
stack-trace: 0.0.10
|
||||||
|
triple-beam: 1.4.1
|
||||||
|
winston-transport: 4.9.0
|
||||||
|
|
||||||
word-wrap@1.2.5: {}
|
word-wrap@1.2.5: {}
|
||||||
|
|
||||||
wrappy@1.0.2: {}
|
wrappy@1.0.2: {}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "Visibility" AS ENUM ('PRIVATE', 'PUBLIC');
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "folders" ADD COLUMN "visibility" "Visibility" NOT NULL DEFAULT 'PRIVATE';
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "folder_favorites" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"user_id" TEXT NOT NULL,
|
||||||
|
"folder_id" INTEGER NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "folder_favorites_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "folder_favorites_folder_id_idx" ON "folder_favorites"("folder_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "folder_favorites_user_id_folder_id_key" ON "folder_favorites"("user_id", "folder_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "folder_favorites_user_id_idx" ON "folder_favorites"("user_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "folders_visibility_idx" ON "folders"("visibility");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "folder_favorites" ADD CONSTRAINT "folder_favorites_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "folder_favorites" ADD CONSTRAINT "folder_favorites_folder_id_fkey" FOREIGN KEY ("folder_id") REFERENCES "folders"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
195
src/app/(features)/explore/ExploreClient.tsx
Normal file
195
src/app/(features)/explore/ExploreClient.tsx
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ChevronRight,
|
||||||
|
Folder as Fd,
|
||||||
|
Heart,
|
||||||
|
Search,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { CircleButton } from "@/design-system/base/button";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { PageLayout } from "@/components/ui/PageLayout";
|
||||||
|
import { PageHeader } from "@/components/ui/PageHeader";
|
||||||
|
import { CardList } from "@/components/ui/CardList";
|
||||||
|
import {
|
||||||
|
actionSearchPublicFolders,
|
||||||
|
actionToggleFavorite,
|
||||||
|
actionCheckFavorite,
|
||||||
|
} from "@/modules/folder/folder-aciton";
|
||||||
|
import { TPublicFolder } from "@/shared/folder-type";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
|
||||||
|
interface PublicFolderCardProps {
|
||||||
|
folder: TPublicFolder;
|
||||||
|
currentUserId?: string;
|
||||||
|
onFavoriteChange?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PublicFolderCard = ({ folder, currentUserId, onFavoriteChange }: PublicFolderCardProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const t = useTranslations("explore");
|
||||||
|
const [isFavorited, setIsFavorited] = useState(false);
|
||||||
|
const [favoriteCount, setFavoriteCount] = useState(folder.favoriteCount);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentUserId) {
|
||||||
|
actionCheckFavorite(folder.id).then((result) => {
|
||||||
|
if (result.success && result.data) {
|
||||||
|
setIsFavorited(result.data.isFavorited);
|
||||||
|
setFavoriteCount(result.data.favoriteCount);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [folder.id, currentUserId]);
|
||||||
|
|
||||||
|
const handleToggleFavorite = async (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!currentUserId) {
|
||||||
|
toast.error(t("pleaseLogin"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await actionToggleFavorite(folder.id);
|
||||||
|
if (result.success && result.data) {
|
||||||
|
setIsFavorited(result.data.isFavorited);
|
||||||
|
setFavoriteCount(result.data.favoriteCount);
|
||||||
|
onFavoriteChange?.();
|
||||||
|
} else {
|
||||||
|
toast.error(result.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex justify-between items-center group py-4 px-5 border-b border-gray-200 last:border-b-0 hover:bg-gray-50 cursor-pointer transition-colors"
|
||||||
|
onClick={() => {
|
||||||
|
router.push(`/explore/${folder.id}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4 flex-1">
|
||||||
|
<div className="shrink-0 text-primary-500">
|
||||||
|
<Fd size={24} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="font-semibold text-gray-900 truncate">{folder.name}</h3>
|
||||||
|
<p className="text-sm text-gray-500 mt-0.5">
|
||||||
|
{t("folderInfo", {
|
||||||
|
userName: folder.userName ?? folder.userUsername ?? t("unknownUser"),
|
||||||
|
totalPairs: folder.totalPairs,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex items-center gap-1 text-sm text-gray-400">
|
||||||
|
<Heart
|
||||||
|
size={14}
|
||||||
|
className={isFavorited ? "fill-red-500 text-red-500" : ""}
|
||||||
|
/>
|
||||||
|
<span>{favoriteCount}</span>
|
||||||
|
</div>
|
||||||
|
<CircleButton
|
||||||
|
onClick={handleToggleFavorite}
|
||||||
|
title={isFavorited ? t("unfavorite") : t("favorite")}
|
||||||
|
>
|
||||||
|
<Heart
|
||||||
|
size={18}
|
||||||
|
className={isFavorited ? "fill-red-500 text-red-500" : ""}
|
||||||
|
/>
|
||||||
|
</CircleButton>
|
||||||
|
<ChevronRight size={20} className="text-gray-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ExploreClientProps {
|
||||||
|
initialPublicFolders: TPublicFolder[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ExploreClient({ initialPublicFolders }: ExploreClientProps) {
|
||||||
|
const t = useTranslations("explore");
|
||||||
|
const router = useRouter();
|
||||||
|
const [publicFolders, setPublicFolders] = useState<TPublicFolder[]>(initialPublicFolders);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
|
const { data: session } = authClient.useSession();
|
||||||
|
const currentUserId = session?.user?.id;
|
||||||
|
|
||||||
|
const handleSearch = async () => {
|
||||||
|
if (!searchQuery.trim()) {
|
||||||
|
setPublicFolders(initialPublicFolders);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
const result = await actionSearchPublicFolders(searchQuery.trim());
|
||||||
|
if (result.success && result.data) {
|
||||||
|
setPublicFolders(result.data);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshFolders = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const result = await actionSearchPublicFolders(searchQuery.trim() || "");
|
||||||
|
if (result.success && result.data) {
|
||||||
|
setPublicFolders(result.data);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageLayout>
|
||||||
|
<PageHeader title={t("title")} subtitle={t("subtitle")} />
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Search size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
|
||||||
|
placeholder={t("searchPlaceholder")}
|
||||||
|
className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<CircleButton onClick={handleSearch}>
|
||||||
|
<Search size={18} />
|
||||||
|
</CircleButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<CardList>
|
||||||
|
{loading ? (
|
||||||
|
<div className="p-8 text-center">
|
||||||
|
<div className="w-8 h-8 border-2 border-gray-200 border-t-gray-400 rounded-full animate-spin mx-auto mb-3"></div>
|
||||||
|
<p className="text-sm text-gray-500">{t("loading")}</p>
|
||||||
|
</div>
|
||||||
|
) : publicFolders.length === 0 ? (
|
||||||
|
<div className="text-center py-12 text-gray-400">
|
||||||
|
<div className="w-16 h-16 mx-auto mb-3 rounded-full bg-gray-100 flex items-center justify-center">
|
||||||
|
<Fd size={24} className="text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm">{t("noFolders")}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
publicFolders.map((folder) => (
|
||||||
|
<PublicFolderCard
|
||||||
|
key={folder.id}
|
||||||
|
folder={folder}
|
||||||
|
currentUserId={currentUserId}
|
||||||
|
onFavoriteChange={refreshFolders}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</CardList>
|
||||||
|
</div>
|
||||||
|
</PageLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
29
src/app/(features)/explore/[id]/page.tsx
Normal file
29
src/app/(features)/explore/[id]/page.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { InFolder } from "@/app/folders/[folder_id]/InFolder";
|
||||||
|
import { actionGetFolderVisibility } from "@/modules/folder/folder-aciton";
|
||||||
|
|
||||||
|
export default async function ExploreFolderPage({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ id: string }>;
|
||||||
|
}) {
|
||||||
|
const { id } = await params;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
redirect("/explore");
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderInfo = (await actionGetFolderVisibility(Number(id))).data;
|
||||||
|
|
||||||
|
if (!folderInfo) {
|
||||||
|
redirect("/explore");
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPublic = folderInfo.visibility === "PUBLIC";
|
||||||
|
|
||||||
|
if (!isPublic) {
|
||||||
|
redirect("/explore");
|
||||||
|
}
|
||||||
|
|
||||||
|
return <InFolder folderId={Number(id)} isReadOnly={true} />;
|
||||||
|
}
|
||||||
9
src/app/(features)/explore/page.tsx
Normal file
9
src/app/(features)/explore/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { ExploreClient } from "./ExploreClient";
|
||||||
|
import { actionGetPublicFolders } from "@/modules/folder/folder-aciton";
|
||||||
|
|
||||||
|
export default async function ExplorePage() {
|
||||||
|
const publicFoldersResult = await actionGetPublicFolders();
|
||||||
|
const publicFolders = publicFoldersResult.success ? publicFoldersResult.data ?? [] : [];
|
||||||
|
|
||||||
|
return <ExploreClient initialPublicFolders={publicFolders} />;
|
||||||
|
}
|
||||||
116
src/app/(features)/favorites/FavoritesClient.tsx
Normal file
116
src/app/(features)/favorites/FavoritesClient.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ChevronRight,
|
||||||
|
Folder as Fd,
|
||||||
|
Heart,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { PageLayout } from "@/components/ui/PageLayout";
|
||||||
|
import { PageHeader } from "@/components/ui/PageHeader";
|
||||||
|
import { CardList } from "@/components/ui/CardList";
|
||||||
|
import { actionGetUserFavorites } from "@/modules/folder/folder-aciton";
|
||||||
|
|
||||||
|
type UserFavorite = {
|
||||||
|
id: number;
|
||||||
|
folderId: number;
|
||||||
|
folderName: string;
|
||||||
|
folderCreatedAt: Date;
|
||||||
|
folderTotalPairs: number;
|
||||||
|
folderOwnerId: string;
|
||||||
|
folderOwnerName: string | null;
|
||||||
|
folderOwnerUsername: string | null;
|
||||||
|
favoritedAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface FavoriteCardProps {
|
||||||
|
favorite: UserFavorite;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FavoriteCard = ({ favorite }: FavoriteCardProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const t = useTranslations("favorites");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex justify-between items-center group py-4 px-5 border-b border-gray-200 last:border-b-0 hover:bg-gray-50 cursor-pointer transition-colors"
|
||||||
|
onClick={() => {
|
||||||
|
router.push(`/explore/${favorite.folderId}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4 flex-1">
|
||||||
|
<div className="shrink-0 text-primary-500">
|
||||||
|
<Fd size={24} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="font-semibold text-gray-900 truncate">{favorite.folderName}</h3>
|
||||||
|
<p className="text-sm text-gray-500 mt-0.5">
|
||||||
|
{t("folderInfo", {
|
||||||
|
userName: favorite.folderOwnerName ?? favorite.folderOwnerUsername ?? t("unknownUser"),
|
||||||
|
totalPairs: favorite.folderTotalPairs,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Heart size={18} className="fill-red-500 text-red-500" />
|
||||||
|
<ChevronRight size={20} className="text-gray-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface FavoritesClientProps {
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FavoritesClient({ userId }: FavoritesClientProps) {
|
||||||
|
const t = useTranslations("favorites");
|
||||||
|
const [favorites, setFavorites] = useState<UserFavorite[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadFavorites();
|
||||||
|
}, [userId]);
|
||||||
|
|
||||||
|
const loadFavorites = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const result = await actionGetUserFavorites();
|
||||||
|
if (result.success && result.data) {
|
||||||
|
setFavorites(result.data);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageLayout>
|
||||||
|
<PageHeader title={t("title")} subtitle={t("subtitle")} />
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<CardList>
|
||||||
|
{loading ? (
|
||||||
|
<div className="p-8 text-center">
|
||||||
|
<div className="w-8 h-8 border-2 border-gray-200 border-t-gray-400 rounded-full animate-spin mx-auto mb-3"></div>
|
||||||
|
<p className="text-sm text-gray-500">{t("loading")}</p>
|
||||||
|
</div>
|
||||||
|
) : favorites.length === 0 ? (
|
||||||
|
<div className="text-center py-12 text-gray-400">
|
||||||
|
<div className="w-16 h-16 mx-auto mb-3 rounded-full bg-gray-100 flex items-center justify-center">
|
||||||
|
<Heart size={24} className="text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm">{t("noFavorites")}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
favorites.map((favorite) => (
|
||||||
|
<FavoriteCard key={favorite.id} favorite={favorite} />
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</CardList>
|
||||||
|
</div>
|
||||||
|
</PageLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
src/app/(features)/favorites/page.tsx
Normal file
14
src/app/(features)/favorites/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { auth } from "@/auth";
|
||||||
|
import { headers } from "next/headers";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { FavoritesClient } from "./FavoritesClient";
|
||||||
|
|
||||||
|
export default async function FavoritesPage() {
|
||||||
|
const session = await auth.api.getSession({ headers: await headers() });
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
redirect("/login?redirect=/favorites");
|
||||||
|
}
|
||||||
|
|
||||||
|
return <FavoritesClient userId={session.user.id} />;
|
||||||
|
}
|
||||||
@@ -6,9 +6,7 @@ import {
|
|||||||
FolderPen,
|
FolderPen,
|
||||||
FolderPlus,
|
FolderPlus,
|
||||||
Globe,
|
Globe,
|
||||||
Heart,
|
|
||||||
Lock,
|
Lock,
|
||||||
Search,
|
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { CircleButton, LightButton } from "@/design-system/base/button";
|
import { CircleButton, LightButton } from "@/design-system/base/button";
|
||||||
@@ -24,22 +22,16 @@ import {
|
|||||||
actionDeleteFolderById,
|
actionDeleteFolderById,
|
||||||
actionGetFoldersWithTotalPairsByUserId,
|
actionGetFoldersWithTotalPairsByUserId,
|
||||||
actionRenameFolderById,
|
actionRenameFolderById,
|
||||||
actionSearchPublicFolders,
|
|
||||||
actionSetFolderVisibility,
|
actionSetFolderVisibility,
|
||||||
actionToggleFavorite,
|
|
||||||
actionCheckFavorite,
|
|
||||||
} from "@/modules/folder/folder-aciton";
|
} from "@/modules/folder/folder-aciton";
|
||||||
import { TPublicFolder, TSharedFolderWithTotalPairs } from "@/shared/folder-type";
|
import { TSharedFolderWithTotalPairs } from "@/shared/folder-type";
|
||||||
|
|
||||||
type TabType = "my" | "public";
|
interface FolderCardProps {
|
||||||
|
|
||||||
interface FolderProps {
|
|
||||||
folder: TSharedFolderWithTotalPairs;
|
folder: TSharedFolderWithTotalPairs;
|
||||||
refresh: () => void;
|
refresh: () => void;
|
||||||
showVisibility?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const FolderCard = ({ folder, refresh, showVisibility = true }: FolderProps) => {
|
const FolderCard = ({ folder, refresh }: FolderCardProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const t = useTranslations("folders");
|
const t = useTranslations("folders");
|
||||||
|
|
||||||
@@ -69,16 +61,14 @@ const FolderCard = ({ folder, refresh, showVisibility = true }: FolderProps) =>
|
|||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="font-semibold text-gray-900 truncate">{folder.name}</h3>
|
<h3 className="font-semibold text-gray-900 truncate">{folder.name}</h3>
|
||||||
{showVisibility && (
|
<span className="flex items-center gap-1 text-xs text-gray-400">
|
||||||
<span className="flex items-center gap-1 text-xs text-gray-400">
|
{folder.visibility === "PUBLIC" ? (
|
||||||
{folder.visibility === "PUBLIC" ? (
|
<Globe size={12} />
|
||||||
<Globe size={12} />
|
) : (
|
||||||
) : (
|
<Lock size={12} />
|
||||||
<Lock size={12} />
|
)}
|
||||||
)}
|
{folder.visibility === "PUBLIC" ? t("public") : t("private")}
|
||||||
{folder.visibility === "PUBLIC" ? t("public") : t("private")}
|
</span>
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-500 mt-0.5">
|
<p className="text-sm text-gray-500 mt-0.5">
|
||||||
{t("folderInfo", {
|
{t("folderInfo", {
|
||||||
@@ -91,32 +81,28 @@ const FolderCard = ({ folder, refresh, showVisibility = true }: FolderProps) =>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-1 ml-4">
|
<div className="flex items-center gap-1 ml-4">
|
||||||
{showVisibility && (
|
<CircleButton
|
||||||
<CircleButton
|
onClick={handleToggleVisibility}
|
||||||
onClick={handleToggleVisibility}
|
title={folder.visibility === "PUBLIC" ? t("setPrivate") : t("setPublic")}
|
||||||
title={folder.visibility === "PUBLIC" ? t("setPrivate") : t("setPublic")}
|
>
|
||||||
>
|
{folder.visibility === "PUBLIC" ? (
|
||||||
{folder.visibility === "PUBLIC" ? (
|
<Lock size={18} />
|
||||||
<Lock size={18} />
|
) : (
|
||||||
) : (
|
<Globe size={18} />
|
||||||
<Globe size={18} />
|
)}
|
||||||
)}
|
</CircleButton>
|
||||||
</CircleButton>
|
|
||||||
)}
|
|
||||||
<CircleButton
|
<CircleButton
|
||||||
onClick={(e: React.MouseEvent) => {
|
onClick={(e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const newName = prompt(t("enterNewName"))?.trim();
|
const newName = prompt(t("enterNewName"))?.trim();
|
||||||
if (newName && newName.length > 0) {
|
if (newName && newName.length > 0) {
|
||||||
actionRenameFolderById(folder.id, newName)
|
actionRenameFolderById(folder.id, newName).then((result) => {
|
||||||
.then(result => {
|
if (result.success) {
|
||||||
if (result.success) {
|
refresh();
|
||||||
refresh();
|
} else {
|
||||||
}
|
toast.error(result.message);
|
||||||
else {
|
}
|
||||||
toast.error(result.message);
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -127,15 +113,13 @@ const FolderCard = ({ folder, refresh, showVisibility = true }: FolderProps) =>
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const confirm = prompt(t("confirmDelete", { name: folder.name }));
|
const confirm = prompt(t("confirmDelete", { name: folder.name }));
|
||||||
if (confirm === folder.name) {
|
if (confirm === folder.name) {
|
||||||
actionDeleteFolderById(folder.id)
|
actionDeleteFolderById(folder.id).then((result) => {
|
||||||
.then(result => {
|
if (result.success) {
|
||||||
if (result.success) {
|
refresh();
|
||||||
refresh();
|
} else {
|
||||||
}
|
toast.error(result.message);
|
||||||
else {
|
}
|
||||||
toast.error(result.message);
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="text-gray-400 hover:text-red-500 hover:bg-red-50"
|
className="text-gray-400 hover:text-red-500 hover:bg-red-50"
|
||||||
@@ -148,121 +132,21 @@ const FolderCard = ({ folder, refresh, showVisibility = true }: FolderProps) =>
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface PublicFolderCardProps {
|
|
||||||
folder: TPublicFolder;
|
|
||||||
currentUserId?: string;
|
|
||||||
onFavoriteChange?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PublicFolderCard = ({ folder, currentUserId, onFavoriteChange }: PublicFolderCardProps) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const t = useTranslations("folders");
|
|
||||||
const [isFavorited, setIsFavorited] = useState(false);
|
|
||||||
const [favoriteCount, setFavoriteCount] = useState(folder.favoriteCount);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentUserId) {
|
|
||||||
actionCheckFavorite(folder.id).then(result => {
|
|
||||||
if (result.success && result.data) {
|
|
||||||
setIsFavorited(result.data.isFavorited);
|
|
||||||
setFavoriteCount(result.data.favoriteCount);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [folder.id, currentUserId]);
|
|
||||||
|
|
||||||
const handleToggleFavorite = async (e: React.MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (!currentUserId) {
|
|
||||||
toast.error(t("pleaseLogin"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const result = await actionToggleFavorite(folder.id);
|
|
||||||
if (result.success && result.data) {
|
|
||||||
setIsFavorited(result.data.isFavorited);
|
|
||||||
setFavoriteCount(result.data.favoriteCount);
|
|
||||||
onFavoriteChange?.();
|
|
||||||
} else {
|
|
||||||
toast.error(result.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="flex justify-between items-center group py-4 px-5 border-b border-gray-200 last:border-b-0 hover:bg-gray-50 cursor-pointer transition-colors"
|
|
||||||
onClick={() => {
|
|
||||||
router.push(`/folders/${folder.id}`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-4 flex-1">
|
|
||||||
<div className="shrink-0 text-primary-500">
|
|
||||||
<Fd size={24} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<h3 className="font-semibold text-gray-900 truncate">{folder.name}</h3>
|
|
||||||
<p className="text-sm text-gray-500 mt-0.5">
|
|
||||||
{t("publicFolderInfo", {
|
|
||||||
userName: folder.userName ?? folder.userUsername ?? t("unknownUser"),
|
|
||||||
totalPairs: folder.totalPairs,
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="flex items-center gap-1 text-sm text-gray-400">
|
|
||||||
<Heart
|
|
||||||
size={14}
|
|
||||||
className={isFavorited ? "fill-red-500 text-red-500" : ""}
|
|
||||||
/>
|
|
||||||
<span>{favoriteCount}</span>
|
|
||||||
</div>
|
|
||||||
<CircleButton
|
|
||||||
onClick={handleToggleFavorite}
|
|
||||||
title={isFavorited ? t("unfavorite") : t("favorite")}
|
|
||||||
>
|
|
||||||
<Heart
|
|
||||||
size={18}
|
|
||||||
className={isFavorited ? "fill-red-500 text-red-500" : ""}
|
|
||||||
/>
|
|
||||||
</CircleButton>
|
|
||||||
<ChevronRight size={20} className="text-gray-400" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface FoldersClientProps {
|
interface FoldersClientProps {
|
||||||
userId: string | null;
|
userId: string;
|
||||||
initialPublicFolders: TPublicFolder[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FoldersClient({ userId, initialPublicFolders }: FoldersClientProps) {
|
export function FoldersClient({ userId }: FoldersClientProps) {
|
||||||
const t = useTranslations("folders");
|
const t = useTranslations("folders");
|
||||||
|
const router = useRouter();
|
||||||
const [folders, setFolders] = useState<TSharedFolderWithTotalPairs[]>([]);
|
const [folders, setFolders] = useState<TSharedFolderWithTotalPairs[]>([]);
|
||||||
const [publicFolders, setPublicFolders] = useState<TPublicFolder[]>(initialPublicFolders);
|
const [loading, setLoading] = useState(true);
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [activeTab, setActiveTab] = useState<TabType>(userId ? "my" : "public");
|
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userId) {
|
loadFolders();
|
||||||
setLoading(true);
|
|
||||||
actionGetFoldersWithTotalPairsByUserId(userId)
|
|
||||||
.then((result) => {
|
|
||||||
if (result.success && result.data) {
|
|
||||||
setFolders(result.data);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [userId]);
|
}, [userId]);
|
||||||
|
|
||||||
const updateFolders = async () => {
|
const loadFolders = async () => {
|
||||||
if (!userId) return;
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const result = await actionGetFoldersWithTotalPairsByUserId(userId);
|
const result = await actionGetFoldersWithTotalPairsByUserId(userId);
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
@@ -271,28 +155,14 @@ export function FoldersClient({ userId, initialPublicFolders }: FoldersClientPro
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearch = async () => {
|
|
||||||
if (!searchQuery.trim()) {
|
|
||||||
setPublicFolders(initialPublicFolders);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setLoading(true);
|
|
||||||
const result = await actionSearchPublicFolders(searchQuery.trim());
|
|
||||||
if (result.success && result.data) {
|
|
||||||
setPublicFolders(result.data);
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateFolder = async () => {
|
const handleCreateFolder = async () => {
|
||||||
if (!userId) return;
|
|
||||||
const folderName = prompt(t("enterFolderName"));
|
const folderName = prompt(t("enterFolderName"));
|
||||||
if (!folderName) return;
|
if (!folderName) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const result = await actionCreateFolder(userId, folderName);
|
const result = await actionCreateFolder(userId, folderName);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
updateFolders();
|
loadFolders();
|
||||||
} else {
|
} else {
|
||||||
toast.error(result.message);
|
toast.error(result.message);
|
||||||
}
|
}
|
||||||
@@ -305,109 +175,36 @@ export function FoldersClient({ userId, initialPublicFolders }: FoldersClientPro
|
|||||||
<PageLayout>
|
<PageLayout>
|
||||||
<PageHeader title={t("title")} subtitle={t("subtitle")} />
|
<PageHeader title={t("title")} subtitle={t("subtitle")} />
|
||||||
|
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<LightButton
|
||||||
{userId && (
|
onClick={handleCreateFolder}
|
||||||
<button
|
disabled={loading}
|
||||||
onClick={() => setActiveTab("my")}
|
className="w-full border-dashed mb-4"
|
||||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
>
|
||||||
activeTab === "my"
|
<FolderPlus size={20} />
|
||||||
? "bg-primary-500 text-white"
|
<span>{loading ? t("creating") : t("newFolder")}</span>
|
||||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
</LightButton>
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{t("myFolders")}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab("public")}
|
|
||||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
||||||
activeTab === "public"
|
|
||||||
? "bg-primary-500 text-white"
|
|
||||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{t("publicFolders")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{activeTab === "public" && (
|
<CardList>
|
||||||
<div className="flex items-center gap-2 mb-4">
|
{loading ? (
|
||||||
<div className="relative flex-1">
|
<div className="p-8 text-center">
|
||||||
<Search size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
<div className="w-8 h-8 border-2 border-gray-200 border-t-gray-400 rounded-full animate-spin mx-auto mb-3"></div>
|
||||||
<input
|
<p className="text-sm text-gray-500">{t("loading")}</p>
|
||||||
type="text"
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
|
|
||||||
placeholder={t("searchPlaceholder")}
|
|
||||||
className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<CircleButton onClick={handleSearch}>
|
) : folders.length === 0 ? (
|
||||||
<Search size={18} />
|
<div className="text-center py-12 text-gray-400">
|
||||||
</CircleButton>
|
<div className="w-16 h-16 mx-auto mb-3 rounded-full bg-gray-100 flex items-center justify-center">
|
||||||
</div>
|
<FolderPlus size={24} className="text-gray-400" />
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === "my" && userId && (
|
|
||||||
<LightButton
|
|
||||||
onClick={handleCreateFolder}
|
|
||||||
disabled={loading}
|
|
||||||
className="w-full border-dashed"
|
|
||||||
>
|
|
||||||
<FolderPlus size={20} />
|
|
||||||
<span>{loading ? t("creating") : t("newFolder")}</span>
|
|
||||||
</LightButton>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-4">
|
|
||||||
<CardList>
|
|
||||||
{loading ? (
|
|
||||||
<div className="p-8 text-center">
|
|
||||||
<div className="w-8 h-8 border-2 border-gray-200 border-t-gray-400 rounded-full animate-spin mx-auto mb-3"></div>
|
|
||||||
<p className="text-sm text-gray-500">{t("loading")}</p>
|
|
||||||
</div>
|
</div>
|
||||||
) : activeTab === "my" && userId ? (
|
<p className="text-sm">{t("noFoldersYet")}</p>
|
||||||
folders.length === 0 ? (
|
</div>
|
||||||
<div className="text-center py-12 text-gray-400">
|
) : (
|
||||||
<div className="w-16 h-16 mx-auto mb-3 rounded-full bg-gray-100 flex items-center justify-center">
|
folders
|
||||||
<FolderPlus size={24} className="text-gray-400" />
|
.toSorted((a, b) => b.id - a.id)
|
||||||
</div>
|
.map((folder) => (
|
||||||
<p className="text-sm">{t("noFoldersYet")}</p>
|
<FolderCard key={folder.id} folder={folder} refresh={loadFolders} />
|
||||||
</div>
|
))
|
||||||
) : (
|
)}
|
||||||
folders
|
</CardList>
|
||||||
.toSorted((a, b) => a.id - b.id)
|
|
||||||
.map((folder) => (
|
|
||||||
<FolderCard
|
|
||||||
key={folder.id}
|
|
||||||
folder={folder}
|
|
||||||
refresh={updateFolders}
|
|
||||||
showVisibility={true}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
publicFolders.length === 0 ? (
|
|
||||||
<div className="text-center py-12 text-gray-400">
|
|
||||||
<div className="w-16 h-16 mx-auto mb-3 rounded-full bg-gray-100 flex items-center justify-center">
|
|
||||||
<Fd size={24} className="text-gray-400" />
|
|
||||||
</div>
|
|
||||||
<p className="text-sm">{t("noPublicFolders")}</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
publicFolders.map((folder) => (
|
|
||||||
<PublicFolderCard
|
|
||||||
key={folder.id}
|
|
||||||
folder={folder}
|
|
||||||
currentUserId={userId ?? undefined}
|
|
||||||
onFavoriteChange={handleSearch}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</CardList>
|
|
||||||
</div>
|
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,16 @@
|
|||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import { FoldersClient } from "./FoldersClient";
|
import { FoldersClient } from "./FoldersClient";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { actionGetPublicFolders } from "@/modules/folder/folder-aciton";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
export default async function FoldersPage() {
|
export default async function FoldersPage() {
|
||||||
const session = await auth.api.getSession(
|
const session = await auth.api.getSession(
|
||||||
{ headers: await headers() }
|
{ headers: await headers() }
|
||||||
);
|
);
|
||||||
|
|
||||||
const publicFoldersResult = await actionGetPublicFolders();
|
|
||||||
const publicFolders = publicFoldersResult.success ? publicFoldersResult.data ?? [] : [];
|
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return (
|
redirect("/login?redirect=/folders");
|
||||||
<FoldersClient
|
|
||||||
userId={null}
|
|
||||||
initialPublicFolders={publicFolders}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <FoldersClient userId={session.user.id} />;
|
||||||
<FoldersClient
|
|
||||||
userId={session.user.id}
|
|
||||||
initialPublicFolders={publicFolders}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { IMAGES } from "@/config/images";
|
import { IMAGES } from "@/config/images";
|
||||||
import { Folder, Home, User } from "lucide-react";
|
import { Compass, Folder, Heart, Home, User } from "lucide-react";
|
||||||
import { LanguageSettings } from "./LanguageSettings";
|
import { LanguageSettings } from "./LanguageSettings";
|
||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
@@ -41,6 +41,22 @@ export async function Navbar() {
|
|||||||
<GhostLightButton href="/folders" className="md:hidden! block!" size="md">
|
<GhostLightButton href="/folders" className="md:hidden! block!" size="md">
|
||||||
<Folder size={20} />
|
<Folder size={20} />
|
||||||
</GhostLightButton>
|
</GhostLightButton>
|
||||||
|
<GhostLightButton href="/explore" className="md:block! hidden!" size="md">
|
||||||
|
{t("explore")}
|
||||||
|
</GhostLightButton>
|
||||||
|
<GhostLightButton href="/explore" className="md:hidden! block!" size="md">
|
||||||
|
<Compass size={20} />
|
||||||
|
</GhostLightButton>
|
||||||
|
{session && (
|
||||||
|
<>
|
||||||
|
<GhostLightButton href="/favorites" className="md:block! hidden!" size="md">
|
||||||
|
{t("favorites")}
|
||||||
|
</GhostLightButton>
|
||||||
|
<GhostLightButton href="/favorites" className="md:hidden! block!" size="md">
|
||||||
|
<Heart size={20} />
|
||||||
|
</GhostLightButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<GhostLightButton
|
<GhostLightButton
|
||||||
className="hidden! md:block!"
|
className="hidden! md:block!"
|
||||||
size="md"
|
size="md"
|
||||||
|
|||||||
16
src/lib/logger/index.ts
Normal file
16
src/lib/logger/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { logger } from "./logger";
|
||||||
|
|
||||||
|
export { logger };
|
||||||
|
|
||||||
|
export function createLogger(context: string) {
|
||||||
|
return {
|
||||||
|
debug: (message: string, meta?: object) =>
|
||||||
|
logger.debug(`[${context}] ${message}`, meta),
|
||||||
|
info: (message: string, meta?: object) =>
|
||||||
|
logger.info(`[${context}] ${message}`, meta),
|
||||||
|
warn: (message: string, meta?: object) =>
|
||||||
|
logger.warn(`[${context}] ${message}`, meta),
|
||||||
|
error: (message: string, meta?: object) =>
|
||||||
|
logger.error(`[${context}] ${message}`, meta),
|
||||||
|
};
|
||||||
|
}
|
||||||
9
src/lib/logger/logger.ts
Normal file
9
src/lib/logger/logger.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import winston from "winston";
|
||||||
|
import { devTransport, prodTransport } from "./transports";
|
||||||
|
|
||||||
|
const isDev = process.env.NODE_ENV !== "production";
|
||||||
|
|
||||||
|
export const logger = winston.createLogger({
|
||||||
|
level: isDev ? "debug" : "info",
|
||||||
|
transports: [isDev ? devTransport : prodTransport],
|
||||||
|
});
|
||||||
20
src/lib/logger/transports.ts
Normal file
20
src/lib/logger/transports.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { transports, format } from "winston";
|
||||||
|
|
||||||
|
const { combine, timestamp, printf, colorize, json } = format;
|
||||||
|
|
||||||
|
const customFormat = printf(({ level, message, timestamp, ...metadata }) => {
|
||||||
|
const metaStr = Object.keys(metadata).length ? JSON.stringify(metadata) : "";
|
||||||
|
return `${timestamp} [${level}]: ${message} ${metaStr}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const devTransport = new transports.Console({
|
||||||
|
format: combine(
|
||||||
|
colorize(),
|
||||||
|
timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||||
|
customFormat
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const prodTransport = new transports.Console({
|
||||||
|
format: combine(timestamp(), json()),
|
||||||
|
});
|
||||||
@@ -3,8 +3,39 @@
|
|||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { ValidateError } from "@/lib/errors";
|
import { ValidateError } from "@/lib/errors";
|
||||||
import { ActionInputCreatePair, ActionInputUpdatePairById, ActionOutputGetFoldersWithTotalPairsByUserId, ActionOutputGetPublicFolders, ActionOutputSetFolderVisibility, ActionOutputToggleFavorite, ActionOutputCheckFavorite, validateActionInputCreatePair, validateActionInputUpdatePairById } from "./folder-action-dto";
|
import {
|
||||||
import { repoCreateFolder, repoCreatePair, repoDeleteFolderById, repoDeletePairById, repoGetFolderIdByPairId, repoGetFolderVisibility, repoGetFoldersByUserId, repoGetFoldersWithTotalPairsByUserId, repoGetPairsByFolderId, repoGetPublicFolders, repoGetUserIdByFolderId, repoRenameFolderById, repoSearchPublicFolders, repoUpdateFolderVisibility, repoUpdatePairById, repoToggleFavorite, repoCheckFavorite } from "./folder-repository";
|
ActionInputCreatePair,
|
||||||
|
ActionInputUpdatePairById,
|
||||||
|
ActionOutputGetFoldersWithTotalPairsByUserId,
|
||||||
|
ActionOutputGetPublicFolders,
|
||||||
|
ActionOutputSetFolderVisibility,
|
||||||
|
ActionOutputToggleFavorite,
|
||||||
|
ActionOutputCheckFavorite,
|
||||||
|
ActionOutputGetUserFavorites,
|
||||||
|
ActionOutputUserFavorite,
|
||||||
|
validateActionInputCreatePair,
|
||||||
|
validateActionInputUpdatePairById,
|
||||||
|
} from "./folder-action-dto";
|
||||||
|
import {
|
||||||
|
repoCreateFolder,
|
||||||
|
repoCreatePair,
|
||||||
|
repoDeleteFolderById,
|
||||||
|
repoDeletePairById,
|
||||||
|
repoGetFolderIdByPairId,
|
||||||
|
repoGetFolderVisibility,
|
||||||
|
repoGetFoldersByUserId,
|
||||||
|
repoGetFoldersWithTotalPairsByUserId,
|
||||||
|
repoGetPairsByFolderId,
|
||||||
|
repoGetPublicFolders,
|
||||||
|
repoGetUserIdByFolderId,
|
||||||
|
repoRenameFolderById,
|
||||||
|
repoSearchPublicFolders,
|
||||||
|
repoUpdateFolderVisibility,
|
||||||
|
repoUpdatePairById,
|
||||||
|
repoToggleFavorite,
|
||||||
|
repoCheckFavorite,
|
||||||
|
repoGetUserFavorites,
|
||||||
|
} from "./folder-repository";
|
||||||
import { validate } from "@/utils/validate";
|
import { validate } from "@/utils/validate";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { LENGTH_MAX_FOLDER_NAME, LENGTH_MIN_FOLDER_NAME } from "@/shared/constant";
|
import { LENGTH_MAX_FOLDER_NAME, LENGTH_MIN_FOLDER_NAME } from "@/shared/constant";
|
||||||
@@ -425,3 +456,41 @@ export async function actionCheckFavorite(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function actionGetUserFavorites(): Promise<ActionOutputGetUserFavorites> {
|
||||||
|
try {
|
||||||
|
const session = await auth.api.getSession({ headers: await headers() });
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Unauthorized',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const favorites = await repoGetUserFavorites({
|
||||||
|
userId: session.user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'success',
|
||||||
|
data: favorites.map((fav) => ({
|
||||||
|
id: fav.id,
|
||||||
|
folderId: fav.folderId,
|
||||||
|
folderName: fav.folderName,
|
||||||
|
folderCreatedAt: fav.folderCreatedAt,
|
||||||
|
folderTotalPairs: fav.folderTotalPairs,
|
||||||
|
folderOwnerId: fav.folderOwnerId,
|
||||||
|
folderOwnerName: fav.folderOwnerName,
|
||||||
|
folderOwnerUsername: fav.folderOwnerUsername,
|
||||||
|
favoritedAt: fav.favoritedAt,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Unknown error occured.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -84,3 +84,21 @@ export type ActionOutputCheckFavorite = {
|
|||||||
favoriteCount: number;
|
favoriteCount: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ActionOutputUserFavorite = {
|
||||||
|
id: number;
|
||||||
|
folderId: number;
|
||||||
|
folderName: string;
|
||||||
|
folderCreatedAt: Date;
|
||||||
|
folderTotalPairs: number;
|
||||||
|
folderOwnerId: string;
|
||||||
|
folderOwnerName: string | null;
|
||||||
|
folderOwnerUsername: string | null;
|
||||||
|
favoritedAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ActionOutputGetUserFavorites = {
|
||||||
|
message: string;
|
||||||
|
success: boolean;
|
||||||
|
data?: ActionOutputUserFavorite[];
|
||||||
|
};
|
||||||
|
|||||||
@@ -71,3 +71,21 @@ export type RepoOutputFavoriteStatus = {
|
|||||||
isFavorited: boolean;
|
isFavorited: boolean;
|
||||||
favoriteCount: number;
|
favoriteCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface RepoInputGetUserFavorites {
|
||||||
|
userId: string;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RepoOutputUserFavorite = {
|
||||||
|
id: number;
|
||||||
|
folderId: number;
|
||||||
|
folderName: string;
|
||||||
|
folderCreatedAt: Date;
|
||||||
|
folderTotalPairs: number;
|
||||||
|
folderOwnerId: string;
|
||||||
|
folderOwnerName: string | null;
|
||||||
|
folderOwnerUsername: string | null;
|
||||||
|
favoritedAt: Date;
|
||||||
|
};
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
RepoInputToggleFavorite,
|
RepoInputToggleFavorite,
|
||||||
RepoInputCheckFavorite,
|
RepoInputCheckFavorite,
|
||||||
RepoOutputFavoriteStatus,
|
RepoOutputFavoriteStatus,
|
||||||
|
RepoInputGetUserFavorites,
|
||||||
|
RepoOutputUserFavorite,
|
||||||
} from "./folder-repository-dto";
|
} from "./folder-repository-dto";
|
||||||
import { Visibility } from "../../../generated/prisma/enums";
|
import { Visibility } from "../../../generated/prisma/enums";
|
||||||
|
|
||||||
@@ -272,3 +274,34 @@ export async function repoCheckFavorite(
|
|||||||
favoriteCount: count,
|
favoriteCount: count,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function repoGetUserFavorites(input: RepoInputGetUserFavorites) {
|
||||||
|
const { userId, limit = 50, offset = 0 } = input;
|
||||||
|
|
||||||
|
const favorites = await prisma.folderFavorite.findMany({
|
||||||
|
where: { userId },
|
||||||
|
include: {
|
||||||
|
folder: {
|
||||||
|
include: {
|
||||||
|
_count: { select: { pairs: true } },
|
||||||
|
user: { select: { name: true, username: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
take: limit,
|
||||||
|
skip: offset,
|
||||||
|
});
|
||||||
|
|
||||||
|
return favorites.map((fav) => ({
|
||||||
|
id: fav.id,
|
||||||
|
folderId: fav.folderId,
|
||||||
|
folderName: fav.folder.name,
|
||||||
|
folderCreatedAt: fav.folder.createdAt,
|
||||||
|
folderTotalPairs: fav.folder._count.pairs,
|
||||||
|
folderOwnerId: fav.folder.userId,
|
||||||
|
folderOwnerName: fav.folder.user.name,
|
||||||
|
folderOwnerUsername: fav.folder.user.username,
|
||||||
|
favoritedAt: fav.createdAt,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user