Compare commits
2 Commits
6389135156
...
3db1b3716f
| Author | SHA1 | Date | |
|---|---|---|---|
| 3db1b3716f | |||
| b30f9fb0c3 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -42,4 +42,6 @@ next-env.d.ts
|
|||||||
|
|
||||||
.env
|
.env
|
||||||
|
|
||||||
build.sh
|
build.sh
|
||||||
|
|
||||||
|
test.ts
|
||||||
@@ -3,7 +3,17 @@ import createNextIntlPlugin from "next-intl/plugin";
|
|||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
/* config options here */
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
allowedDevOrigins: ["192.168.3.65", "192.168.3.66"],
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "avatars.githubusercontent.com",
|
||||||
|
port: "",
|
||||||
|
pathname: "/u/**",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// allowedDevOrigins: ["192.168.3.65", "192.168.3.66"],
|
||||||
};
|
};
|
||||||
|
|
||||||
const withNextIntl = createNextIntlPlugin();
|
const withNextIntl = createNextIntlPlugin();
|
||||||
|
|||||||
564
package-lock.json
generated
564
package-lock.json
generated
@@ -9,17 +9,23 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"edge-tts-universal": "^1.3.2",
|
"edge-tts-universal": "^1.3.2",
|
||||||
"next": "15.5.3",
|
"next": "15.5.3",
|
||||||
|
"next-auth": "^4.24.13",
|
||||||
"next-intl": "^4.4.0",
|
"next-intl": "^4.4.0",
|
||||||
|
"pg": "^8.16.3",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
|
"unstorage": "^1.17.2",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
|
"@types/pg": "^8.15.6",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
@@ -1721,8 +1727,6 @@
|
|||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@babel/runtime/-/runtime-7.28.4.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||||
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
@@ -3521,6 +3525,15 @@
|
|||||||
"node": ">=12.4.0"
|
"node": ">=12.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@panva/hkdf": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/@panva/hkdf/-/hkdf-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@peculiar/asn1-schema": {
|
"node_modules/@peculiar/asn1-schema": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@peculiar/asn1-schema/-/asn1-schema-2.5.0.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/@peculiar/asn1-schema/-/asn1-schema-2.5.0.tgz",
|
||||||
@@ -4282,6 +4295,13 @@
|
|||||||
"@babel/types": "^7.28.2"
|
"@babel/types": "^7.28.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/bcryptjs": {
|
||||||
|
"version": "2.4.6",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
|
||||||
|
"integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/@types/estree/-/estree-1.0.8.tgz",
|
||||||
@@ -4354,6 +4374,18 @@
|
|||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/pg": {
|
||||||
|
"version": "8.15.6",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/@types/pg/-/pg-8.15.6.tgz",
|
||||||
|
"integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*",
|
||||||
|
"pg-protocol": "*",
|
||||||
|
"pg-types": "^2.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.2.2",
|
"version": "19.2.2",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@types/react/-/react-19.2.2.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/@types/react/-/react-19.2.2.tgz",
|
||||||
@@ -5198,8 +5230,6 @@
|
|||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/anymatch/-/anymatch-3.1.3.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/anymatch/-/anymatch-3.1.3.tgz",
|
||||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"normalize-path": "^3.0.0",
|
"normalize-path": "^3.0.0",
|
||||||
"picomatch": "^2.0.4"
|
"picomatch": "^2.0.4"
|
||||||
@@ -5799,6 +5829,15 @@
|
|||||||
"baseline-browser-mapping": "dist/cli.js"
|
"baseline-browser-mapping": "dist/cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bcryptjs": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"bin": {
|
||||||
|
"bcrypt": "bin/bcrypt"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/better-opn": {
|
"node_modules/better-opn": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/better-opn/-/better-opn-3.0.2.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/better-opn/-/better-opn-3.0.2.tgz",
|
||||||
@@ -6093,6 +6132,21 @@
|
|||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chokidar": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"readdirp": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.16.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chrome-launcher": {
|
"node_modules/chrome-launcher": {
|
||||||
"version": "0.15.2",
|
"version": "0.15.2",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/chrome-launcher/-/chrome-launcher-0.15.2.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/chrome-launcher/-/chrome-launcher-0.15.2.tgz",
|
||||||
@@ -6430,6 +6484,21 @@
|
|||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/cookie/-/cookie-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cookie-es": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/cookie-es/-/cookie-es-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/core-js-compat": {
|
"node_modules/core-js-compat": {
|
||||||
"version": "3.46.0",
|
"version": "3.46.0",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/core-js-compat/-/core-js-compat-3.46.0.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/core-js-compat/-/core-js-compat-3.46.0.tgz",
|
||||||
@@ -6469,6 +6538,15 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crossws": {
|
||||||
|
"version": "0.3.5",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/crossws/-/crossws-0.3.5.tgz",
|
||||||
|
"integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"uncrypto": "^0.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/crypto-random-string": {
|
"node_modules/crypto-random-string": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
|
||||||
@@ -6661,6 +6739,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/defu": {
|
||||||
|
"version": "6.1.4",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/defu/-/defu-6.1.4.tgz",
|
||||||
|
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/delayed-stream": {
|
"node_modules/delayed-stream": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
@@ -6681,6 +6765,12 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/destr": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/destr/-/destr-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/destroy": {
|
"node_modules/destroy": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/destroy/-/destroy-1.2.0.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/destroy/-/destroy-1.2.0.tgz",
|
||||||
@@ -8214,6 +8304,23 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/h3": {
|
||||||
|
"version": "1.15.4",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/h3/-/h3-1.15.4.tgz",
|
||||||
|
"integrity": "sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cookie-es": "^1.2.2",
|
||||||
|
"crossws": "^0.3.5",
|
||||||
|
"defu": "^6.1.4",
|
||||||
|
"destr": "^2.0.5",
|
||||||
|
"iron-webcrypto": "^1.2.1",
|
||||||
|
"node-mock-http": "^1.0.2",
|
||||||
|
"radix3": "^1.1.2",
|
||||||
|
"ufo": "^1.6.1",
|
||||||
|
"uncrypto": "^0.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/has-bigints": {
|
"node_modules/has-bigints": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/has-bigints/-/has-bigints-1.1.0.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/has-bigints/-/has-bigints-1.1.0.tgz",
|
||||||
@@ -8537,6 +8644,15 @@
|
|||||||
"loose-envify": "^1.0.0"
|
"loose-envify": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/iron-webcrypto": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/brc-dd"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-array-buffer": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||||
@@ -10247,6 +10363,15 @@
|
|||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jose": {
|
||||||
|
"version": "4.15.9",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/jose/-/jose-4.15.9.tgz",
|
||||||
|
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -11548,6 +11673,47 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/next-auth": {
|
||||||
|
"version": "4.24.13",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/next-auth/-/next-auth-4.24.13.tgz",
|
||||||
|
"integrity": "sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.20.13",
|
||||||
|
"@panva/hkdf": "^1.0.2",
|
||||||
|
"cookie": "^0.7.0",
|
||||||
|
"jose": "^4.15.5",
|
||||||
|
"oauth": "^0.9.15",
|
||||||
|
"openid-client": "^5.4.0",
|
||||||
|
"preact": "^10.6.3",
|
||||||
|
"preact-render-to-string": "^5.1.19",
|
||||||
|
"uuid": "^8.3.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@auth/core": "0.34.3",
|
||||||
|
"next": "^12.2.5 || ^13 || ^14 || ^15 || ^16",
|
||||||
|
"nodemailer": "^7.0.7",
|
||||||
|
"react": "^17.0.2 || ^18 || ^19",
|
||||||
|
"react-dom": "^17.0.2 || ^18 || ^19"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@auth/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"nodemailer": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/next-auth/node_modules/uuid": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/next-intl": {
|
"node_modules/next-intl": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/next-intl/-/next-intl-4.4.0.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/next-intl/-/next-intl-4.4.0.tgz",
|
||||||
@@ -11623,6 +11789,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-fetch-native": {
|
||||||
|
"version": "1.6.7",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
|
||||||
|
"integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/node-forge": {
|
"node_modules/node-forge": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/node-forge/-/node-forge-1.3.1.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/node-forge/-/node-forge-1.3.1.tgz",
|
||||||
@@ -11642,6 +11814,12 @@
|
|||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/node-mock-http": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/node-mock-http/-/node-mock-http-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-jN8dK25fsfnMrVsEhluUTPkBFY+6ybu7jSB1n+ri/vOGjJxU8J9CZhpSGkHXSkFjtUhbmoncG/YG9ta5Ludqog==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.27",
|
"version": "2.0.27",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/node-releases/-/node-releases-2.0.27.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/node-releases/-/node-releases-2.0.27.tgz",
|
||||||
@@ -11655,8 +11833,6 @@
|
|||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/normalize-path/-/normalize-path-3.0.0.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -11686,6 +11862,12 @@
|
|||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/oauth": {
|
||||||
|
"version": "0.9.15",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/oauth/-/oauth-0.9.15.tgz",
|
||||||
|
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ob1": {
|
"node_modules/ob1": {
|
||||||
"version": "0.83.2",
|
"version": "0.83.2",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/ob1/-/ob1-0.83.2.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/ob1/-/ob1-0.83.2.tgz",
|
||||||
@@ -11710,6 +11892,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/object-hash": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/object-hash/-/object-hash-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-inspect": {
|
"node_modules/object-inspect": {
|
||||||
"version": "1.13.4",
|
"version": "1.13.4",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/object-inspect/-/object-inspect-1.13.4.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
@@ -11823,6 +12014,26 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ofetch": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/ofetch/-/ofetch-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"destr": "^2.0.5",
|
||||||
|
"node-fetch-native": "^1.6.7",
|
||||||
|
"ufo": "^1.6.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/oidc-token-hash": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^10.13.0 || >=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/on-finished": {
|
"node_modules/on-finished": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/on-finished/-/on-finished-2.3.0.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/on-finished/-/on-finished-2.3.0.tgz",
|
||||||
@@ -11891,6 +12102,39 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/openid-client": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/openid-client/-/openid-client-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jose": "^4.15.9",
|
||||||
|
"lru-cache": "^6.0.0",
|
||||||
|
"object-hash": "^2.2.0",
|
||||||
|
"oidc-token-hash": "^5.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/openid-client/node_modules/lru-cache": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"yallist": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/openid-client/node_modules/yallist": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/yallist/-/yallist-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/optionator/-/optionator-0.9.4.tgz",
|
||||||
@@ -12209,6 +12453,95 @@
|
|||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/pg": {
|
||||||
|
"version": "8.16.3",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/pg/-/pg-8.16.3.tgz",
|
||||||
|
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pg-connection-string": "^2.9.1",
|
||||||
|
"pg-pool": "^3.10.1",
|
||||||
|
"pg-protocol": "^1.10.3",
|
||||||
|
"pg-types": "2.2.0",
|
||||||
|
"pgpass": "1.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"pg-cloudflare": "^1.2.7"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"pg-native": ">=3.0.1"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"pg-native": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pg-cloudflare": {
|
||||||
|
"version": "1.2.7",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz",
|
||||||
|
"integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/pg-connection-string": {
|
||||||
|
"version": "2.9.1",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
|
||||||
|
"integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/pg-int8": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pg-pool": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/pg-pool/-/pg-pool-3.10.1.tgz",
|
||||||
|
"integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"pg": ">=8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pg-protocol": {
|
||||||
|
"version": "1.10.3",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/pg-protocol/-/pg-protocol-1.10.3.tgz",
|
||||||
|
"integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/pg-types": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/pg-types/-/pg-types-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pg-int8": "1.0.1",
|
||||||
|
"postgres-array": "~2.0.0",
|
||||||
|
"postgres-bytea": "~1.0.0",
|
||||||
|
"postgres-date": "~1.0.4",
|
||||||
|
"postgres-interval": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pgpass": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/pgpass/-/pgpass-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"split2": "^4.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -12219,7 +12552,6 @@
|
|||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/picomatch/-/picomatch-2.3.1.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/picomatch/-/picomatch-2.3.1.tgz",
|
||||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
@@ -12305,6 +12637,73 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/postgres-array": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postgres-bytea": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postgres-date": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/postgres-date/-/postgres-date-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postgres-interval": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"xtend": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/preact": {
|
||||||
|
"version": "10.27.2",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/preact/-/preact-10.27.2.tgz",
|
||||||
|
"integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/preact"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/preact-render-to-string": {
|
||||||
|
"version": "5.2.6",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz",
|
||||||
|
"integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pretty-format": "^3.8.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"preact": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/preact-render-to-string/node_modules/pretty-format": {
|
||||||
|
"version": "3.8.0",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/pretty-format/-/pretty-format-3.8.0.tgz",
|
||||||
|
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
@@ -12503,6 +12902,12 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/radix3": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/radix3/-/radix3-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/range-parser": {
|
"node_modules/range-parser": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/range-parser/-/range-parser-1.2.1.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/range-parser/-/range-parser-1.2.1.tgz",
|
||||||
@@ -12616,6 +13021,19 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/readdirp": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.18.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/reflect.getprototypeof": {
|
"node_modules/reflect.getprototypeof": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
||||||
@@ -13568,6 +13986,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/split2": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/split2/-/split2-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sprintf-js": {
|
"node_modules/sprintf-js": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||||
@@ -14469,6 +14896,12 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ufo": {
|
||||||
|
"version": "1.6.1",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/ufo/-/ufo-1.6.1.tgz",
|
||||||
|
"integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/unbox-primitive": {
|
"node_modules/unbox-primitive": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
||||||
@@ -14488,6 +14921,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uncrypto": {
|
||||||
|
"version": "0.1.3",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/uncrypto/-/uncrypto-0.1.3.tgz",
|
||||||
|
"integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/undici": {
|
"node_modules/undici": {
|
||||||
"version": "6.22.0",
|
"version": "6.22.0",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/undici/-/undici-6.22.0.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/undici/-/undici-6.22.0.tgz",
|
||||||
@@ -14624,6 +15063,108 @@
|
|||||||
"@unrs/resolver-binding-win32-x64-msvc": "1.11.1"
|
"@unrs/resolver-binding-win32-x64-msvc": "1.11.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/unstorage": {
|
||||||
|
"version": "1.17.2",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/unstorage/-/unstorage-1.17.2.tgz",
|
||||||
|
"integrity": "sha512-cKEsD6iBWJgOMJ6vW1ID/SYuqNf8oN4yqRk8OYqaVQ3nnkJXOT1PSpaMh2QfzLs78UN5kSNRD2c/mgjT8tX7+w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"anymatch": "^3.1.3",
|
||||||
|
"chokidar": "^4.0.3",
|
||||||
|
"destr": "^2.0.5",
|
||||||
|
"h3": "^1.15.4",
|
||||||
|
"lru-cache": "^10.4.3",
|
||||||
|
"node-fetch-native": "^1.6.7",
|
||||||
|
"ofetch": "^1.5.0",
|
||||||
|
"ufo": "^1.6.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@azure/app-configuration": "^1.8.0",
|
||||||
|
"@azure/cosmos": "^4.2.0",
|
||||||
|
"@azure/data-tables": "^13.3.0",
|
||||||
|
"@azure/identity": "^4.6.0",
|
||||||
|
"@azure/keyvault-secrets": "^4.9.0",
|
||||||
|
"@azure/storage-blob": "^12.26.0",
|
||||||
|
"@capacitor/preferences": "^6.0.3 || ^7.0.0",
|
||||||
|
"@deno/kv": ">=0.9.0",
|
||||||
|
"@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0",
|
||||||
|
"@planetscale/database": "^1.19.0",
|
||||||
|
"@upstash/redis": "^1.34.3",
|
||||||
|
"@vercel/blob": ">=0.27.1",
|
||||||
|
"@vercel/functions": "^2.2.12 || ^3.0.0",
|
||||||
|
"@vercel/kv": "^1.0.1",
|
||||||
|
"aws4fetch": "^1.0.20",
|
||||||
|
"db0": ">=0.2.1",
|
||||||
|
"idb-keyval": "^6.2.1",
|
||||||
|
"ioredis": "^5.4.2",
|
||||||
|
"uploadthing": "^7.4.4"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@azure/app-configuration": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@azure/cosmos": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@azure/data-tables": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@azure/identity": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@azure/keyvault-secrets": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@azure/storage-blob": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@capacitor/preferences": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@deno/kv": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@netlify/blobs": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@planetscale/database": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@upstash/redis": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vercel/blob": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vercel/functions": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vercel/kv": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"aws4fetch": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"db0": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"idb-keyval": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"ioredis": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"uploadthing": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unstorage/node_modules/lru-cache": {
|
||||||
|
"version": "10.4.3",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||||
|
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
|
||||||
@@ -15172,6 +15713,15 @@
|
|||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xtend": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/y18n": {
|
"node_modules/y18n": {
|
||||||
"version": "5.0.8",
|
"version": "5.0.8",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/y18n/-/y18n-5.0.8.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
|||||||
@@ -10,17 +10,23 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"edge-tts-universal": "^1.3.2",
|
"edge-tts-universal": "^1.3.2",
|
||||||
"next": "15.5.3",
|
"next": "15.5.3",
|
||||||
|
"next-auth": "^4.24.13",
|
||||||
"next-intl": "^4.4.0",
|
"next-intl": "^4.4.0",
|
||||||
|
"pg": "^8.16.3",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
|
"unstorage": "^1.17.2",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
|
"@types/pg": "^8.15.6",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
2025.11.10 重构了translator,将其改为并发请求多个数据,速度大大提升
|
||||||
2025.10.31 添加国际化支持
|
2025.10.31 添加国际化支持
|
||||||
2025.10.30 添加背单词功能
|
2025.10.30 添加背单词功能
|
||||||
2025.10.12 添加朗读器本地保存功能
|
2025.10.12 添加朗读器本地保存功能
|
||||||
@@ -8,4 +9,3 @@
|
|||||||
2025.10.05 新增IPA生成与文本朗读功能
|
2025.10.05 新增IPA生成与文本朗读功能
|
||||||
2025.09.25 优化了主界面UI
|
2025.09.25 优化了主界面UI
|
||||||
2025.09.19 更新了单词板,单词不再会重叠
|
2025.09.19 更新了单词板,单词不再会重叠
|
||||||
|
|
||||||
|
|||||||
BIN
public/images/github-mark/github-mark-white.png
Normal file
BIN
public/images/github-mark/github-mark-white.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
1
public/images/github-mark/github-mark-white.svg
Normal file
1
public/images/github-mark/github-mark-white.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 960 B |
BIN
public/images/github-mark/github-mark.png
Normal file
BIN
public/images/github-mark/github-mark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
1
public/images/github-mark/github-mark.svg
Normal file
1
public/images/github-mark/github-mark.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/></svg>
|
||||||
|
After Width: | Height: | Size: 963 B |
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"title": "LL",
|
"title": "LL",
|
||||||
"about": "About",
|
"about": "About",
|
||||||
"sourceCode": "GitHub"
|
"sourceCode": "GitHub",
|
||||||
|
"login": "Login",
|
||||||
|
"profile": "Profile"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"title": "学语言",
|
"title": "学语言",
|
||||||
"about": "关于",
|
"about": "关于",
|
||||||
"sourceCode": "源码"
|
"sourceCode": "源码",
|
||||||
|
"login": "登录",
|
||||||
|
"profile": "个人资料"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import LightButton from "@/components/buttons/LightButton";
|
|||||||
import { Letter, SupportedAlphabets } from "@/interfaces";
|
import { Letter, SupportedAlphabets } from "@/interfaces";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import MemoryCard from "./MemoryCard";
|
import MemoryCard from "./MemoryCard";
|
||||||
import { Navbar } from "@/components/Navbar";
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export default function Alphabet() {
|
export default function Alphabet() {
|
||||||
@@ -58,7 +58,6 @@ export default function Alphabet() {
|
|||||||
if (!chosenAlphabet)
|
if (!chosenAlphabet)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navbar></Navbar>
|
|
||||||
<div className="border border-gray-200 m-4 mt-4 flex flex-col justify-center items-center p-4 rounded-2xl gap-2">
|
<div className="border border-gray-200 m-4 mt-4 flex flex-col justify-center items-center p-4 rounded-2xl gap-2">
|
||||||
<span className="text-2xl md:text-3xl">{t("chooseCharacters")}</span>
|
<span className="text-2xl md:text-3xl">{t("chooseCharacters")}</span>
|
||||||
<div className="flex gap-1 flex-wrap">
|
<div className="flex gap-1 flex-wrap">
|
||||||
@@ -87,7 +86,6 @@ export default function Alphabet() {
|
|||||||
if (loadingState === "success" && alphabetData[chosenAlphabet]) {
|
if (loadingState === "success" && alphabetData[chosenAlphabet]) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navbar></Navbar>
|
|
||||||
<MemoryCard
|
<MemoryCard
|
||||||
alphabet={alphabetData[chosenAlphabet]}
|
alphabet={alphabetData[chosenAlphabet]}
|
||||||
setChosenAlphabet={setChosenAlphabet}
|
setChosenAlphabet={setChosenAlphabet}
|
||||||
|
|||||||
15
src/app/api/auth/[...nextauth]/route.ts
Normal file
15
src/app/api/auth/[...nextauth]/route.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import NextAuth, { AuthOptions } from "next-auth";
|
||||||
|
import GithubProvider from "next-auth/providers/github";
|
||||||
|
|
||||||
|
export const authOptions: AuthOptions = {
|
||||||
|
providers: [
|
||||||
|
GithubProvider({
|
||||||
|
clientId: process.env.GITHUB_CLIENT_ID!,
|
||||||
|
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const handler = NextAuth(authOptions);
|
||||||
|
|
||||||
|
export { handler as GET, handler as POST };
|
||||||
18
src/app/api/folder/[id]/route.ts
Normal file
18
src/app/api/folder/[id]/route.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { getServerSession } from "next-auth";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { authOptions } from "../../auth/[...nextauth]/route";
|
||||||
|
import { WordPairController } from "@/lib/db";
|
||||||
|
|
||||||
|
export async function GET({ params }: { params: { slug: number } }) {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (session) {
|
||||||
|
const id = params.slug;
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify(
|
||||||
|
await WordPairController.getWordPairsByFolderId(id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return new NextResponse("Unauthorized");
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/app/api/folders/route.ts
Normal file
35
src/app/api/folders/route.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { getServerSession } from "next-auth";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { authOptions } from "../auth/[...nextauth]/route";
|
||||||
|
import { FolderController } from "@/lib/db";
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (session) {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify(
|
||||||
|
await FolderController.getFoldersByOwner(session.user!.name as string),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return new NextResponse("Unauthorized");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (session) {
|
||||||
|
const body = await req.json();
|
||||||
|
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify(
|
||||||
|
await FolderController.createFolder(
|
||||||
|
body.name,
|
||||||
|
session.user!.name as string,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return new NextResponse("Unauthorized");
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/app/api/v1/ipa/route.ts
Normal file
10
src/app/api/v1/ipa/route.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { simpleGetLLMAnswer } from "@/lib/ai";
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
return await simpleGetLLMAnswer(
|
||||||
|
`请生成%s的严式国际音标(International Phonetic Alphabet),然后直接发给我。`,
|
||||||
|
req.nextUrl.searchParams,
|
||||||
|
["text"],
|
||||||
|
);
|
||||||
|
}
|
||||||
10
src/app/api/v1/locale/route.ts
Normal file
10
src/app/api/v1/locale/route.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { simpleGetLLMAnswer } from "@/lib/ai";
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
return await simpleGetLLMAnswer(
|
||||||
|
`请根据文本“%s”推断地区(locale),形如zh-CN、en-US,然后直接发给我。`,
|
||||||
|
req.nextUrl.searchParams,
|
||||||
|
["text"],
|
||||||
|
);
|
||||||
|
}
|
||||||
10
src/app/api/v1/translate/route.ts
Normal file
10
src/app/api/v1/translate/route.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { simpleGetLLMAnswer } from "@/lib/ai";
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
return await simpleGetLLMAnswer(
|
||||||
|
`请翻译%s到%s然后直接发给我。`,
|
||||||
|
req.nextUrl.searchParams,
|
||||||
|
["text", "lang"],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import type { Metadata } from "next";
|
|||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import type { Viewport } from "next";
|
import type { Viewport } from "next";
|
||||||
import { NextIntlClientProvider } from "next-intl";
|
import { NextIntlClientProvider } from "next-intl";
|
||||||
|
import SessionWrapper from "@/lib/SessionWrapper";
|
||||||
|
import { Navbar } from "@/components/Navbar";
|
||||||
|
|
||||||
export const viewport: Viewport = {
|
export const viewport: Viewport = {
|
||||||
width: "device-width",
|
width: "device-width",
|
||||||
@@ -19,12 +21,15 @@ export default async function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<SessionWrapper>
|
||||||
<body
|
<html lang="en">
|
||||||
className={`antialiased`}
|
<body className={`antialiased`}>
|
||||||
>
|
<NextIntlClientProvider>
|
||||||
<NextIntlClientProvider>{children}</NextIntlClientProvider>
|
<Navbar></Navbar>
|
||||||
</body>
|
{children}
|
||||||
</html>
|
</NextIntlClientProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
</SessionWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
42
src/app/login/page.tsx
Normal file
42
src/app/login/page.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import LightButton from "@/components/buttons/LightButton";
|
||||||
|
import { Center } from "@/components/Center";
|
||||||
|
import IMAGES from "@/config/images";
|
||||||
|
import { signIn, useSession } from "next-auth/react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
const session = useSession();
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (session.status === "authenticated") {
|
||||||
|
router.push(searchParams.get("redirect") || "/");
|
||||||
|
}
|
||||||
|
}, [session.status, router, searchParams]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Center>
|
||||||
|
{session.status === "loading" ? (
|
||||||
|
<div>Loading...</div>
|
||||||
|
) : (
|
||||||
|
<LightButton
|
||||||
|
className="flex flex-row p-2 gap-2"
|
||||||
|
onClick={() => signIn("github")}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={IMAGES.github_mark}
|
||||||
|
alt="GitHub Logo"
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
/>
|
||||||
|
<span>GitHub Login</span>
|
||||||
|
</LightButton>
|
||||||
|
)}
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import BCard from "@/components/cards/BCard";
|
|||||||
import { LOCALES } from "@/config/locales";
|
import { LOCALES } from "@/config/locales";
|
||||||
import { Dispatch, SetStateAction, useState } from "react";
|
import { Dispatch, SetStateAction, useState } from "react";
|
||||||
import { WordData } from "@/interfaces";
|
import { WordData } from "@/interfaces";
|
||||||
import NavbarCenterWrapper from "@/components/NavbarCenterWrapper";
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -39,7 +39,7 @@ export default function Choose({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavbarCenterWrapper className="bg-gray-100">
|
<div className="w-screen flex justify-center items-center">
|
||||||
<ACard className="flex flex-col">
|
<ACard className="flex flex-col">
|
||||||
<div className="overflow-y-auto flex-1 border border-gray-200 rounded-2xl p-2 grid grid-cols-4 md:grid-cols-6 md:gap-2">
|
<div className="overflow-y-auto flex-1 border border-gray-200 rounded-2xl p-2 grid grid-cols-4 md:grid-cols-6 md:gap-2">
|
||||||
{LOCALES.map((locale, index) => (
|
{LOCALES.map((locale, index) => (
|
||||||
@@ -62,6 +62,6 @@ export default function Choose({
|
|||||||
</BCard>
|
</BCard>
|
||||||
</div>
|
</div>
|
||||||
</ACard>
|
</ACard>
|
||||||
</NavbarCenterWrapper>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { ChangeEvent, Dispatch, SetStateAction, useRef, useState } from "react";
|
|||||||
import DarkButton from "@/components/buttons/DarkButton";
|
import DarkButton from "@/components/buttons/DarkButton";
|
||||||
import { WordData } from "@/interfaces";
|
import { WordData } from "@/interfaces";
|
||||||
import Choose from "./Choose";
|
import Choose from "./Choose";
|
||||||
import NavbarCenterWrapper from "@/components/NavbarCenterWrapper";
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -51,7 +51,7 @@ export default function Edit({ setPage, wordData, setWordData }: Props) {
|
|||||||
setWordData(newWordData);
|
setWordData(newWordData);
|
||||||
if (textareaRef.current)
|
if (textareaRef.current)
|
||||||
textareaRef.current.value = convertFromWordData(newWordData);
|
textareaRef.current.value = convertFromWordData(newWordData);
|
||||||
if(localStorage) {
|
if (localStorage) {
|
||||||
localStorage.setItem("wordData", JSON.stringify(newWordData));
|
localStorage.setItem("wordData", JSON.stringify(newWordData));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -60,7 +60,7 @@ export default function Edit({ setPage, wordData, setWordData }: Props) {
|
|||||||
};
|
};
|
||||||
if (editPage === "edit")
|
if (editPage === "edit")
|
||||||
return (
|
return (
|
||||||
<NavbarCenterWrapper className="bg-gray-100">
|
<div className="w-screen flex justify-center items-center">
|
||||||
<ACard className="flex flex-col">
|
<ACard className="flex flex-col">
|
||||||
<textarea
|
<textarea
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
@@ -96,7 +96,7 @@ export default function Edit({ setPage, wordData, setWordData }: Props) {
|
|||||||
</BCard>
|
</BCard>
|
||||||
</div>
|
</div>
|
||||||
</ACard>
|
</ACard>
|
||||||
</NavbarCenterWrapper>
|
</div>
|
||||||
);
|
);
|
||||||
if (editPage === "choose")
|
if (editPage === "choose")
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import BCard from "@/components/cards/BCard";
|
|||||||
import { WordData, WordDataSchema } from "@/interfaces";
|
import { WordData, WordDataSchema } from "@/interfaces";
|
||||||
import { Dispatch, SetStateAction } from "react";
|
import { Dispatch, SetStateAction } from "react";
|
||||||
import useFileUpload from "@/hooks/useFileUpload";
|
import useFileUpload from "@/hooks/useFileUpload";
|
||||||
import NavbarCenterWrapper from "@/components/NavbarCenterWrapper";
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -43,7 +42,7 @@ export default function Main({
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<NavbarCenterWrapper className="bg-gray-100">
|
<div className="w-screen flex justify-center items-center">
|
||||||
<ACard className="flex-col flex">
|
<ACard className="flex-col flex">
|
||||||
<h1 className="text-center font-extrabold text-4xl text-gray-800 m-2 mb-4">
|
<h1 className="text-center font-extrabold text-4xl text-gray-800 m-2 mb-4">
|
||||||
{t("title")}
|
{t("title")}
|
||||||
@@ -69,6 +68,6 @@ export default function Main({
|
|||||||
</div>
|
</div>
|
||||||
</ACard>
|
</ACard>
|
||||||
<input type="file" hidden ref={inputRef}></input>
|
<input type="file" hidden ref={inputRef}></input>
|
||||||
</NavbarCenterWrapper>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Dispatch, SetStateAction, useState } from "react";
|
|||||||
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
||||||
import { getTTSAudioUrl } from "@/utils";
|
import { getTTSAudioUrl } from "@/utils";
|
||||||
import { VOICES } from "@/config/locales";
|
import { VOICES } from "@/config/locales";
|
||||||
import NavbarCenterWrapper from "@/components/NavbarCenterWrapper";
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
interface WordBoardProps {
|
interface WordBoardProps {
|
||||||
@@ -49,7 +49,7 @@ export default function Start({ wordData, setPage }: Props) {
|
|||||||
).then(play);
|
).then(play);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<NavbarCenterWrapper className="bg-gray-100">
|
<div className="w-screen flex justify-center items-center">
|
||||||
<div className="flex-col flex items-center h-96">
|
<div className="flex-col flex items-center h-96">
|
||||||
<div className="flex-1 w-[95dvw] md:w-fit p-4 gap-4 flex flex-col overflow-x-auto">
|
<div className="flex-1 w-[95dvw] md:w-fit p-4 gap-4 flex flex-col overflow-x-auto">
|
||||||
{dictation ? (
|
{dictation ? (
|
||||||
@@ -95,6 +95,6 @@ export default function Start({ wordData, setPage }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</NavbarCenterWrapper>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const getLocalWordData = (): WordData => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Memorize() {
|
export default function MemorizePage() {
|
||||||
const [page, setPage] = useState<"start" | "main" | "edit">("main");
|
const [page, setPage] = useState<"start" | "main" | "edit">("main");
|
||||||
const [wordData, setWordData] = useState<WordData>(getLocalWordData());
|
const [wordData, setWordData] = useState<WordData>(getLocalWordData());
|
||||||
if (page === "main")
|
if (page === "main")
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { Navbar } from "@/components/Navbar";
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
export default function Home() {
|
export default function HomePage() {
|
||||||
const t = useTranslations("home");
|
const t = useTranslations("home");
|
||||||
function TopArea() {
|
function TopArea() {
|
||||||
return (
|
return (
|
||||||
@@ -101,7 +100,6 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navbar></Navbar>
|
|
||||||
<TopArea></TopArea>
|
<TopArea></TopArea>
|
||||||
<Fortune></Fortune>
|
<Fortune></Fortune>
|
||||||
<Explore></Explore>
|
<Explore></Explore>
|
||||||
|
|||||||
55
src/app/profile/page.tsx
Normal file
55
src/app/profile/page.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { signOut, useSession } from "next-auth/react";
|
||||||
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
|
import Image from "next/image";
|
||||||
|
import DarkButton from "@/components/buttons/DarkButton";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import ACard from "@/components/cards/ACard";
|
||||||
|
import { Center } from "@/components/Center";
|
||||||
|
import LightButton from "@/components/buttons/LightButton";
|
||||||
|
|
||||||
|
export default function MePage() {
|
||||||
|
const session = useSession();
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (session.status !== "authenticated") {
|
||||||
|
router.push(`/login?redirect=${encodeURIComponent(pathname)}`);
|
||||||
|
}
|
||||||
|
}, [session.status, router, pathname]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Center>
|
||||||
|
<ACard>
|
||||||
|
<h1>My Profile</h1>
|
||||||
|
{(session.data?.user?.image as string) && (
|
||||||
|
<Image
|
||||||
|
width={64}
|
||||||
|
height={64}
|
||||||
|
alt="User Avatar"
|
||||||
|
src={session.data?.user?.image as string}
|
||||||
|
className="rounded-4xl"
|
||||||
|
></Image>
|
||||||
|
)}
|
||||||
|
<p>{session.data?.user?.name}</p>
|
||||||
|
<p>Email: {session.data?.user?.email}</p>
|
||||||
|
<DarkButton onClick={signOut}>Logout</DarkButton>
|
||||||
|
<LightButton
|
||||||
|
onClick={() => {
|
||||||
|
fetch("/api/folders", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ name: "New Folder" }),
|
||||||
|
}).then(async (res) => console.log(await res.json()));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
POST
|
||||||
|
</LightButton>
|
||||||
|
</ACard>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,16 +3,14 @@
|
|||||||
import { KeyboardEvent, useRef, useState } from "react";
|
import { KeyboardEvent, useRef, useState } from "react";
|
||||||
import UploadArea from "./UploadArea";
|
import UploadArea from "./UploadArea";
|
||||||
import VideoPanel from "./VideoPlayer/VideoPanel";
|
import VideoPanel from "./VideoPlayer/VideoPanel";
|
||||||
import { Navbar } from "@/components/Navbar";
|
|
||||||
|
|
||||||
export default function SrtPlayer() {
|
export default function SrtPlayerPage() {
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
|
|
||||||
const [videoUrl, setVideoUrl] = useState<string | null>(null);
|
const [videoUrl, setVideoUrl] = useState<string | null>(null);
|
||||||
const [srtUrl, setSrtUrl] = useState<string | null>(null);
|
const [srtUrl, setSrtUrl] = useState<string | null>(null);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navbar></Navbar>
|
|
||||||
<div
|
<div
|
||||||
className="flex w-screen pt-8 items-center justify-center"
|
className="flex w-screen pt-8 items-center justify-center"
|
||||||
onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => e.preventDefault()}
|
onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => e.preventDefault()}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { getTextSpeakerData, setTextSpeakerData } from "@/utils";
|
import { getLocalStorageOperator } from "@/utils";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { TextSpeakerItemSchema } from "@/interfaces";
|
import { TextSpeakerArraySchema, TextSpeakerItemSchema } from "@/interfaces";
|
||||||
import IconClick from "@/components/IconClick";
|
import IconClick from "@/components/IconClick";
|
||||||
import IMAGES from "@/config/images";
|
import IMAGES from "@/config/images";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
@@ -49,23 +49,27 @@ interface SaveListProps {
|
|||||||
}
|
}
|
||||||
export default function SaveList({ show = false, handleUse }: SaveListProps) {
|
export default function SaveList({ show = false, handleUse }: SaveListProps) {
|
||||||
const t = useTranslations("text-speaker");
|
const t = useTranslations("text-speaker");
|
||||||
const [data, setData] = useState(getTextSpeakerData());
|
const { get: getFromLocalStorage, set: setIntoLocalStorage } = getLocalStorageOperator<
|
||||||
|
typeof TextSpeakerArraySchema
|
||||||
|
>("text-speaker", TextSpeakerArraySchema);
|
||||||
|
const [data, setData] = useState(getFromLocalStorage());
|
||||||
const handleDel = (item: z.infer<typeof TextSpeakerItemSchema>) => {
|
const handleDel = (item: z.infer<typeof TextSpeakerItemSchema>) => {
|
||||||
const current_data = getTextSpeakerData();
|
const current_data = getFromLocalStorage();
|
||||||
|
|
||||||
current_data.splice(
|
current_data.splice(
|
||||||
current_data.findIndex((v) => v.text === item.text),
|
current_data.findIndex((v) => v.text === item.text),
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
setTextSpeakerData(current_data);
|
setIntoLocalStorage(current_data);
|
||||||
refresh();
|
refresh();
|
||||||
};
|
};
|
||||||
const refresh = () => {
|
const refresh = () => {
|
||||||
setData(getTextSpeakerData());
|
setData(getFromLocalStorage());
|
||||||
};
|
};
|
||||||
const handleDeleteAll = () => {
|
const handleDeleteAll = () => {
|
||||||
const yesorno = prompt(t("confirmDeleteAll"))?.trim();
|
const yesorno = prompt(t("confirmDeleteAll"))?.trim();
|
||||||
if (yesorno && (yesorno === "Y" || yesorno === "y")) {
|
if (yesorno && (yesorno === "Y" || yesorno === "y")) {
|
||||||
setTextSpeakerData([]);
|
setIntoLocalStorage([]);
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,20 +4,16 @@ import LightButton from "@/components/buttons/LightButton";
|
|||||||
import IconClick from "@/components/IconClick";
|
import IconClick from "@/components/IconClick";
|
||||||
import IMAGES from "@/config/images";
|
import IMAGES from "@/config/images";
|
||||||
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
||||||
import {
|
import { TextSpeakerArraySchema, TextSpeakerItemSchema } from "@/interfaces";
|
||||||
getTextSpeakerData,
|
import { getLocalStorageOperator, getTTSAudioUrl } from "@/utils";
|
||||||
getTTSAudioUrl,
|
|
||||||
setTextSpeakerData,
|
|
||||||
} from "@/utils";
|
|
||||||
import { ChangeEvent, useEffect, useRef, useState } from "react";
|
import { ChangeEvent, useEffect, useRef, useState } from "react";
|
||||||
import SaveList from "./SaveList";
|
|
||||||
import { TextSpeakerItemSchema } from "@/interfaces";
|
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { Navbar } from "@/components/Navbar";
|
import SaveList from "./SaveList";
|
||||||
|
|
||||||
import { VOICES } from "@/config/locales";
|
import { VOICES } from "@/config/locales";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export default function TextSpeaker() {
|
export default function TextSpeakerPage() {
|
||||||
const t = useTranslations("text-speaker");
|
const t = useTranslations("text-speaker");
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const [showSpeedAdjust, setShowSpeedAdjust] = useState(false);
|
const [showSpeedAdjust, setShowSpeedAdjust] = useState(false);
|
||||||
@@ -33,6 +29,11 @@ export default function TextSpeaker() {
|
|||||||
const objurlRef = useRef<string | null>(null);
|
const objurlRef = useRef<string | null>(null);
|
||||||
const [processing, setProcessing] = useState(false);
|
const [processing, setProcessing] = useState(false);
|
||||||
const { play, stop, load, audioRef } = useAudioPlayer();
|
const { play, stop, load, audioRef } = useAudioPlayer();
|
||||||
|
|
||||||
|
const { get: getFromLocalStorage, set: setIntoLocalStorage } = getLocalStorageOperator<
|
||||||
|
typeof TextSpeakerArraySchema
|
||||||
|
>("text-speaker", TextSpeakerArraySchema);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const audio = audioRef.current;
|
const audio = audioRef.current;
|
||||||
if (!audio) return;
|
if (!audio) return;
|
||||||
@@ -196,14 +197,14 @@ export default function TextSpeaker() {
|
|||||||
theIPA = tmp.ipa;
|
theIPA = tmp.ipa;
|
||||||
}
|
}
|
||||||
|
|
||||||
const save = getTextSpeakerData();
|
const save = getFromLocalStorage();
|
||||||
const oldIndex = save.findIndex((v) => v.text === textRef.current);
|
const oldIndex = save.findIndex((v) => v.text === textRef.current);
|
||||||
if (oldIndex !== -1) {
|
if (oldIndex !== -1) {
|
||||||
const oldItem = save[oldIndex];
|
const oldItem = save[oldIndex];
|
||||||
if (theIPA) {
|
if (theIPA) {
|
||||||
if (!oldItem.ipa || oldItem.ipa !== theIPA) {
|
if (!oldItem.ipa || oldItem.ipa !== theIPA) {
|
||||||
oldItem.ipa = theIPA;
|
oldItem.ipa = theIPA;
|
||||||
setTextSpeakerData(save);
|
setIntoLocalStorage(save);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (theIPA.length === 0) {
|
} else if (theIPA.length === 0) {
|
||||||
@@ -218,7 +219,7 @@ export default function TextSpeaker() {
|
|||||||
ipa: theIPA,
|
ipa: theIPA,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setTextSpeakerData(save);
|
setIntoLocalStorage(save);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setLocale(null);
|
setLocale(null);
|
||||||
@@ -229,9 +230,8 @@ export default function TextSpeaker() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navbar></Navbar>
|
|
||||||
<div
|
<div
|
||||||
className="my-4 p-4 mx-4 md:mx-32 border-1 border-gray-200 rounded-2xl"
|
className="my-4 p-4 mx-4 md:mx-32 border border-gray-200 rounded-2xl"
|
||||||
style={{ fontFamily: "Times New Roman, serif" }}
|
style={{ fontFamily: "Times New Roman, serif" }}
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
|
|||||||
@@ -1,293 +1,253 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ChangeEvent, useState } from "react";
|
|
||||||
import LightButton from "@/components/buttons/LightButton";
|
import LightButton from "@/components/buttons/LightButton";
|
||||||
import IconClick from "@/components/IconClick";
|
import IconClick from "@/components/IconClick";
|
||||||
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
|
||||||
import IMAGES from "@/config/images";
|
import IMAGES from "@/config/images";
|
||||||
import { getTTSAudioUrl } from "@/utils";
|
|
||||||
import { Navbar } from "@/components/Navbar";
|
|
||||||
import { VOICES } from "@/config/locales";
|
import { VOICES } from "@/config/locales";
|
||||||
|
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
||||||
|
import { TranslationHistorySchema } from "@/interfaces";
|
||||||
|
import { tlsoPush, tlso } from "@/lib/localStorageOperators";
|
||||||
|
import { getTTSAudioUrl } from "@/lib/tts";
|
||||||
|
import { letsFetch } from "@/utils";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
export default function Translator() {
|
export default function TranslatorPage() {
|
||||||
const t = useTranslations("translator");
|
const t = useTranslations("translator");
|
||||||
const [ipaEnabled, setIPAEnabled] = useState(true);
|
|
||||||
const [targetLang, setTargetLang] = useState("Chinese");
|
const taref = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const [lang, setLang] = useState<string>("chinese");
|
||||||
|
const [tresult, setTresult] = useState<string>("");
|
||||||
|
const [genIpa, setGenIpa] = useState(true);
|
||||||
|
const [ipaTexts, setIpaTexts] = useState(["", ""]);
|
||||||
|
const [processing, setProcessing] = useState(false);
|
||||||
|
const { load, play } = useAudioPlayer();
|
||||||
|
|
||||||
const [sourceText, setSourceText] = useState("");
|
const lastTTS = useRef({
|
||||||
const [targetText, setTargetText] = useState("");
|
text: "",
|
||||||
const [sourceIPA, setSourceIPA] = useState("");
|
url: "",
|
||||||
const [targetIPA, setTargetIPA] = useState("");
|
});
|
||||||
const [sourceLocale, setSourceLocale] = useState<string | null>(null);
|
|
||||||
const [targetLocale, setTargetLocale] = useState<string | null>(null);
|
|
||||||
const [translating, setTranslating] = useState(false);
|
|
||||||
const { play, load } = useAudioPlayer();
|
|
||||||
|
|
||||||
const tl = ["Chinese", "English", "Italian"];
|
const tts = async (text: string, locale: string) => {
|
||||||
|
if (lastTTS.current.text !== text) {
|
||||||
const inputLanguage = () => {
|
const url = await getTTSAudioUrl(
|
||||||
const lang = prompt(t("inputLanguage"))?.trim();
|
text,
|
||||||
if (lang) {
|
VOICES.find((v) => v.locale === locale)!.short_name,
|
||||||
setTargetLang(lang);
|
);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const translate = () => {
|
|
||||||
if (translating) return;
|
|
||||||
if (sourceText.length === 0) return;
|
|
||||||
|
|
||||||
setTranslating(true);
|
|
||||||
|
|
||||||
setTargetText("");
|
|
||||||
setSourceLocale(null);
|
|
||||||
setTargetLocale(null);
|
|
||||||
setSourceIPA("");
|
|
||||||
setTargetIPA("");
|
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
text: sourceText,
|
|
||||||
target: targetLang,
|
|
||||||
});
|
|
||||||
fetch(`/api/translate?${params}`)
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((obj) => {
|
|
||||||
setSourceLocale(obj.source_locale);
|
|
||||||
setTargetLocale(obj.target_locale);
|
|
||||||
setTargetText(obj.target_text);
|
|
||||||
|
|
||||||
if (ipaEnabled) {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
text: sourceText,
|
|
||||||
});
|
|
||||||
fetch(`/api/ipa?${params}`)
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data) => {
|
|
||||||
setSourceIPA(data.ipa);
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
setSourceIPA("");
|
|
||||||
});
|
|
||||||
const params2 = new URLSearchParams({
|
|
||||||
text: obj.target_text,
|
|
||||||
});
|
|
||||||
fetch(`/api/ipa?${params2}`)
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data) => {
|
|
||||||
setTargetIPA(data.ipa);
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
setTargetIPA("");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((r) => {
|
|
||||||
console.error(r);
|
|
||||||
setSourceLocale("");
|
|
||||||
setTargetLocale("");
|
|
||||||
setTargetText("");
|
|
||||||
})
|
|
||||||
.finally(() => setTranslating(false));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInputChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
|
||||||
setSourceText(e.target.value.trim());
|
|
||||||
setTargetText("");
|
|
||||||
setSourceLocale(null);
|
|
||||||
setTargetLocale(null);
|
|
||||||
setSourceIPA("");
|
|
||||||
setTargetIPA("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const readSource = async () => {
|
|
||||||
if (sourceText.length === 0) return;
|
|
||||||
|
|
||||||
if (sourceIPA.length === 0 && ipaEnabled) {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
text: sourceText,
|
|
||||||
});
|
|
||||||
fetch(`/api/ipa?${params}`)
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data) => {
|
|
||||||
setSourceIPA(data.ipa);
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
setSourceIPA("");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sourceLocale) {
|
|
||||||
try {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
text: sourceText.slice(0, 30),
|
|
||||||
});
|
|
||||||
const res = await fetch(`/api/locale?${params}`);
|
|
||||||
const info = await res.json();
|
|
||||||
setSourceLocale(info.locale);
|
|
||||||
|
|
||||||
const voice = VOICES.find((v) => v.locale.startsWith(info.locale));
|
|
||||||
if (!voice) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = await getTTSAudioUrl(sourceText, voice.short_name);
|
|
||||||
await load(url);
|
|
||||||
await play();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
setSourceLocale(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const voice = VOICES.find((v) => v.locale.startsWith(sourceLocale!));
|
|
||||||
if (!voice) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = await getTTSAudioUrl(sourceText, voice.short_name);
|
|
||||||
await load(url);
|
await load(url);
|
||||||
await play();
|
lastTTS.current.text = text;
|
||||||
URL.revokeObjectURL(url);
|
lastTTS.current.url = url;
|
||||||
}
|
}
|
||||||
|
play();
|
||||||
};
|
};
|
||||||
|
|
||||||
const readTarget = async () => {
|
const translate = async () => {
|
||||||
if (targetText.length === 0) return;
|
if (processing) return;
|
||||||
|
setProcessing(true);
|
||||||
|
|
||||||
if (targetIPA.length === 0 && ipaEnabled) {
|
if (!taref.current) return;
|
||||||
const params = new URLSearchParams({
|
const text = taref.current.value;
|
||||||
text: targetText,
|
|
||||||
});
|
|
||||||
fetch(`/api/ipa?${params}`)
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data) => {
|
|
||||||
setTargetIPA(data.ipa);
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
setTargetIPA("");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const voice = VOICES.find((v) => v.locale.startsWith(targetLocale!));
|
const newItem: {
|
||||||
if (!voice) return;
|
text1: string | null;
|
||||||
|
text2: string | null;
|
||||||
|
locale1: string | null;
|
||||||
|
locale2: string | null;
|
||||||
|
} = {
|
||||||
|
text1: text,
|
||||||
|
text2: null,
|
||||||
|
locale1: null,
|
||||||
|
locale2: null,
|
||||||
|
};
|
||||||
|
|
||||||
const url = await getTTSAudioUrl(targetText, voice.short_name);
|
const checkUpdateLocalStorage = (item: typeof newItem) => {
|
||||||
await load(url);
|
if (item.text1 && item.text2 && item.locale1 && item.locale2) {
|
||||||
await play();
|
tlsoPush(item as z.infer<typeof TranslationHistorySchema>);
|
||||||
URL.revokeObjectURL(url);
|
}
|
||||||
|
};
|
||||||
|
const innerStates = {
|
||||||
|
text2: false,
|
||||||
|
ipa1: !genIpa,
|
||||||
|
ipa2: !genIpa,
|
||||||
|
};
|
||||||
|
const checkUpdateProcessStates = () => {
|
||||||
|
if (innerStates.ipa1 && innerStates.ipa2 && innerStates.text2)
|
||||||
|
setProcessing(false);
|
||||||
|
};
|
||||||
|
const updateState = (stateName: keyof typeof innerStates) => () => {
|
||||||
|
innerStates[stateName] = true;
|
||||||
|
checkUpdateLocalStorage(newItem);
|
||||||
|
checkUpdateProcessStates();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch locale for text1
|
||||||
|
letsFetch(
|
||||||
|
`/api/v1/locale?text=${encodeURIComponent(text)}`,
|
||||||
|
(locale: string) => {
|
||||||
|
newItem.locale1 = locale;
|
||||||
|
},
|
||||||
|
console.log,
|
||||||
|
() => {},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (genIpa)
|
||||||
|
// Fetch IPA for text1
|
||||||
|
letsFetch(
|
||||||
|
`/api/v1/ipa?text=${encodeURIComponent(text)}`,
|
||||||
|
(ipa: string) => setIpaTexts((prev) => [ipa, prev[1]]),
|
||||||
|
console.log,
|
||||||
|
updateState("ipa1"),
|
||||||
|
);
|
||||||
|
// Fetch translation for text2
|
||||||
|
letsFetch(
|
||||||
|
`/api/v1/translate?text=${encodeURIComponent(text)}&lang=${encodeURIComponent(lang)}`,
|
||||||
|
(text2) => {
|
||||||
|
setTresult(text2);
|
||||||
|
newItem.text2 = text2;
|
||||||
|
if (genIpa)
|
||||||
|
// Fetch IPA for text2
|
||||||
|
letsFetch(
|
||||||
|
`/api/v1/ipa?text=${encodeURIComponent(text2)}`,
|
||||||
|
(ipa: string) => setIpaTexts((prev) => [prev[0], ipa]),
|
||||||
|
console.log,
|
||||||
|
updateState("ipa2"),
|
||||||
|
);
|
||||||
|
// Fetch locale for text2
|
||||||
|
letsFetch(
|
||||||
|
`/api/v1/locale?text=${encodeURIComponent(text2)}`,
|
||||||
|
(locale: string) => {
|
||||||
|
newItem.locale2 = locale;
|
||||||
|
},
|
||||||
|
console.log,
|
||||||
|
() => {},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
console.log,
|
||||||
|
updateState("text2"),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navbar></Navbar>
|
{/* TCard Component */}
|
||||||
<div className="w-screen flex flex-col md:flex-row md:justify-between gap-2 p-2">
|
<div className="w-screen flex flex-col md:flex-row md:justify-between gap-2 p-2">
|
||||||
<div className="card1 w-full md:w-1/2 flex flex-col-reverse gap-2">
|
{/* Card Component - Left Side */}
|
||||||
<div className="textarea1 border border-gray-200 rounded-2xl w-full h-64 p-2">
|
<div className="w-full md:w-1/2 flex flex-col-reverse gap-2">
|
||||||
|
{/* ICard1 Component */}
|
||||||
|
<div className="border border-gray-200 rounded-2xl w-full h-64 p-2">
|
||||||
<textarea
|
<textarea
|
||||||
|
className="resize-none h-8/12 w-full focus:outline-0"
|
||||||
|
ref={taref}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.ctrlKey && e.key === "Enter") translate();
|
if (e.ctrlKey && e.key === "Enter") translate();
|
||||||
}}
|
}}
|
||||||
onChange={handleInputChange}
|
|
||||||
className="resize-none h-8/12 w-full focus:outline-0"
|
|
||||||
></textarea>
|
></textarea>
|
||||||
<div className="ipa w-full h-2/12 overflow-auto text-gray-600">
|
<div className="ipa w-full h-2/12 overflow-auto text-gray-600">
|
||||||
{sourceIPA}
|
{ipaTexts[0]}
|
||||||
</div>
|
</div>
|
||||||
<div className="h-2/12 w-full flex justify-end items-center">
|
<div className="h-2/12 w-full flex justify-end items-center">
|
||||||
<IconClick
|
<IconClick
|
||||||
onClick={async () => {
|
|
||||||
if (sourceText.length !== 0)
|
|
||||||
await navigator.clipboard.writeText(sourceText);
|
|
||||||
}}
|
|
||||||
src={IMAGES.copy_all}
|
src={IMAGES.copy_all}
|
||||||
alt="copy"
|
alt="copy"
|
||||||
|
onClick={async () => {
|
||||||
|
await navigator.clipboard.writeText(
|
||||||
|
taref.current?.value || "",
|
||||||
|
);
|
||||||
|
}}
|
||||||
></IconClick>
|
></IconClick>
|
||||||
<IconClick
|
<IconClick
|
||||||
onClick={readSource}
|
|
||||||
src={IMAGES.play_arrow}
|
src={IMAGES.play_arrow}
|
||||||
alt="play"
|
alt="play"
|
||||||
|
onClick={() => {
|
||||||
|
const t = taref.current?.value;
|
||||||
|
if (!t) return;
|
||||||
|
tts(t, tlso.get().find((v) => v.text1 === t)?.locale1 || "");
|
||||||
|
}}
|
||||||
></IconClick>
|
></IconClick>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="option1 w-full flex flex-row justify-between items-center">
|
<div className="option1 w-full flex flex-row justify-between items-center">
|
||||||
<span>{t("detectLanguage")}</span>
|
<span>{t("detectLanguage")}</span>
|
||||||
<LightButton
|
<LightButton
|
||||||
selected={ipaEnabled}
|
selected={genIpa}
|
||||||
onClick={() => setIPAEnabled(!ipaEnabled)}
|
onClick={() => setGenIpa((prev) => !prev)}
|
||||||
>
|
>
|
||||||
{t("generateIPA")}
|
{t("generateIPA")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card2 w-full md:w-1/2 flex flex-col-reverse gap-2">
|
|
||||||
<div className="textarea2 bg-gray-100 rounded-2xl w-full h-64 p-2">
|
{/* Card Component - Right Side */}
|
||||||
<div className="h-8/12 w-full">{targetText}</div>
|
<div className="w-full md:w-1/2 flex flex-col-reverse gap-2">
|
||||||
|
{/* ICard2 Component */}
|
||||||
|
<div className="bg-gray-100 rounded-2xl w-full h-64 p-2">
|
||||||
|
<div className="h-8/12 w-full">{tresult}</div>
|
||||||
<div className="ipa w-full h-2/12 overflow-auto text-gray-600">
|
<div className="ipa w-full h-2/12 overflow-auto text-gray-600">
|
||||||
{targetIPA}
|
{ipaTexts[1]}
|
||||||
</div>
|
</div>
|
||||||
<div className="h-2/12 w-full flex justify-end items-center">
|
<div className="h-2/12 w-full flex justify-end items-center">
|
||||||
<IconClick
|
<IconClick
|
||||||
onClick={async () => {
|
|
||||||
if (targetText.length !== 0)
|
|
||||||
await navigator.clipboard.writeText(targetText);
|
|
||||||
}}
|
|
||||||
src={IMAGES.copy_all}
|
src={IMAGES.copy_all}
|
||||||
alt="copy"
|
alt="copy"
|
||||||
|
onClick={async () => {
|
||||||
|
await navigator.clipboard.writeText(tresult);
|
||||||
|
}}
|
||||||
></IconClick>
|
></IconClick>
|
||||||
<IconClick
|
<IconClick
|
||||||
onClick={readTarget}
|
|
||||||
src={IMAGES.play_arrow}
|
src={IMAGES.play_arrow}
|
||||||
alt="play"
|
alt="play"
|
||||||
|
onClick={() => {
|
||||||
|
tts(
|
||||||
|
tresult,
|
||||||
|
tlso.get().find((v) => v.text2 === tresult)?.locale2 || "",
|
||||||
|
);
|
||||||
|
}}
|
||||||
></IconClick>
|
></IconClick>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="option2 w-full flex gap-1 items-center flex-wrap">
|
<div className="option2 w-full flex gap-1 items-center flex-wrap">
|
||||||
<span>{t("translateInto")}</span>
|
<span>{t("translateInto")}</span>
|
||||||
<LightButton
|
<LightButton
|
||||||
onClick={() => {
|
selected={lang === "chinese"}
|
||||||
setTargetLang("Chinese");
|
onClick={() => setLang("chinese")}
|
||||||
}}
|
|
||||||
selected={targetLang === "Chinese"}
|
|
||||||
>
|
>
|
||||||
{t("chinese")}
|
{t("chinese")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
<LightButton
|
<LightButton
|
||||||
onClick={() => {
|
selected={lang === "english"}
|
||||||
setTargetLang("English");
|
onClick={() => setLang("english")}
|
||||||
}}
|
|
||||||
selected={targetLang === "English"}
|
|
||||||
>
|
>
|
||||||
{t("english")}
|
{t("english")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
<LightButton
|
<LightButton
|
||||||
onClick={() => {
|
selected={lang === "italian"}
|
||||||
setTargetLang("Italian");
|
onClick={() => setLang("italian")}
|
||||||
}}
|
|
||||||
selected={targetLang === "Italian"}
|
|
||||||
>
|
>
|
||||||
{t("italian")}
|
{t("italian")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
<LightButton
|
<LightButton
|
||||||
onClick={inputLanguage}
|
selected={!["chinese", "english", "italian"].includes(lang)}
|
||||||
selected={!tl.includes(targetLang)}
|
onClick={() => {
|
||||||
|
const newLang = prompt("Enter language");
|
||||||
|
if (newLang) {
|
||||||
|
setLang(newLang);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t("other") + (tl.includes(targetLang) ? "" : ": " + targetLang)}
|
{t("other")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="button-area w-screen flex justify-center items-center">
|
{/* TranslateButton Component */}
|
||||||
|
<div className="w-screen flex justify-center items-center">
|
||||||
<button
|
<button
|
||||||
|
className={`duration-150 ease-in text-xl font-extrabold border rounded-4xl p-3 border-gray-200 h-16 ${processing ? "bg-gray-200" : "bg-white hover:bg-gray-200 hover:cursor-pointer"}`}
|
||||||
onClick={translate}
|
onClick={translate}
|
||||||
className={`duration-150 ease-in text-xl font-extrabold border rounded-4xl p-3 border-gray-200 h-16 ${translating ? "bg-gray-200" : "bg-white hover:bg-gray-200 hover:cursor-pointer"}`}
|
|
||||||
>
|
>
|
||||||
{translating ? t("translating") : t("translate")}
|
{t("translate")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -10,9 +10,8 @@ import {
|
|||||||
TEXT_SIZE,
|
TEXT_SIZE,
|
||||||
} from "@/config/word-board-config";
|
} from "@/config/word-board-config";
|
||||||
import { inspect } from "@/utils";
|
import { inspect } from "@/utils";
|
||||||
import { Navbar } from "@/components/Navbar";
|
|
||||||
|
|
||||||
export default function WordBoard() {
|
export default function WordBoardPage() {
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const inputFileRef = useRef<HTMLInputElement>(null);
|
const inputFileRef = useRef<HTMLInputElement>(null);
|
||||||
const initialWords = [
|
const initialWords = [
|
||||||
@@ -147,7 +146,6 @@ export default function WordBoard() {
|
|||||||
// }
|
// }
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navbar></Navbar>
|
|
||||||
<div className="flex w-screen h-screen justify-center items-center">
|
<div className="flex w-screen h-screen justify-center items-center">
|
||||||
<div
|
<div
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
|
|||||||
7
src/components/Center.tsx
Normal file
7
src/components/Center.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const Center = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-[calc(100dvh-64px)]">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
25
src/components/Input.tsx
Normal file
25
src/components/Input.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
interface Props {
|
||||||
|
ref?: React.Ref<HTMLInputElement>;
|
||||||
|
placeholder?: string;
|
||||||
|
type?: string;
|
||||||
|
className?: string;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Input({
|
||||||
|
ref,
|
||||||
|
placeholder = "",
|
||||||
|
type = "text",
|
||||||
|
className = "",
|
||||||
|
name = "",
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
ref={ref}
|
||||||
|
placeholder={placeholder}
|
||||||
|
type={type}
|
||||||
|
className={`block focus:outline-none border-b-2 border-gray-600 ${className}`}
|
||||||
|
name={name}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,11 +7,18 @@ import IconClick from "./IconClick";
|
|||||||
import IMAGES from "@/config/images";
|
import IMAGES from "@/config/images";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import LightButton from "./buttons/LightButton";
|
import LightButton from "./buttons/LightButton";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
|
||||||
function MyLink({ href, label }: { href: string; label: string }) {
|
function MyLink({
|
||||||
|
href,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
href: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<Link className="font-bold" href={href}>
|
<Link className="font-bold" href={href}>
|
||||||
{label}
|
{children}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -25,6 +32,7 @@ export function Navbar() {
|
|||||||
document.cookie = `locale=${locale}`;
|
document.cookie = `locale=${locale}`;
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
};
|
};
|
||||||
|
const session = useSession();
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between items-center w-full h-16 px-8 bg-[#35786f] text-white">
|
<div className="flex justify-between items-center w-full h-16 px-8 bg-[#35786f] text-white">
|
||||||
<Link href={"/"} className="text-xl flex">
|
<Link href={"/"} className="text-xl flex">
|
||||||
@@ -64,11 +72,17 @@ export function Navbar() {
|
|||||||
onClick={handleLanguageClick}
|
onClick={handleLanguageClick}
|
||||||
></IconClick>
|
></IconClick>
|
||||||
</div>
|
</div>
|
||||||
<MyLink href="/changelog.txt" label={t("about")}></MyLink>
|
{session?.status === "authenticated" ? (
|
||||||
<MyLink
|
<div className="flex gap-2">
|
||||||
href="https://github.com/GoddoNebianU/learn-languages"
|
<MyLink href="/profile">{t("profile")}</MyLink>
|
||||||
label={t("sourceCode")}
|
</div>
|
||||||
></MyLink>
|
) : (
|
||||||
|
<MyLink href="/login">{t("login")}</MyLink>
|
||||||
|
)}
|
||||||
|
<MyLink href="/changelog.txt">{t("about")}</MyLink>
|
||||||
|
<MyLink href="https://github.com/GoddoNebianU/learn-languages">
|
||||||
|
{t("sourceCode")}
|
||||||
|
</MyLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import NavbarWrapper from "./NavbarWrapper";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
children: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function NavbarCenterWrapper({ children, className }: Props) {
|
|
||||||
return (
|
|
||||||
<NavbarWrapper>
|
|
||||||
<div
|
|
||||||
className={`flex-1 flex justify-center items-center ${className}`}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</NavbarWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { Navbar } from "./Navbar";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function NavbarWrapper({ children }: Props) {
|
|
||||||
return (
|
|
||||||
<div className="h-screen flex flex-col">
|
|
||||||
<Navbar></Navbar>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
34
src/components/auth-components.tsx
Normal file
34
src/components/auth-components.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { signIn, signOut } from "next-auth/react";
|
||||||
|
import LightButton from "./buttons/LightButton";
|
||||||
|
|
||||||
|
export function SignIn({
|
||||||
|
provider,
|
||||||
|
...props
|
||||||
|
}: { provider?: string } & React.ComponentPropsWithRef<typeof LightButton>) {
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
action={async () => {
|
||||||
|
"use server"
|
||||||
|
await signIn(provider)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LightButton {...props}>Sign In</LightButton>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SignOut(props: React.ComponentPropsWithRef<typeof LightButton>) {
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
action={async () => {
|
||||||
|
"use server"
|
||||||
|
await signOut()
|
||||||
|
}}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<LightButton className="w-full p-0" {...props}>
|
||||||
|
Sign Out
|
||||||
|
</LightButton>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,16 +5,19 @@ export default function DarkButton({
|
|||||||
className,
|
className,
|
||||||
selected,
|
selected,
|
||||||
children,
|
children,
|
||||||
|
type = "button",
|
||||||
}: {
|
}: {
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
type?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<PlainButton
|
<PlainButton
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={`hover:bg-gray-600 text-white ${selected ? "bg-gray-600" : "bg-gray-800"} ${className}`}
|
className={`hover:bg-gray-600 text-white ${selected ? "bg-gray-600" : "bg-gray-800"} ${className}`}
|
||||||
|
type={type}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</PlainButton>
|
</PlainButton>
|
||||||
|
|||||||
@@ -5,16 +5,19 @@ export default function LightButton({
|
|||||||
className,
|
className,
|
||||||
selected,
|
selected,
|
||||||
children,
|
children,
|
||||||
|
type = "button"
|
||||||
}: {
|
}: {
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
type?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<PlainButton
|
<PlainButton
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={`hover:bg-gray-200 text-gray-800 ${selected ? "bg-gray-200" : "bg-white"} ${className}`}
|
className={`hover:bg-gray-200 text-gray-800 ${selected ? "bg-gray-200" : "bg-white"} ${className}`}
|
||||||
|
type={type}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</PlainButton>
|
</PlainButton>
|
||||||
|
|||||||
@@ -2,15 +2,18 @@ export default function PlainButton({
|
|||||||
onClick,
|
onClick,
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
|
type = "button",
|
||||||
}: {
|
}: {
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
type?: "button" | "submit" | "reset" | undefined;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={`px-2 py-1 rounded shadow font-bold hover:cursor-pointer ${className}`}
|
className={`px-2 py-1 rounded shadow font-bold hover:cursor-pointer ${className}`}
|
||||||
|
type={type}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ interface ACardProps {
|
|||||||
export default function ACard({ children, className }: ACardProps) {
|
export default function ACard({ children, className }: ACardProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${className} w-[95dvw] md:w-[61vw] h-96 p-2 shadow-2xl bg-white rounded-xl`}
|
className={`${className} w-[95dvw] md:w-[61vw] h-96 p-2 md:shadow-2xl rounded-xl`}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const IMAGES = {
|
|||||||
speed: "/images/speed_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg",
|
speed: "/images/speed_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg",
|
||||||
language_black: "/images/language_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg",
|
language_black: "/images/language_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg",
|
||||||
language_white: "/images/language_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg",
|
language_white: "/images/language_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg",
|
||||||
|
github_mark: "/images/github-mark/github-mark.svg",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default IMAGES;
|
export default IMAGES;
|
||||||
|
|||||||
@@ -24,17 +24,33 @@ export const TextSpeakerItemSchema = z.object({
|
|||||||
export const TextSpeakerArraySchema = z.array(TextSpeakerItemSchema);
|
export const TextSpeakerArraySchema = z.array(TextSpeakerItemSchema);
|
||||||
|
|
||||||
export const WordDataSchema = z.object({
|
export const WordDataSchema = z.object({
|
||||||
locales: z.tuple([z.string(), z.string()])
|
locales: z
|
||||||
|
.tuple([z.string(), z.string()])
|
||||||
.refine(([first, second]) => first !== second, {
|
.refine(([first, second]) => first !== second, {
|
||||||
message: "Locales must be different"
|
message: "Locales must be different",
|
||||||
}),
|
}),
|
||||||
wordPairs: z.array(z.tuple([z.string(), z.string()]))
|
wordPairs: z
|
||||||
|
.array(z.tuple([z.string(), z.string()]))
|
||||||
.min(1, "At least one word pair is required")
|
.min(1, "At least one word pair is required")
|
||||||
.refine((pairs) => {
|
.refine(
|
||||||
return pairs.every(([first, second]) => first.trim() !== '' && second.trim() !== '');
|
(pairs) => {
|
||||||
}, {
|
return pairs.every(
|
||||||
message: "Word pairs cannot contain empty strings"
|
([first, second]) => first.trim() !== "" && second.trim() !== "",
|
||||||
})
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Word pairs cannot contain empty strings",
|
||||||
|
},
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const TranslationHistorySchema = z.object({
|
||||||
|
text1: z.string(),
|
||||||
|
text2: z.string(),
|
||||||
|
locale1: z.string(),
|
||||||
|
locale2: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TranslationHistoryArraySchema = z.array(TranslationHistorySchema);
|
||||||
|
|
||||||
export type WordData = z.infer<typeof WordDataSchema>;
|
export type WordData = z.infer<typeof WordDataSchema>;
|
||||||
|
|||||||
11
src/lib/SessionWrapper.tsx
Normal file
11
src/lib/SessionWrapper.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { SessionProvider } from "next-auth/react";
|
||||||
|
|
||||||
|
export default function SessionWrapper({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return <SessionProvider>{children}</SessionProvider>;
|
||||||
|
}
|
||||||
11
src/lib/actions.ts
Normal file
11
src/lib/actions.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { UserController } from "./db";
|
||||||
|
|
||||||
|
export async function loginAction(formData: FormData) {
|
||||||
|
const username = formData.get("username")?.toString();
|
||||||
|
const password = formData.get("password")?.toString();
|
||||||
|
|
||||||
|
|
||||||
|
if (username && password) await UserController.createUser(username, password);
|
||||||
|
}
|
||||||
63
src/lib/ai.ts
Normal file
63
src/lib/ai.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { format } from "util";
|
||||||
|
|
||||||
|
async function callZhipuAPI(
|
||||||
|
messages: { role: string; content: string }[],
|
||||||
|
model = "glm-4.6",
|
||||||
|
) {
|
||||||
|
const url = "https://open.bigmodel.cn/api/paas/v4/chat/completions";
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer " + process.env.ZHIPU_API_KEY,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: model,
|
||||||
|
messages: messages,
|
||||||
|
temperature: 0.2,
|
||||||
|
thinking: {
|
||||||
|
type: "disabled",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API 调用失败: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLLMAnswer(prompt: string) {
|
||||||
|
return (
|
||||||
|
await callZhipuAPI([
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: prompt,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
).choices[0].message.content.trim() as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function simpleGetLLMAnswer(
|
||||||
|
prompt: string,
|
||||||
|
searchParams: URLSearchParams,
|
||||||
|
args: string[],
|
||||||
|
) {
|
||||||
|
if (args.some((arg) => typeof searchParams.get(arg) !== "string")) {
|
||||||
|
return Response.json({
|
||||||
|
status: "error",
|
||||||
|
message: "Missing required parameters",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Response.json({
|
||||||
|
status: "success",
|
||||||
|
message: await getLLMAnswer(
|
||||||
|
format(
|
||||||
|
prompt,
|
||||||
|
...args.map((v) => searchParams.get(v)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
149
src/lib/db.ts
Normal file
149
src/lib/db.ts
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import bcrypt from "bcryptjs";
|
||||||
|
import { Pool } from "pg";
|
||||||
|
|
||||||
|
export const pool = new Pool({
|
||||||
|
user: "postgres",
|
||||||
|
host: "localhost",
|
||||||
|
max: 20,
|
||||||
|
idleTimeoutMillis: 3000,
|
||||||
|
connectionTimeoutMillis: 2000,
|
||||||
|
maxLifetimeSeconds: 60,
|
||||||
|
});
|
||||||
|
|
||||||
|
export class UserController {
|
||||||
|
static async createUser(username: string, password: string) {
|
||||||
|
const encodedPassword = await bcrypt.hash(password, 10);
|
||||||
|
try {
|
||||||
|
await pool.query(
|
||||||
|
"INSERT INTO users (username, password) VALUES ($1, $2)",
|
||||||
|
[username, encodedPassword],
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async getUserByUsername(username: string) {
|
||||||
|
try {
|
||||||
|
const user = await pool.query("SELECT * FROM users WHERE username = $1", [
|
||||||
|
username,
|
||||||
|
]);
|
||||||
|
return user.rows[0];
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async deleteUserById(id: number) {
|
||||||
|
try {
|
||||||
|
await pool.query("DELETE FROM users WHERE id = $1", [id]);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FolderController {
|
||||||
|
static async getFolderById(id: number) {
|
||||||
|
try {
|
||||||
|
const folder = await pool.query("SELECT * FROM folders WHERE id = $1", [
|
||||||
|
id,
|
||||||
|
]);
|
||||||
|
return folder.rows[0];
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async deleteFolderById(id: number) {
|
||||||
|
try {
|
||||||
|
await pool.query("DELETE FROM folders WHERE id = $1", [id]);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async getFoldersByOwner(owner: string) {
|
||||||
|
try {
|
||||||
|
const folders = await pool.query(
|
||||||
|
"SELECT * FROM folders WHERE owner = $1",
|
||||||
|
[owner],
|
||||||
|
);
|
||||||
|
return folders.rows;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async createFolder(name: string, owner: string) {
|
||||||
|
try {
|
||||||
|
return (
|
||||||
|
await pool.query("INSERT INTO folders (name, owner) VALUES ($1, $2)", [
|
||||||
|
name,
|
||||||
|
owner,
|
||||||
|
])
|
||||||
|
).rows[0];
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WordPairController {
|
||||||
|
static async createWordPair(
|
||||||
|
locale1: string,
|
||||||
|
locale2: string,
|
||||||
|
text1: string,
|
||||||
|
text2: string,
|
||||||
|
folderId: number,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await pool.query(
|
||||||
|
"INSERT INTO word_pairs (locale1, locale2, text1, text2, folder_id) VALUES ($1, $2, $3, $4, $5)",
|
||||||
|
[locale1, locale2, text1, text2, folderId],
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async getWordPairById(id: number) {
|
||||||
|
try {
|
||||||
|
const wordPair = await pool.query(
|
||||||
|
"SELECT * FROM word_pairs WHERE id = $1",
|
||||||
|
[id],
|
||||||
|
);
|
||||||
|
return wordPair.rows[0];
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async deleteWordPairById(id: number) {
|
||||||
|
try {
|
||||||
|
await pool.query("DELETE FROM word_pairs WHERE id = $1", [id]);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async updateWordPairById(
|
||||||
|
id: number,
|
||||||
|
locale1: string,
|
||||||
|
locale2: string,
|
||||||
|
text1: string,
|
||||||
|
text2: string,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await pool.query(
|
||||||
|
"UPDATE word_pairs SET locale1 = $1, locale2 = $2, text1 = $3, text2 = $4 WHERE id = $5",
|
||||||
|
[locale1, locale2, text1, text2, id],
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async getWordPairsByFolderId(folderId: number) {
|
||||||
|
try {
|
||||||
|
const wordPairs = await pool.query(
|
||||||
|
"SELECT * FROM word_pairs WHERE folder_id = $1",
|
||||||
|
[folderId],
|
||||||
|
);
|
||||||
|
return wordPairs.rows;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/lib/localStorageOperators.ts
Normal file
17
src/lib/localStorageOperators.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { TranslationHistoryArraySchema, TranslationHistorySchema } from "@/interfaces";
|
||||||
|
import { getLocalStorageOperator } from "@/utils";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
const MAX_HISTORY_LENGTH = 50;
|
||||||
|
|
||||||
|
export const tlso = getLocalStorageOperator<typeof TranslationHistoryArraySchema>(
|
||||||
|
"translator",
|
||||||
|
TranslationHistoryArraySchema,
|
||||||
|
);
|
||||||
|
export const tlsoPush = (item: z.infer<typeof TranslationHistorySchema>) => {
|
||||||
|
tlso.set(
|
||||||
|
[...tlso.get(), item as z.infer<typeof TranslationHistorySchema>].slice(
|
||||||
|
-MAX_HISTORY_LENGTH,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
16
src/lib/tts.ts
Normal file
16
src/lib/tts.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { ProsodyOptions } from "edge-tts-universal";
|
||||||
|
import { EdgeTTS } from "edge-tts-universal/browser";
|
||||||
|
|
||||||
|
export async function getTTSAudioUrl(
|
||||||
|
text: string,
|
||||||
|
short_name: string,
|
||||||
|
options: ProsodyOptions | undefined = undefined,
|
||||||
|
) {
|
||||||
|
const tts = new EdgeTTS(text, short_name, options);
|
||||||
|
try {
|
||||||
|
const result = await tts.synthesize();
|
||||||
|
return URL.createObjectURL(result.audio);
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
80
src/utils.ts
80
src/utils.ts
@@ -1,6 +1,5 @@
|
|||||||
import { EdgeTTS, ProsodyOptions } from "edge-tts-universal/browser";
|
import { EdgeTTS, ProsodyOptions } from "edge-tts-universal/browser";
|
||||||
import { env } from "process";
|
import { env } from "process";
|
||||||
import { TextSpeakerArraySchema } from "./interfaces";
|
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
@@ -60,33 +59,43 @@ export async function getTTSAudioUrl(
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export const getTextSpeakerData = () => {
|
|
||||||
try {
|
|
||||||
if (!localStorage) return [];
|
|
||||||
const item = localStorage.getItem("text-speaker");
|
|
||||||
|
|
||||||
if (!item) return [];
|
export const getLocalStorageOperator = <T extends z.ZodTypeAny>(
|
||||||
|
key: string,
|
||||||
const rawData = JSON.parse(item);
|
schema: T,
|
||||||
const result = TextSpeakerArraySchema.safeParse(rawData);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
return result.data;
|
|
||||||
} else {
|
|
||||||
console.error("Invalid data structure in localStorage:", result.error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to parse text-speaker data:", e);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
export const setTextSpeakerData = (
|
|
||||||
data: z.infer<typeof TextSpeakerArraySchema>,
|
|
||||||
) => {
|
) => {
|
||||||
if (!localStorage) return;
|
return {
|
||||||
localStorage.setItem("text-speaker", JSON.stringify(data));
|
get: (): z.infer<T> => {
|
||||||
|
try {
|
||||||
|
if (!localStorage) return [];
|
||||||
|
const item = localStorage.getItem(key);
|
||||||
|
|
||||||
|
if (!item) return [];
|
||||||
|
|
||||||
|
const rawData = JSON.parse(item) as z.infer<T>;
|
||||||
|
const result = schema.safeParse(rawData);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return result.data;
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"Invalid data structure in localStorage:",
|
||||||
|
result.error,
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to parse ${key} data:`, e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set: (data: z.infer<T>) => {
|
||||||
|
if (!localStorage) return;
|
||||||
|
localStorage.setItem(key, JSON.stringify(data));
|
||||||
|
},
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function handleAPIError(error: unknown, message: string) {
|
export function handleAPIError(error: unknown, message: string) {
|
||||||
console.error(message, error);
|
console.error(message, error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -94,3 +103,24 @@ export function handleAPIError(error: unknown, message: string) {
|
|||||||
{ status: 500 },
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const letsFetch = (
|
||||||
|
url: string,
|
||||||
|
onSuccess: (message: string) => void,
|
||||||
|
onError: (message: string) => void,
|
||||||
|
onFinally: () => void,
|
||||||
|
) => {
|
||||||
|
return fetch(url)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data.status === "success") {
|
||||||
|
onSuccess(data.message);
|
||||||
|
} else if (data.status === "error") {
|
||||||
|
onError(data.message);
|
||||||
|
} else {
|
||||||
|
onError("Unknown error");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(onFinally);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user