Merge pull request 'make translate for login, register pages' (#1) from translate into main

Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
2025-12-10 17:40:21 +08:00
31 changed files with 830 additions and 103 deletions
+3 -1
View File
@@ -1,7 +1,9 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
import createNextIntlPlugin from 'next-intl/plugin';
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* output: 'export' */ /* output: 'export' */
}; };
export default nextConfig; const withNextIntl = createNextIntlPlugin();
export default withNextIntl(nextConfig);
+375 -1
View File
@@ -14,6 +14,7 @@
"jose": "^6.1.2", "jose": "^6.1.2",
"next": "^16.0.7", "next": "^16.0.7",
"next-auth": "5.0.0-beta.30", "next-auth": "5.0.0-beta.30",
"next-intl": "^4.5.8",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"tailwindcss": "^4.1.17", "tailwindcss": "^4.1.17",
@@ -485,6 +486,66 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@formatjs/ecma402-abstract": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz",
"integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==",
"license": "MIT",
"dependencies": {
"@formatjs/fast-memoize": "2.2.7",
"@formatjs/intl-localematcher": "0.6.2",
"decimal.js": "^10.4.3",
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/ecma402-abstract/node_modules/@formatjs/intl-localematcher": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz",
"integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/fast-memoize": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz",
"integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==",
"license": "MIT",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/icu-messageformat-parser": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz",
"integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==",
"license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.6",
"@formatjs/icu-skeleton-parser": "1.8.16",
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/icu-skeleton-parser": {
"version": "1.8.16",
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz",
"integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==",
"license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.6",
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/intl-localematcher": {
"version": "0.5.10",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz",
"integrity": "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==",
"license": "MIT",
"dependencies": {
"tslib": "2"
}
},
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
"version": "0.19.1", "version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -1592,6 +1653,178 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@schummar/icu-type-parser": {
"version": "1.21.5",
"resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz",
"integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==",
"license": "MIT"
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.3.tgz",
"integrity": "sha512-AXfeQn0CvcQ4cndlIshETx6jrAM45oeUrK8YeEY6oUZU/qzz0Id0CyvlEywxkWVC81Ajpd8TQQ1fW5yx6zQWkQ==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-darwin-x64": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.3.tgz",
"integrity": "sha512-p68OeCz1ui+MZYG4wmfJGvcsAcFYb6Sl25H9TxWl+GkBgmNimIiRdnypK9nBGlqMZAcxngNPtnG3kEMNnvoJ2A==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.3.tgz",
"integrity": "sha512-Nuj5iF4JteFgwrai97mUX+xUOl+rQRHqTvnvHMATL/l9xE6/TJfPBpd3hk/PVpClMXG3Uvk1MxUFOEzM1JrMYg==",
"cpu": [
"arm"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.3.tgz",
"integrity": "sha512-2Nc/s8jE6mW2EjXWxO/lyQuLKShcmTrym2LRf5Ayp3ICEMX6HwFqB1EzDhwoMa2DcUgmnZIalesq2lG3krrUNw==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-musl": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.3.tgz",
"integrity": "sha512-j4SJniZ/qaZ5g8op+p1G9K1z22s/EYGg1UXIb3+Cg4nsxEpF5uSIGEE4mHUfA70L0BR9wKT2QF/zv3vkhfpX4g==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.3.tgz",
"integrity": "sha512-aKttAZnz8YB1VJwPQZtyU8Uk0BfMP63iDMkvjhJzRZVgySmqt/apWSdnoIcZlUoGheBrcqbMC17GGUmur7OT5A==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.3.tgz",
"integrity": "sha512-oe8FctPu1gnUsdtGJRO2rvOUIkkIIaHqsO9xxN0bTR7dFTlPTGi2Fhk1tnvXeyAvCPxLIcwD8phzKg6wLv9yug==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.3.tgz",
"integrity": "sha512-L9AjzP2ZQ/Xh58e0lTRMLvEDrcJpR7GwZqAtIeNLcTK7JVE+QineSyHp0kLkO1rttCHyCy0U74kDTj0dRz6raA==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.3.tgz",
"integrity": "sha512-B8UtogMzErUPDWUoKONSVBdsgKYd58rRyv2sHJWKOIMCHfZ22FVXICR4O/VwIYtlnZ7ahERcjayBHDlBZpR0aw==",
"cpu": [
"ia32"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-x64-msvc": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.3.tgz",
"integrity": "sha512-SpZKMR9QBTecHeqpzJdYEfgw30Oo8b/Xl6rjSzBt1g0ZsXyy60KLXrp6IagQyfTYqNYE/caDvwtF2FPn7pomog==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
"license": "Apache-2.0"
},
"node_modules/@swc/helpers": { "node_modules/@swc/helpers": {
"version": "0.5.15", "version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -1601,6 +1834,15 @@
"tslib": "^2.8.0" "tslib": "^2.8.0"
} }
}, },
"node_modules/@swc/types": {
"version": "0.1.25",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz",
"integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==",
"license": "Apache-2.0",
"dependencies": {
"@swc/counter": "^0.1.3"
}
},
"node_modules/@tailwindcss/node": { "node_modules/@tailwindcss/node": {
"version": "4.1.17", "version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz",
@@ -3166,6 +3408,12 @@
} }
} }
}, },
"node_modules/decimal.js": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"license": "MIT"
},
"node_modules/deep-is": { "node_modules/deep-is": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -4408,6 +4656,18 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/intl-messageformat": {
"version": "10.7.18",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz",
"integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==",
"license": "BSD-3-Clause",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.6",
"@formatjs/fast-memoize": "2.2.7",
"@formatjs/icu-messageformat-parser": "2.11.4",
"tslib": "^2.8.0"
}
},
"node_modules/is-array-buffer": { "node_modules/is-array-buffer": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -5409,6 +5669,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/negotiator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/next": { "node_modules/next": {
"version": "16.0.7", "version": "16.0.7",
"resolved": "https://registry.npmjs.org/next/-/next-16.0.7.tgz", "resolved": "https://registry.npmjs.org/next/-/next-16.0.7.tgz",
@@ -5488,6 +5757,91 @@
} }
} }
}, },
"node_modules/next-intl": {
"version": "4.5.8",
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.5.8.tgz",
"integrity": "sha512-BdN6494nvt09WtmW5gbWdwRhDDHC/Sg7tBMhN7xfYds3vcRCngSDXat81gmJkblw9jYOv8zXzzFJyu5VYXnJzg==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/amannn"
}
],
"license": "MIT",
"dependencies": {
"@formatjs/intl-localematcher": "^0.5.4",
"@swc/core": "^1.15.2",
"negotiator": "^1.0.0",
"next-intl-swc-plugin-extractor": "^4.5.8",
"po-parser": "^1.0.2",
"use-intl": "^4.5.8"
},
"peerDependencies": {
"next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0",
"typescript": "^5.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/next-intl-swc-plugin-extractor": {
"version": "4.5.8",
"resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.5.8.tgz",
"integrity": "sha512-hscCKUv+5GQ0CCNbvqZ8gaxnAGToCgDTbL++jgCq8SCk/ljtZDEeQZcMk46Nm6Ynn49Q/JKF4Npo/Sq1mpbusA==",
"license": "MIT"
},
"node_modules/next-intl/node_modules/@swc/core": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.3.tgz",
"integrity": "sha512-Qd8eBPkUFL4eAONgGjycZXj1jFCBW8Fd+xF0PzdTlBCWQIV1xnUT7B93wUANtW3KGjl3TRcOyxwSx/u/jyKw/Q==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.25"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.15.3",
"@swc/core-darwin-x64": "1.15.3",
"@swc/core-linux-arm-gnueabihf": "1.15.3",
"@swc/core-linux-arm64-gnu": "1.15.3",
"@swc/core-linux-arm64-musl": "1.15.3",
"@swc/core-linux-x64-gnu": "1.15.3",
"@swc/core-linux-x64-musl": "1.15.3",
"@swc/core-win32-arm64-msvc": "1.15.3",
"@swc/core-win32-ia32-msvc": "1.15.3",
"@swc/core-win32-x64-msvc": "1.15.3"
},
"peerDependencies": {
"@swc/helpers": ">=0.5.17"
},
"peerDependenciesMeta": {
"@swc/helpers": {
"optional": true
}
}
},
"node_modules/next-intl/node_modules/@swc/helpers": {
"version": "0.5.17",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
"license": "Apache-2.0",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/node-addon-api": { "node_modules/node-addon-api": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
@@ -5762,6 +6116,12 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/po-parser": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/po-parser/-/po-parser-1.0.2.tgz",
"integrity": "sha512-yTIQL8PZy7V8c0psPoJUx7fayez+Mo/53MZgX9MPuPHx+Dt+sRPNuRbI+6Oqxnddhkd68x4Nlgon/zizL1Xg+w==",
"license": "MIT"
},
"node_modules/possible-typed-array-names": { "node_modules/possible-typed-array-names": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -6767,7 +7127,7 @@
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
@@ -6903,6 +7263,20 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/use-intl": {
"version": "4.5.8",
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.5.8.tgz",
"integrity": "sha512-rWPV2Sirw55BQbA/7ndUBtsikh8WXwBrUkZJ1mD35+emj/ogPPqgCZdv1DdrEFK42AjF1g5w8d3x8govhqPH6Q==",
"license": "MIT",
"dependencies": {
"@formatjs/fast-memoize": "^2.2.0",
"@schummar/icu-type-parser": "1.21.5",
"intl-messageformat": "^10.5.14"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+1
View File
@@ -15,6 +15,7 @@
"jose": "^6.1.2", "jose": "^6.1.2",
"next": "^16.0.7", "next": "^16.0.7",
"next-auth": "5.0.0-beta.30", "next-auth": "5.0.0-beta.30",
"next-intl": "^4.5.8",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"tailwindcss": "^4.1.17", "tailwindcss": "^4.1.17",
+41
View File
@@ -0,0 +1,41 @@
import Providers from '@/app/providers/getQueryServer'
import type { Metadata } from "next";
import { NextIntlClientProvider, hasLocale } from 'next-intl';
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';
import "../styles/globals.css";
import "../styles/global-styles.scss";
export function generateStaticParams() {
return routing.locales.map((locale: any) => ({ locale }));
}
export const metadata: Metadata = {
title: "No copy",
description: "No copy",
};
export default async function RootLayout({
children,
params
}: Readonly<{
children: React.ReactNode;
params: Promise<{ locale: string }>;
}>) {
const { locale } = await params;
if (!hasLocale(routing.locales, locale)) {
notFound();
}
return (
<html lang={locale}>
<body>
<NextIntlClientProvider>
<Providers>
{children}
</Providers>
</NextIntlClientProvider>
</body>
</html>
);
}
@@ -3,45 +3,58 @@ import styles from '@/app/styles/login.module.scss'
import LogoIcon from '@/app/ui/logo-icon'; import LogoIcon from '@/app/ui/logo-icon';
import Link from 'next/link'; import Link from 'next/link';
import LoginForm from '@/app/ui/login-form'; import LoginForm from '@/app/ui/login-form';
import LanguageSwitcher from '@/app/components/LanguageSwitcher';
import { useTranslations } from 'next-intl';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Login', title: 'Login',
}; };
export default function Page() { export default function Page() {
const t = useTranslations('Login-register-form');
return ( return (
<div className={`${styles['login-container-wrapper']}`}> <div className={`${styles['login-container-wrapper']}`}>
<div className={`${styles['login-container-language']}`}>
<LanguageSwitcher />
</div>
<div className={`${styles['login-container']}`}> <div className={`${styles['login-container']}`}>
<div className={`${styles['logo']}`}> <div className={`${styles['logo']}`}>
<LogoIcon /> <LogoIcon />
<h1>NO COPY</h1> <h1>NO COPY</h1>
<p>Защита авторских прав</p> <p>
{t('copyright-protection')}
</p>
</div> </div>
<LoginForm /> <LoginForm />
<div className="divider"> <div className="divider">
<span>или войти через</span> <span>
{t('or-log-in-via')}
</span>
</div> </div>
<div className={`${styles['register-link']}`}> <div className={`${styles['register-link']}`}>
<p>Нет аккаунта? <Link href="register">Зарегистрироваться</Link></p> <p>{t('no-account')}?
<Link href="register"> {t('register')}</Link>
</p>
</div> </div>
<div className={`${styles['features']}`}> <div className={`${styles['features']}`}>
<div className={`${styles['feature']}`}> <div className={`${styles['feature']}`}>
<svg className={`${styles['feature-icon']}`} viewBox="0 0 24 24" fill="currentColor"> <svg className={`${styles['feature-icon']}`} viewBox="0 0 24 24" fill="currentColor">
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path> <path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg> </svg>
Защита контента скрытыми водяными знаками {t('protecting-watermark')}
</div> </div>
<div className={`${styles['feature']}`}> <div className={`${styles['feature']}`}>
<svg className={`${styles['feature-icon']}`} viewBox="0 0 24 24" fill="currentColor"> <svg className={`${styles['feature-icon']}`} viewBox="0 0 24 24" fill="currentColor">
<path d="M13 10V3L4 14h7v7l9-11h-7z"></path> <path d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg> </svg>
Мониторинг нарушений в реальном времени {t('monitoring-violations')}
</div> </div>
<div className={`${styles['feature']}`}> <div className={`${styles['feature']}`}>
<svg className={`${styles['feature-icon']}`} viewBox="0 0 24 24" fill="currentColor"> <svg className={`${styles['feature-icon']}`} viewBox="0 0 24 24" fill="currentColor">
<path d="M12 1L3 5v6c0 5.55 3.84 9.74 9 11 5.16-1.26 9-5.45 9-11V5l-9-4z"></path> <path d="M12 1L3 5v6c0 5.55 3.84 9.74 9 11 5.16-1.26 9-5.45 9-11V5l-9-4z"></path>
</svg> </svg>
Надежное хранение и безопасность данных {t('safe-storage')}
</div> </div>
</div> </div>
</div> </div>
@@ -1,17 +1,21 @@
import styles from "@/app/styles/page.module.scss"; import styles from "@/app/styles/page.module.scss";
import { Metadata } from 'next'; import { Metadata } from 'next';
import Link from 'next/link'; import Link from 'next/link';
import { useTranslations } from 'next-intl';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Home page', title: 'Home page',
}; };
export default function Home() { export default function Home() {
const t = useTranslations('HomePage');
return ( return (
<div className={styles.page}> <div className={styles.page}>
<main className={styles.main}> <main className={styles.main}>
<div className={styles.intro}> <div className={styles.intro}>
Home page Home page
{t('title')}
</div> </div>
<Link key={'dashboard'} <Link key={'dashboard'}
href={'/pages/dashboard'} href={'/pages/dashboard'}
@@ -1,25 +1,26 @@
import { Metadata } from 'next'; import { Metadata } from 'next';
import { Suspense } from 'react'; import { Suspense } from 'react';
import ButtonTest from '@/app/ui/button-test'
import ProtectionOverview from '@/app/ui/dashboard/protection-overview'; import ProtectionOverview from '@/app/ui/dashboard/protection-overview';
import StatsGrid from '@/app/ui/dashboard/stats-grid'; import StatsGrid from '@/app/ui/dashboard/stats-grid';
import StorageBreakdown from '@/app/ui/dashboard/storage-breakdown'; import StorageBreakdown from '@/app/ui/dashboard/storage-breakdown';
import RecentFilesList from '@/app/ui/dashboard/recent-files-list';
import ChartContainer from '@/app/ui/dashboard/chart-container'; import ChartContainer from '@/app/ui/dashboard/chart-container';
import UserInfo from '@/app/ui/dashboard/user-info';
import ViolationsTable from '@/app/ui/dashboard/violations-table'; import ViolationsTable from '@/app/ui/dashboard/violations-table';
import ViolationsTypeTable from '@/app/ui/dashboard/violations-type-table'; import ViolationsTypeTable from '@/app/ui/dashboard/violations-type-table';
import ActivityChart from '@/app/ui/dashboard/activity-chart'; import { useTranslations } from 'next-intl';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Dashboard', title: 'Dashboard',
}; };
export default function Page() { export default function Page() {
const t = useTranslations('Global');
return ( return (
<div> <div>
<Suspense fallback={<>...</>}> <Suspense fallback={<>...</>}>
<h1 className="page-title">Статистика</h1> <h1 className="page-title">
{t("statistics")}
</h1>
<ProtectionOverview /> <ProtectionOverview />
<StatsGrid /> <StatsGrid />
<StorageBreakdown /> <StorageBreakdown />
@@ -1,6 +1,6 @@
import NavLinks from '@/app/ui/nav-links' import NavLinks from '@/app/ui/nav-links'
import styles from '@/app/styles/page.module.scss' import styles from '@/app/styles/page.module.scss'
import HeaderPanel from '../ui/header/headerPanel'; import HeaderPanel from '@/app/ui/header/headerPanel';
import { getQueryClient } from '@/app/providers/getQueryClient'; import { getQueryClient } from '@/app/providers/getQueryClient';
import { HydrationBoundary, dehydrate } from '@tanstack/react-query'; import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
import { getUserData } from '@/app/actions/action'; import { getUserData } from '@/app/actions/action';
@@ -2,26 +2,39 @@ import LogoIcon from '@/app/ui/logo-icon';
import styles from '@/app/styles/login.module.scss' import styles from '@/app/styles/login.module.scss'
import Link from 'next/link'; import Link from 'next/link';
import RegisterForm from '@/app/ui/register-form'; import RegisterForm from '@/app/ui/register-form';
import LanguageSwitcher from '@/app/components/LanguageSwitcher';
import {useTranslations} from 'next-intl';
export default function Page() { export default function Page() {
const t = useTranslations('Login-register-form');
return ( return (
<div className={`${styles['login-container-wrapper']}`}> <div className={`${styles['login-container-wrapper']}`}>
<div className={`${styles['login-container-language']}`}>
<LanguageSwitcher />
</div>
<div className={`${styles['login-container']}`}> <div className={`${styles['login-container']}`}>
<div className={`${styles['logo']}`}> <div className={`${styles['logo']}`}>
<LogoIcon /> <LogoIcon />
<h1>NO COPY</h1> <h1>NO COPY</h1>
<p>Создать аккаунт</p> <p>
{t('create-an-account')}
</p>
</div> </div>
<div className={`${styles['subscription-info']}`}> <div className={`${styles['subscription-info']}`}>
<h4>🎉 Бесплатный старт</h4> <h4>🎉 {t('free-start')}</h4>
<p>При регистрации вы получаете подписку "ДЕМО" с возможностью маркировки до 3 файлов в месяц и 10 МБ хранилища.</p> <p>
{t('free-start-description')}
</p>
</div> </div>
<RegisterForm /> <RegisterForm />
<div className="divider"> <div className="divider">
<span>или зарегистрироваться через</span> <span>
{t('or-register-via')}
</span>
</div> </div>
<div className={`${styles['register-link']}`}> <div className={`${styles['register-link']}`}>
<p>Уже есть аккаунт? <Link href="login">Войти</Link></p> <p>{t('already-have-an-account')}? <Link href="login">{t('login')}</Link></p>
</div> </div>
</div> </div>
</div> </div>
+4 -4
View File
@@ -83,7 +83,7 @@ export async function authorization(
let parsed = await response.json(); let parsed = await response.json();
await createSession(parsed.token, parsed.email); await createSession(parsed.token, parsed.email);
} else { } else {
throw (`Запрос завершился с ошибкой: ${response.status}`); throw ('request-ended-with-an-error');
} }
} catch (error) { } catch (error) {
@@ -146,7 +146,7 @@ export async function registration(
path: string path: string
}) => { }) => {
if (errors[obj.path[0]]) { if (errors[obj.path[0]]) {
errors[obj.path[0]] = `${errors[obj.path[0]]} ${obj.message}`; errors[obj.path[0]] = `${errors[obj.path[0]]}&${obj.message}`;
} else { } else {
errors[obj.path[0]] = obj.message; errors[obj.path[0]] = obj.message;
} }
@@ -168,7 +168,7 @@ export async function registration(
try { try {
const { full_name, email, password, phone } = validatedFields.data; const { full_name, email, password, phone } = validatedFields.data;
const response = await fetch(`${API_BASE_URL}/v1/api/auth/register`, { const response = await fetch(`${API_BASE_URL}/v1/api/auth/register1`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ fullName: full_name, email, password, companyName: company, phone: phone }), body: JSON.stringify({ fullName: full_name, email, password, companyName: company, phone: phone }),
headers: { headers: {
@@ -186,7 +186,7 @@ export async function registration(
} catch (error) { } catch (error) {
if (error) { if (error) {
const errors: Record<string, string> = {}; const errors: Record<string, string> = {};
errors['server'] = `Запрос завершился с ошибкой: ${error}` errors['server'] = 'request-ended-with-an-error'
return { return {
previousState: { previousState: {
+10 -10
View File
@@ -20,26 +20,26 @@ export const SignupFormSchema = z
.object({ .object({
full_name: z full_name: z
.string() .string()
.min(3, { error: 'Имя должно содержать не менее 3 символов.' }) .min(3, { error: 'register-error-name' })
.trim(), .trim(),
email: z.email({ error: 'Пожалуйста, введите правильный адрес электронной почты.' }).trim(), email: z.email({ error: 'register-error-valid-email' }).trim(),
phone: z phone: z
.string() .string()
.min(12, { error: 'Пожалуйста, введите телефонный номер.' }), .min(12, { error: 'register-error-phone' }),
password: z password: z
.string() .string()
.min(8, { error: 'Длина пароля должна быть не менее 8 символов.' }) .min(8, { error: 'register-error-password-symbols' })
.regex(/[a-zA-Zа-яёА-ЯЁ]/, { error: 'Пароль должен содержать как минимум 1 букву.' }) .regex(/[a-zA-Zа-яёА-ЯЁ]/, { error: 'register-error-password-one-letter'})
.regex(/[0-9]/, { error: 'Пароль должен содержать как минимум 1 цифру.' }) .regex(/[0-9]/, { error: 'register-error-password-one-digit' })
.trim(), .trim(),
confirm_password: z confirm_password: z
.string(), .string(),
agree: z agree: z
.string() .string()
.min(2, { error: 'Пожалуйста, ознакомьтесь с условиями использования и политикой конфиденциальности.' }) .min(2, { error: 'register-error-agree' })
}) })
.refine((data) => data.password === data.confirm_password, { .refine((data) => data.password === data.confirm_password, {
message: 'Повторите пароль', message: 'repeat-password',
path: ['confirm_password'], path: ['confirm_password'],
}); });
@@ -47,11 +47,11 @@ export const loginFormSchema = z
.object({ .object({
email: z email: z
.string() .string()
.min(1, { error: 'Адрес электронной почты не должен быть пустым.' }) .min(1, { error: 'login-error-email' })
.trim(), .trim(),
password: z password: z
.string() .string()
.min(1, { error: 'Пароль не должен быть пустым.' }) .min(1, { error: 'login-error-password' })
.trim(), .trim(),
}) })
+85
View File
@@ -0,0 +1,85 @@
'use client'
import { useLocale } from 'next-intl'
import { usePathname, useRouter } from '@/i18n/navigation'
import { routing } from '@/i18n/routing'
import { useState, useRef, useEffect } from 'react'
import { useClickOutside } from '@/app/hooks/useClickOutside';
export default function LanguageSwitcher() {
const locale = useLocale()
const router = useRouter()
const pathname = usePathname()
const [isOpen, setIsOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
const switchLanguage = (newLocale: string) => {
router.replace(pathname, { locale: newLocale })
setIsOpen(false)
}
useClickOutside(dropdownRef, () => {
setIsOpen(false);
});
useEffect(() => {
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') setIsOpen(false)
}
document.addEventListener('keydown', handleEscape)
return () => document.removeEventListener('keydown', handleEscape)
}, []);
return (
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center justify-between w-full px-4 py-2 text-sm font-medium text-[#6366f1] bg-white border-2 border-[#6366f1] rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
aria-haspopup="listbox"
aria-expanded={isOpen}
>
<span className="flex items-center">
<span className="mr-2">{locale === 'ru' ? 'РУ' : locale.toUpperCase()}</span>
</span>
<svg
className={`w-5 h-5 ml-2 transition-transform ${isOpen ? 'rotate-180' : ''}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
{isOpen && (
<div className="absolute right-0 z-10 w-full mt-2 origin-top-right bg-white rounded-md shadow-lg border-2 border-[#6366f1] focus:outline-none">
<ul
className="py-1"
role="listbox"
aria-labelledby="language-selector"
>
{routing.locales.map((lang) => (
<li key={lang} role="option">
<button
onClick={() => switchLanguage(lang)}
className={`flex items-center w-full px-4 py-2 text-sm text-left hover:bg-blue-100 ${locale === lang ? 'bg-blue-50 text-blue-600 font-medium' : 'text-[#6366f1]'
}`}
role="menuitem"
>
<span className="mr-2 font-medium">
{lang === 'ru' ? 'РУ' : lang.toUpperCase()}
</span>
</button>
</li>
))}
</ul>
</div>
)}
</div>
)
}
-25
View File
@@ -1,25 +0,0 @@
import Providers from './providers/getQueryServer'
import type { Metadata } from "next";
import "./styles/globals.css";
import "./styles/global-styles.scss"
export const metadata: Metadata = {
title: "No copy",
description: "No copy",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="ru">
<body className={``}>
<Providers>
{children}
</Providers>
</body>
</html>
);
}
+6
View File
@@ -15,6 +15,12 @@
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1); box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
width: 100%; width: 100%;
max-width: 500px; max-width: 500px;
&-language {
position: fixed;
right: 20px;
top: 20px;
}
} }
.logo { .logo {
+2
View File
@@ -1,11 +1,13 @@
import NotificationsButton from './notificationsButton'; import NotificationsButton from './notificationsButton';
import UserMenuButton from './userMenuButton'; import UserMenuButton from './userMenuButton';
import Link from 'next/link'; import Link from 'next/link';
import LanguageSwitcher from '@/app/components/LanguageSwitcher';
export default function HeaderPanel() { export default function HeaderPanel() {
return ( return (
<header className="header"> <header className="header">
<div className="header-action"> <div className="header-action">
<LanguageSwitcher/>
<Link <Link
href={'/pages/payment'} href={'/pages/payment'}
className="tokens-balance" className="tokens-balance"
+28 -10
View File
@@ -3,53 +3,71 @@
import styles from '@/app/styles/login.module.scss' import styles from '@/app/styles/login.module.scss'
import { useActionState } from 'react'; import { useActionState } from 'react';
import { authorization } from '@/app/actions/auth'; import { authorization } from '@/app/actions/auth';
import { useTranslations } from 'next-intl';
export default function LoginForm() { export default function LoginForm() {
const [state, formAction, isPending] = useActionState( const [state, formAction, isPending] = useActionState(
authorization, authorization,
undefined, undefined,
); );
const t = useTranslations('Login-register-form');
return ( return (
<form action={formAction}> <form action={formAction}>
<div className={`${styles['form-group']}`}> <div className={`${styles['form-group']}`}>
<label className={`${styles['form-label']}`}>Email адрес</label> <label className={`${styles['form-label']}`}>
{t('email-adress')}
</label>
<input <input
type="readOnly" type="readOnly"
id="email" id="email"
name="email" name="email"
className={`${styles['form-input']}`} className={`${styles['form-input']}`}
placeholder="Введите ваш email" placeholder={t('enter-email')}
defaultValue={state?.previousState?.email ? state?.previousState?.email : ''} defaultValue={state?.previousState?.email ? state?.previousState?.email : ''}
/> />
{state?.error?.email && ( {state?.error?.email && (
<p className={`${styles['form-error']}`}>{state?.error?.email}</p> <p className={`${styles['form-error']}`}>
{t(state?.error?.email)}
</p>
)} )}
</div> </div>
<div className={`${styles['form-group']}`}> <div className={`${styles['form-group']}`}>
<label className={`${styles['form-label']}`}>Пароль</label> <label className={`${styles['form-label']}`}>
{t('password')}
</label>
<input <input
type="password" type="password"
id="password" id="password"
name="password" name="password"
className={`${styles['form-input']}`} className={`${styles['form-input']}`}
placeholder="Введите пароль" placeholder={t('enter-password')}
/> />
{state?.error?.password && ( {state?.error?.password && (
<p className={`${styles['form-error']}`}>{state?.error?.password}</p> <p className={`${styles['form-error']}`}>
{t(state?.error?.password)}
</p>
)} )}
</div> </div>
{state?.error.server && ( {state?.error.server && (
<p className={`${styles['form-error']}`}>{state?.error.server}</p> <p className={`${styles['form-error']}`}>
{t(state?.error.server)}
</p>
)} )}
<div className={`${styles['form-options']}`}> <div className={`${styles['form-options']}`}>
<div className={`${styles['form-checkbox']}`}> <div className={`${styles['form-checkbox']}`}>
<input type="checkbox" id="remember" name="remember" /> <input type="checkbox" id="remember" name="remember" />
<label>Запомнить меня</label> <label>
{t('remember-me')}
</label>
</div> </div>
<a href="forgot-password" className={`${styles['forgot-password']}`} >Забыли пароль?</a> <a href="forgot-password" className={`${styles['forgot-password']}`} >
{t('forgot-password')}?
</a>
</div> </div>
<button type="submit" className={`${styles['btn']}`}>Войти в систему</button> <button type="submit" className={`${styles['btn']}`}>
{t('sign-in')}
</button>
</form> </form>
) )
} }
+51 -21
View File
@@ -5,12 +5,14 @@ import { useActionState } from 'react';
import { registration } from '@/app/actions/auth'; import { registration } from '@/app/actions/auth';
import PhoneInput from '@/app/ui/inputs/phone-input'; import PhoneInput from '@/app/ui/inputs/phone-input';
import Link from 'next/link'; import Link from 'next/link';
import { useTranslations } from 'next-intl';
export default function RegisterForm() { export default function RegisterForm() {
const [state, formAction, isPending] = useActionState( const [state, formAction, isPending] = useActionState(
registration, registration,
undefined, undefined,
); );
const t = useTranslations('Login-register-form');
return ( return (
<form action={(e) => { <form action={(e) => {
@@ -20,23 +22,27 @@ export default function RegisterForm() {
<label <label
className={`${styles['form-label']} ${styles['required']}`} className={`${styles['form-label']} ${styles['required']}`}
> >
Полное имя {t('full-name')}
</label> </label>
<input <input
type="text" type="text"
id="full_name" id="full_name"
name="full_name" name="full_name"
className={`${styles['form-input']}`} className={`${styles['form-input']}`}
placeholder="Иванов Иван Иванович" placeholder={t('name-placeholder')}
defaultValue={state?.previousState?.full_name ? state?.previousState?.full_name : ''} defaultValue={state?.previousState?.full_name ? state?.previousState?.full_name : ''}
/> />
{state?.error?.full_name && ( {state?.error?.full_name && (
<p className="text-sm text-red-500">{state?.error?.full_name}</p> <p className="text-sm text-red-500">
{t(state?.error?.full_name)}
</p>
)} )}
</div> </div>
<div className={`${styles['form-row']}`}> <div className={`${styles['form-row']}`}>
<div className={`${styles['form-group']}`}> <div className={`${styles['form-group']}`}>
<label className={`${styles['form-label']} ${styles['required']}`}>Email адрес</label> <label className={`${styles['form-label']} ${styles['required']}`}>
{t('email-adress')}
</label>
<input <input
type="text" type="text"
id="email" id="email"
@@ -46,14 +52,20 @@ export default function RegisterForm() {
defaultValue={state?.previousState?.email ? state?.previousState?.email : ''} defaultValue={state?.previousState?.email ? state?.previousState?.email : ''}
/> />
{state?.error?.email && ( {state?.error?.email && (
<p className="text-sm text-red-500">{state?.error?.email}</p> <p className="text-sm text-red-500">
{t(state?.error?.email)}
</p>
)} )}
</div> </div>
<div className={`${styles['form-group']}`}> <div className={`${styles['form-group']}`}>
<label className={`${styles['form-label']} ${styles['required']}`}>Телефон</label> <label className={`${styles['form-label']} ${styles['required']}`}>
{t('phone')}
</label>
<PhoneInput phoneState={state?.previousState?.phone} /> <PhoneInput phoneState={state?.previousState?.phone} />
{state?.error?.phone && ( {state?.error?.phone && (
<p className="text-sm text-red-500">{state?.error?.phone}</p> <p className="text-sm text-red-500">
{t(state?.error?.phone)}
</p>
)} )}
</div> </div>
</div> </div>
@@ -61,43 +73,55 @@ export default function RegisterForm() {
<label <label
className={`${styles['form-label']}`} className={`${styles['form-label']}`}
> >
Компания {t('company')}
</label> </label>
<input <input
type="text" type="text"
id="company" id="company"
name="company" name="company"
className={`${styles['form-input']}`} className={`${styles['form-input']}`}
placeholder="ООО «Ваша компания»" placeholder={t('company-placeholder')}
defaultValue={state?.previousState?.company ? state?.previousState?.company : ''} defaultValue={state?.previousState?.company ? state?.previousState?.company : ''}
/> />
</div> </div>
<div className={`${styles['form-row']}`}> <div className={`${styles['form-row']}`}>
<div className={`${styles['form-group']}`}> <div className={`${styles['form-group']}`}>
<label className={`${styles['form-label']} ${styles['required']}`}>Пароль</label> <label className={`${styles['form-label']} ${styles['required']}`}>
{t('password')}
</label>
<input <input
type="password" type="password"
id="password" name="password" id="password" name="password"
className={`${styles['form-input']}`} className={`${styles['form-input']}`}
placeholder="Минимум 8 символов" placeholder={t('password-placeholder')}
defaultValue={state?.previousState?.password ? state?.previousState?.password : ''} defaultValue={state?.previousState?.password ? state?.previousState?.password : ''}
/> />
{state?.error?.password && ( {state?.error?.password && (
<p className="text-sm text-red-500">{state?.error?.password}</p> <p className="text-sm text-red-500">
{
state?.error?.password.split('&').map((e) => {
return t(e)
})
}
</p>
)} )}
</div> </div>
<div className={`${styles['form-group']}`}> <div className={`${styles['form-group']}`}>
<label className={`${styles['form-label']} ${styles['required']}`}>Подтвердите пароль</label> <label className={`${styles['form-label']} ${styles['required']}`}>
{t('confirm-password')}
</label>
<input <input
type="password" type="password"
id="confirm_password" id="confirm_password"
name="confirm_password" name="confirm_password"
className={`${styles['form-input']}`} className={`${styles['form-input']}`}
placeholder="Повторите пароль" placeholder={t('repeat-password')}
defaultValue={state?.previousState?.confirm_password ? state?.previousState?.confirm_password : ''} defaultValue={state?.previousState?.confirm_password ? state?.previousState?.confirm_password : ''}
/> />
{state?.error?.confirm_password && ( {state?.error?.confirm_password && (
<p className="text-sm text-red-500">{state?.error?.confirm_password}</p> <p className="text-sm text-red-500">
{t(state?.error?.confirm_password)}
</p>
)} )}
</div> </div>
</div> </div>
@@ -108,22 +132,28 @@ export default function RegisterForm() {
name="agree" name="agree"
defaultChecked={state?.previousState?.agree ? true : false} defaultChecked={state?.previousState?.agree ? true : false}
/> />
<label htmlFor="agree">Я соглашаюсь с <label htmlFor="agree">
<Link {t('i-agree-to')} <Link
href="/terms-and-privacy-policy" href="/terms-and-privacy-policy"
target="_blank" target="_blank"
> >
условиями использования и политикой конфиденциальности {t('terms-of-use')}
</Link> </Link>
</label> </label>
</div> </div>
{state?.error?.agree && ( {state?.error?.agree && (
<p className="text-sm text-red-500">{state?.error?.agree}</p> <p className="text-sm text-red-500">
{t(state?.error?.agree)}
</p>
)} )}
{state?.error?.server && ( {state?.error?.server && (
<p className="text-sm text-red-500">{state?.error?.server}</p> <p className="text-sm text-red-500">
{t(state?.error?.server)}
</p>
)} )}
<button type="submit" className={`${styles['btn']}`}>Создать аккаунт</button> <button type="submit" className={`${styles['btn']}`}>
{t('create-an-account')}
</button>
</form > </form >
) )
} }
+51
View File
@@ -0,0 +1,51 @@
{
"HomePage": {
"title": "Welcome to our application",
"description": "This is a demo application with i18n support"
},
"Global": {
"statistics": "Statistik"
},
"Login-register-form": {
"already-have-an-account": "Already have an account",
"company": "Company",
"company-placeholder": "Your company",
"confirm-password": "Confirm password",
"copyright-protection": "Copyright protection",
"create-an-account": "Create an account",
"email-adress": "Email adress",
"enter-email": "Enter your email",
"enter-password": "Enter your password",
"forgot-password": "Forgot your password",
"free-start": "Free start",
"free-start-description": "Upon registration, you receive a \"DEMO\" subscription with the ability to tag up to 3 files per month and 10 MB of storage.",
"full-name": "Full name",
"i-agree-to": "I agree to the",
"login": "Login",
"login-error-email": "Email address must not be empty.",
"login-error-password": "The password must not be empty.",
"monitoring-violations": "Real-time monitoring of violations",
"name-placeholder": "Ivanov Ivan Ivanovich",
"no-account": "No account",
"or-log-in-via": "or log in via",
"or-register-via": "or register via",
"password": "Password",
"password-placeholder": "Minimum 8 characters",
"phone": "Phone",
"protecting-watermark": "Protecting content with hidden watermarks",
"register": "Register",
"register-error-agree": "Please review the terms of use and privacy policy.",
"register-error-name": "Name must be at least 3 characters long.",
"register-error-password-one-digit": "Password must contain at least 1 digit.",
"register-error-password-one-letter": "Password must contain at least 1 letter.",
"register-error-password-symbols": "Password must be at least 8 characters long.",
"register-error-phone": "Please enter a phone number.",
"register-error-valid-email": "Please enter a valid email address.",
"remember-me": "Remember me",
"repeat-password": "Repeat password",
"request-ended-with-an-error": "The request ended with an error",
"safe-storage": "Reliable storage and data security",
"sign-in": "Sign in",
"terms-of-use": "terms of use and privacy policy"
}
}
+51
View File
@@ -0,0 +1,51 @@
{
"HomePage": {
"title": "Добро пожаловать в наше приложение",
"description": "Это демонстрационное приложение с поддержкой i18n"
},
"Global": {
"statistics": "Статистика"
},
"Login-register-form": {
"already-have-an-account": "Уже есть аккаунт",
"company": "Компания",
"company-placeholder": "ООО «Ваша компания»",
"confirm-password": "Подтвердите пароль",
"copyright-protection": "Защита авторских прав",
"create-an-account": "Создать аккаунт",
"email-adress": "Email адрес",
"enter-email": "Введите ваш email",
"enter-password": "Введите пароль",
"forgot-password": "Забыли пароль",
"free-start": "Бесплатный старт",
"free-start-description": "При регистрации вы получаете подписку \"ДЕМО\" с возможностью маркировки до 3 файлов в месяц и 10 МБ хранилища.",
"full-name": "Полное имя",
"i-agree-to": "Я соглашаюсь с",
"login": "Войти",
"login-error-email": "Адрес электронной почты не должен быть пустым.",
"login-error-password": "Пароль не должен быть пустым.",
"monitoring-violations": "Мониторинг нарушений в реальном времени",
"name-placeholder": "Иванов Иван Иванович",
"no-account": "Нет аккаунта",
"or-log-in-via": "или войти через",
"or-register-via": "или зарегистрироваться через",
"password": "Пароль",
"password-placeholder": "Минимум 8 символов",
"phone": "Телефон",
"protecting-watermark": "Защита контента скрытыми водяными знаками",
"register": "Зарегистрироваться",
"register-error-agree": "Пожалуйста, ознакомьтесь с условиями использования и политикой конфиденциальности.",
"register-error-name": "Имя должно содержать не менее 3 символов.",
"register-error-password-one-digit": "Пароль должен содержать как минимум 1 цифру.",
"register-error-password-one-letter": "Пароль должен содержать как минимум 1 букву.",
"register-error-password-symbols": "Длина пароля должна быть не менее 8 символов.",
"register-error-phone": "Пожалуйста, введите телефонный номер.",
"register-error-valid-email": "Пожалуйста, введите правильный адрес электронной почты.",
"remember-me": "Запомнить меня",
"repeat-password": "Повторите пароль",
"request-ended-with-an-error": "Запрос завершился с ошибкой",
"safe-storage": "Надежное хранение и безопасность данных",
"sign-in": "Войти в систему",
"terms-of-use": "условиями использования и политикой конфиденциальности"
}
}
+5
View File
@@ -0,0 +1,5 @@
import { createNavigation } from 'next-intl/navigation';
import { routing } from './routing';
export const { Link, redirect, usePathname, useRouter, getPathname } =
createNavigation(routing);
+15
View File
@@ -0,0 +1,15 @@
import { getRequestConfig } from 'next-intl/server';
import { hasLocale } from 'next-intl';
import { routing } from './routing';
export default getRequestConfig(async ({ requestLocale }) => {
const requested = await requestLocale;
const locale = hasLocale(routing.locales, requested)
? requested
: routing.defaultLocale;
return {
locale,
messages: (await import(`./messages/${locale}.json`)).default
};
});
+6
View File
@@ -0,0 +1,6 @@
import { defineRouting } from 'next-intl/routing';
export const routing = defineRouting({
locales: ['ru', 'en'],
defaultLocale: 'ru'
});
+41 -7
View File
@@ -1,24 +1,58 @@
import { NextResponse, NextRequest } from 'next/server'; import { NextResponse, NextRequest } from 'next/server';
import { cookies } from 'next/headers' import { cookies } from 'next/headers'
import { decrypt } from '@/app/actions/session'; import { decrypt } from '@/app/actions/session';
import createIntlMiddleware from 'next-intl/middleware'
import { routing } from '@/i18n/routing'
const protectedRoutes = ['/pages'] const protectedRoutes = ['/pages'];
const publicRoutes = ['/login', '/register', '/'] const publicRoutes = ['/login', '/register', '/'];
const intlMiddleware = createIntlMiddleware(routing);
export default async function proxy(request: NextRequest) { export default async function proxy(request: NextRequest) {
const path = request.nextUrl.pathname; const path = request.nextUrl.pathname;
const isProtectedRoute = protectedRoutes.includes(path.slice(0, 6));
const isPublicRoute = publicRoutes.includes(path); if (
path.startsWith('/_next') ||
path.includes('/api/') ||
path.includes('.')
) {
return NextResponse.next()
}
const intlResponse = intlMiddleware(request);
if (intlResponse) {
const localizedPath = intlResponse.headers.get('x-middleware-request') ||
intlResponse.headers.get('x-middleware-next') ||
path
const isProtectedRoute = protectedRoutes.some(route =>
localizedPath.includes(route) ||
path.includes(route)
)
const isPublicRoute = publicRoutes.some(route =>
localizedPath === `/${request.nextUrl.locale}${route}` ||
localizedPath === route
)
const cookie = (await cookies()).get('session')?.value; const cookie = (await cookies()).get('session')?.value;
const session = await decrypt(cookie); const session = await decrypt(cookie);
if (isProtectedRoute && !session?.token) { if (isProtectedRoute && !session?.token) {
return NextResponse.redirect(new URL('/login', request.nextUrl)); const loginUrl = new URL('/login', request.nextUrl)
loginUrl.searchParams.set('locale', request.nextUrl.locale || 'ru')
return NextResponse.redirect(loginUrl)
} }
if (path === '/') { if (path === '/en' || path === '/ru' || localizedPath === `/${request.nextUrl.locale}`) {
return NextResponse.redirect(new URL('/pages/dashboard', request.nextUrl)); const dashboardUrl = new URL('/pages/dashboard', request.nextUrl)
dashboardUrl.searchParams.set('locale', request.nextUrl.locale || 'ru')
return NextResponse.redirect(dashboardUrl)
}
return intlResponse
} }
return NextResponse.next(); return NextResponse.next();