From 4b4dcb1ff562255d052a2dda5f4ac1aeefc0e726 Mon Sep 17 00:00:00 2001 From: cfngc4594 Date: Sun, 4 May 2025 21:26:41 +0800 Subject: [PATCH 01/58] chore(deps): add pino and pino-pretty packages --- bun.lock | 42 ++++++++++++++++++++++++++++++++++++++++++ package.json | 2 ++ 2 files changed, 44 insertions(+) diff --git a/bun.lock b/bun.lock index b6f3c98..d530a4e 100644 --- a/bun.lock +++ b/bun.lock @@ -50,6 +50,7 @@ "next-mdx-remote": "^5.0.0", "next-themes": "^0.4.6", "normalize-url": "^8.0.1", + "pino": "^9.6.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.54.2", @@ -82,6 +83,7 @@ "@types/tar-stream": "^3.1.3", "eslint": "^9", "eslint-config-next": "15.1.7", + "pino-pretty": "^13.0.0", "postcss": "^8", "postcss-github-markdown-css": "^0.0.3", "prisma": "^6.6.0", @@ -598,6 +600,8 @@ "async-function": ["async-function@1.0.0", "https://registry.npmmirror.com/async-function/-/async-function-1.0.0.tgz", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + "atomic-sleep": ["atomic-sleep@1.0.0", "https://registry.npmmirror.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], + "available-typed-arrays": ["available-typed-arrays@1.0.7", "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], "axe-core": ["axe-core@4.10.3", "https://registry.npmmirror.com/axe-core/-/axe-core-4.10.3.tgz", {}, "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg=="], @@ -684,6 +688,8 @@ "color-support": ["color-support@1.1.3", "https://registry.npmmirror.com/color-support/-/color-support-1.1.3.tgz", { "bin": { "color-support": "bin.js" } }, "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="], + "colorette": ["colorette@2.0.20", "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "https://registry.npmmirror.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], "commander": ["commander@4.1.1", "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], @@ -720,6 +726,8 @@ "date-fns": ["date-fns@4.1.0", "https://registry.npmmirror.com/date-fns/-/date-fns-4.1.0.tgz", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], + "dateformat": ["dateformat@4.6.3", "https://registry.npmmirror.com/dateformat/-/dateformat-4.6.3.tgz", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], + "debug": ["debug@4.4.0", "https://registry.npmmirror.com/debug/-/debug-4.4.0.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "decimal.js": ["decimal.js@10.5.0", "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.5.0.tgz", {}, "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw=="], @@ -860,6 +868,8 @@ "extend": ["extend@3.0.2", "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + "fast-copy": ["fast-copy@3.0.2", "https://registry.npmmirror.com/fast-copy/-/fast-copy-3.0.2.tgz", {}, "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-fifo": ["fast-fifo@1.3.2", "https://registry.npmmirror.com/fast-fifo/-/fast-fifo-1.3.2.tgz", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], @@ -870,6 +880,10 @@ "fast-levenshtein": ["fast-levenshtein@2.0.6", "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fast-redact": ["fast-redact@3.5.0", "https://registry.npmmirror.com/fast-redact/-/fast-redact-3.5.0.tgz", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="], + + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "https://registry.npmmirror.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], + "fastq": ["fastq@1.19.1", "https://registry.npmmirror.com/fastq/-/fastq-1.19.1.tgz", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], "fdir": ["fdir@6.4.3", "https://registry.npmmirror.com/fdir/-/fdir-6.4.3.tgz", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="], @@ -980,6 +994,8 @@ "hastscript": ["hastscript@9.0.1", "https://registry.npmmirror.com/hastscript/-/hastscript-9.0.1.tgz", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], + "help-me": ["help-me@5.0.0", "https://registry.npmmirror.com/help-me/-/help-me-5.0.0.tgz", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], + "html-void-elements": ["html-void-elements@3.0.0", "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-3.0.0.tgz", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], "https-proxy-agent": ["https-proxy-agent@5.0.1", "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], @@ -1080,6 +1096,8 @@ "jose": ["jose@6.0.9", "https://registry.npmmirror.com/jose/-/jose-6.0.9.tgz", {}, "sha512-6HEy/G3IBiGwOeT0phvu19yt/zagFKSpQPpQ6YUIiCxBUPfThVkOv9wlwHGkatUqbHvkWHYPtJJil4U5jHwllw=="], + "joycon": ["joycon@3.1.1", "https://registry.npmmirror.com/joycon/-/joycon-3.1.1.tgz", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + "js-tokens": ["js-tokens@4.0.0", "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@4.1.0", "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], @@ -1326,6 +1344,8 @@ "object.values": ["object.values@1.2.1", "https://registry.npmmirror.com/object.values/-/object.values-1.2.1.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "https://registry.npmmirror.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], + "once": ["once@1.4.0", "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], "oniguruma-parser": ["oniguruma-parser@0.5.4", "https://registry.npmmirror.com/oniguruma-parser/-/oniguruma-parser-0.5.4.tgz", {}, "sha512-yNxcQ8sKvURiTwP0mV6bLQCYE7NKfKRRWunhbZnXgxSmB1OXa1lHrN3o4DZd+0Si0kU5blidK7BcROO8qv5TZA=="], @@ -1366,6 +1386,14 @@ "pify": ["pify@2.3.0", "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + "pino": ["pino@9.6.0", "https://registry.npmmirror.com/pino/-/pino-9.6.0.tgz", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^4.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg=="], + + "pino-abstract-transport": ["pino-abstract-transport@2.0.0", "https://registry.npmmirror.com/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="], + + "pino-pretty": ["pino-pretty@13.0.0", "https://registry.npmmirror.com/pino-pretty/-/pino-pretty-13.0.0.tgz", { "dependencies": { "colorette": "^2.0.7", "dateformat": "^4.6.3", "fast-copy": "^3.0.2", "fast-safe-stringify": "^2.1.1", "help-me": "^5.0.0", "joycon": "^3.1.1", "minimist": "^1.2.6", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pump": "^3.0.0", "secure-json-parse": "^2.4.0", "sonic-boom": "^4.0.1", "strip-json-comments": "^3.1.1" }, "bin": { "pino-pretty": "bin.js" } }, "sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA=="], + + "pino-std-serializers": ["pino-std-serializers@7.0.0", "https://registry.npmmirror.com/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", {}, "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="], + "pirates": ["pirates@4.0.6", "https://registry.npmmirror.com/pirates/-/pirates-4.0.6.tgz", {}, "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="], "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], @@ -1396,6 +1424,8 @@ "prisma": ["prisma@6.6.0", "https://registry.npmmirror.com/prisma/-/prisma-6.6.0.tgz", { "dependencies": { "@prisma/config": "6.6.0", "@prisma/engines": "6.6.0" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-SYCUykz+1cnl6Ugd8VUvtTQq5+j1Q7C0CtzKPjQ8JyA2ALh0EEJkMCS+KgdnvKW1lrxjtjCyJSHOOT236mENYg=="], + "process-warning": ["process-warning@4.0.1", "https://registry.npmmirror.com/process-warning/-/process-warning-4.0.1.tgz", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="], + "prop-types": ["prop-types@15.8.1", "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], "property-information": ["property-information@7.0.0", "https://registry.npmmirror.com/property-information/-/property-information-7.0.0.tgz", {}, "sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg=="], @@ -1408,6 +1438,8 @@ "queue-microtask": ["queue-microtask@1.2.3", "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "https://registry.npmmirror.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], + "react": ["react@19.0.0", "https://registry.npmmirror.com/react/-/react-19.0.0.tgz", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="], "react-dom": ["react-dom@19.0.0", "https://registry.npmmirror.com/react-dom/-/react-dom-19.0.0.tgz", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="], @@ -1432,6 +1464,8 @@ "readdirp": ["readdirp@3.6.0", "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + "real-require": ["real-require@0.2.0", "https://registry.npmmirror.com/real-require/-/real-require-0.2.0.tgz", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], + "recma-build-jsx": ["recma-build-jsx@1.0.0", "https://registry.npmmirror.com/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], "recma-jsx": ["recma-jsx@1.0.0", "https://registry.npmmirror.com/recma-jsx/-/recma-jsx-1.0.0.tgz", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" } }, "sha512-5vwkv65qWwYxg+Atz95acp8DMu1JDSqdGkA2Of1j6rCreyFUE/gp15fC8MnGEuG1W68UKjM6x6+YTWIh7hZM/Q=="], @@ -1496,6 +1530,8 @@ "safe-regex-test": ["safe-regex-test@1.1.0", "https://registry.npmmirror.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "https://registry.npmmirror.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + "safer-buffer": ["safer-buffer@2.1.2", "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "scheduler": ["scheduler@0.25.0", "https://registry.npmmirror.com/scheduler/-/scheduler-0.25.0.tgz", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="], @@ -1534,6 +1570,8 @@ "simple-swizzle": ["simple-swizzle@0.2.2", "https://registry.npmmirror.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + "sonic-boom": ["sonic-boom@4.2.0", "https://registry.npmmirror.com/sonic-boom/-/sonic-boom-4.2.0.tgz", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], + "sonner": ["sonner@2.0.1", "https://registry.npmmirror.com/sonner/-/sonner-2.0.1.tgz", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-FRBphaehZ5tLdLcQ8g2WOIRE+Y7BCfWi5Zyd8bCvBjiW8TxxAyoWZIxS661Yz6TGPqFQ4VLzOF89WEYhfynSFQ=="], "source-map": ["source-map@0.7.4", "https://registry.npmmirror.com/source-map/-/source-map-0.7.4.tgz", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="], @@ -1544,6 +1582,8 @@ "split-ca": ["split-ca@1.0.1", "https://registry.npmmirror.com/split-ca/-/split-ca-1.0.1.tgz", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="], + "split2": ["split2@4.2.0", "https://registry.npmmirror.com/split2/-/split2-4.2.0.tgz", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + "ssh2": ["ssh2@1.16.0", "https://registry.npmmirror.com/ssh2/-/ssh2-1.16.0.tgz", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.20.0" } }, "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg=="], "stable-hash": ["stable-hash@0.0.4", "https://registry.npmmirror.com/stable-hash/-/stable-hash-0.0.4.tgz", {}, "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g=="], @@ -1620,6 +1660,8 @@ "thenify-all": ["thenify-all@1.6.0", "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + "thread-stream": ["thread-stream@3.1.0", "https://registry.npmmirror.com/thread-stream/-/thread-stream-3.1.0.tgz", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="], + "throttleit": ["throttleit@2.1.0", "https://registry.npmmirror.com/throttleit/-/throttleit-2.1.0.tgz", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="], "tinyglobby": ["tinyglobby@0.2.12", "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.12.tgz", { "dependencies": { "fdir": "^6.4.3", "picomatch": "^4.0.2" } }, "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww=="], diff --git a/package.json b/package.json index d81ce8f..7bcb4ff 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "next-mdx-remote": "^5.0.0", "next-themes": "^0.4.6", "normalize-url": "^8.0.1", + "pino": "^9.6.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.54.2", @@ -91,6 +92,7 @@ "@types/tar-stream": "^3.1.3", "eslint": "^9", "eslint-config-next": "15.1.7", + "pino-pretty": "^13.0.0", "postcss": "^8", "postcss-github-markdown-css": "^0.0.3", "prisma": "^6.6.0", From 613dd5ef172cd3f4ec263ff0b14b1e0024872331 Mon Sep 17 00:00:00 2001 From: cfngc4594 Date: Sun, 4 May 2025 21:35:44 +0800 Subject: [PATCH 02/58] feat(log): add pino logging support --- next.config.ts | 2 +- src/lib/logger.ts | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 src/lib/logger.ts diff --git a/next.config.ts b/next.config.ts index f3b403a..7bdf5d2 100644 --- a/next.config.ts +++ b/next.config.ts @@ -3,7 +3,7 @@ import createNextIntlPlugin from 'next-intl/plugin'; const nextConfig: NextConfig = { output: "standalone", - serverExternalPackages: ["dockerode"], + serverExternalPackages: ["dockerode", "pino", "pino-pretty"], }; const withNextIntl = createNextIntlPlugin(); diff --git a/src/lib/logger.ts b/src/lib/logger.ts new file mode 100644 index 0000000..398702f --- /dev/null +++ b/src/lib/logger.ts @@ -0,0 +1,21 @@ +import pino from "pino"; + +const logger = + process.env["NODE_ENV"] === "production" + ? // JSON in production + pino({ level: "info" }) + : // Pretty print in development + pino({ + level: "debug", + transport: { + target: "pino-pretty", + options: { + levelFirst: true, + colorize: true, + ignore: "hostname,pid", + translateTime: "yyyy-mm-dd HH:MM:ss", + }, + }, + }); + +export { logger }; From e42aa935be13d42fe5dd68cb03d0d62d3103957a Mon Sep 17 00:00:00 2001 From: cfngc4594 Date: Mon, 5 May 2025 09:23:55 +0800 Subject: [PATCH 03/58] chore(tailwind): update config with features path and animate import --- tailwind.config.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tailwind.config.ts b/tailwind.config.ts index d0c5701..9327e9f 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,10 +1,12 @@ import type { Config } from "tailwindcss"; +import animate from "tailwindcss-animate"; export default { darkMode: ["class"], content: [ "./src/app/**/*.{js,ts,jsx,tsx,mdx}", "./src/components/**/*.{js,ts,jsx,tsx,mdx}", + "./src/features/**/*.{js,ts,jsx,tsx,mdx}", "./src/hooks/**/*.{js,ts,jsx,tsx,mdx}", "./src/lib/**/*.{js,ts,jsx,tsx,mdx}", ], @@ -91,5 +93,5 @@ export default { }, }, }, - plugins: [require("tailwindcss-animate")], + plugins: [animate], } satisfies Config; From 7db089dd4c5966ba3453d46c90d6b89a0fc46f59 Mon Sep 17 00:00:00 2001 From: cfngc4594 Date: Mon, 5 May 2025 14:12:41 +0800 Subject: [PATCH 04/58] chore(deps): add react-icons package --- bun.lock | 3 +++ package.json | 1 + 2 files changed, 4 insertions(+) diff --git a/bun.lock b/bun.lock index d530a4e..127ae62 100644 --- a/bun.lock +++ b/bun.lock @@ -54,6 +54,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.54.2", + "react-icons": "^5.5.0", "react-resizable-panels": "^2.1.7", "react-world-flags": "^1.6.0", "rehype-katex": "^7.0.1", @@ -1446,6 +1447,8 @@ "react-hook-form": ["react-hook-form@7.54.2", "https://registry.npmmirror.com/react-hook-form/-/react-hook-form-7.54.2.tgz", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg=="], + "react-icons": ["react-icons@5.5.0", "https://registry.npmmirror.com/react-icons/-/react-icons-5.5.0.tgz", { "peerDependencies": { "react": "*" } }, "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw=="], + "react-is": ["react-is@16.13.1", "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "react-remove-scroll": ["react-remove-scroll@2.6.3", "https://registry.npmmirror.com/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ=="], diff --git a/package.json b/package.json index 7bcb4ff..c2194d8 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.54.2", + "react-icons": "^5.5.0", "react-resizable-panels": "^2.1.7", "react-world-flags": "^1.6.0", "rehype-katex": "^7.0.1", From 1f21cad4d18890a40c67e5d7e6a77298063eaec9 Mon Sep 17 00:00:00 2001 From: cfngc4594 Date: Mon, 5 May 2025 17:05:01 +0800 Subject: [PATCH 05/58] refactor(auth)!: replace credentials with OAuth providers and add logging BREAKING CHANGE: - Removed credentials-based authentication - Added Google OAuth provider - Implemented detailed logging for auth events - Removed custom JWT/session handling - Added sign-in page configuration - Marked as server-only --- src/lib/auth.ts | 229 ++++++++++++++++++++++++++++++------------------ 1 file changed, 146 insertions(+), 83 deletions(-) diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 24a8cba..7ace576 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,102 +1,165 @@ -import bcrypt from "bcrypt"; -import { ZodError } from "zod"; +import "server-only"; + import NextAuth from "next-auth"; import prisma from "@/lib/prisma"; -import { v4 as uuid } from "uuid"; -import { authSchema } from "@/lib/zod"; -import { encode } from "next-auth/jwt"; +import { logger } from "@/lib/logger"; import GitHub from "next-auth/providers/github"; +import Google from "next-auth/providers/google"; +import type { Provider } from "next-auth/providers"; import { PrismaAdapter } from "@auth/prisma-adapter"; -import Credentials from "next-auth/providers/credentials"; + +const log = logger.child({ module: "auth" }); const adapter = PrismaAdapter(prisma); -// Constant for session expiry time (30 days) -const SESSION_EXPIRY_MS = 30 * 24 * 60 * 60 * 1000; // 30 days in milliseconds +const providers: Provider[] = [ + GitHub({ allowDangerousEmailAccountLinking: true }), + Google({ allowDangerousEmailAccountLinking: true }), +]; -// Helper function to create a session and return the session token -const createSession = async (userId: string) => { - const sessionToken = uuid(); - const createdSession = await adapter?.createSession?.({ - sessionToken, - userId, - expires: new Date(Date.now() + SESSION_EXPIRY_MS), - }); - - if (!createdSession) { - throw new Error("Failed to create session"); +export const providerMap = providers.map((provider) => { + if (typeof provider === "function") { + const providerData = provider(); + return { id: providerData.id, name: providerData.name }; } - - return sessionToken; -}; + return { id: provider.id, name: provider.name }; +}); // NextAuth configuration export const { auth, handlers, signIn, signOut } = NextAuth({ adapter, - providers: [ - GitHub({ - allowDangerousEmailAccountLinking: true, - }), - Credentials({ - credentials: { - email: {}, - password: {}, - }, - authorize: async (credentials) => { - try { - // Parse credentials using authSchema for validation - const { email, password } = await authSchema.parseAsync(credentials); - - // Find user by email - const user = await prisma.user.findUnique({ - where: { email }, - }); - - // Check if the user exists and validate password - if (!user || !user.password || !(await bcrypt.compare(password, user.password))) { - throw new Error("Invalid credentials."); - } - - // Return the user object if credentials are valid - return user; - } catch (error) { - if (error instanceof ZodError) { - // Return null if validation fails - return null; - } - console.error(error); // Log other errors for debugging - return null; - } - }, - }), - ], - callbacks: { - async jwt({ token, account }) { - if (account?.provider === "credentials") { - token.credentials = true; // Add flag to token for credentials provider - } - return token; - }, - }, - jwt: { - encode: async function (params) { - if (params.token?.credentials) { - if (!params.token?.sub) { - throw new Error("No user ID found in token"); - } - return await createSession(params.token.sub); // Create session for the user and return session token - } - return encode(params); // Default encoding for JWT - }, + providers, + pages: { + signIn: "/sign-in", }, events: { async createUser({ user }) { - const count = await prisma.user.count(); - if (count === 1) { - await prisma.user.update({ - where: { id: user.id }, - data: { role: "ADMIN" }, - }); + const startTime = Date.now(); + log.debug({ user }, "Creating new user"); + + try { + const count = await prisma.user.count(); + log.debug({ count }, "Total user count"); + + if (count === 1) { + log.debug("First user detected, assigning ADMIN role"); + await prisma.user.update({ + where: { id: user.id }, + data: { role: "ADMIN" }, + }); + log.info( + { userId: user.id, durationMs: Date.now() - startTime }, + "User created and assigned ADMIN role", + ); + } else { + log.info( + { userId: user.id, durationMs: Date.now() - startTime }, + "User created successfully", + ); + } + } catch (error) { + log.error( + { userId: user.id, durationMs: Date.now() - startTime, error }, + "Failed to create user or assign role", + ); + throw error; + } + }, + + async linkAccount({ user, account, profile }) { + const startTime = Date.now(); + log.debug( + { userId: user.id, provider: account.provider }, + "Linking new provider account to existing user", + ); + + try { + log.info( + { + userId: user.id, + provider: account.provider, + email: profile.email, + durationMs: Date.now() - startTime, + }, + "Successfully linked provider account", + ); + } catch (error) { + log.error( + { + userId: user.id, + provider: account.provider, + durationMs: Date.now() - startTime, + error, + }, + "Failed to link provider account", + ); + throw error; + } + }, + + async signIn({ user, account, profile, isNewUser }) { + const startTime = Date.now(); + log.debug( + { userId: user.id, provider: account?.provider }, + "User attempting to sign in", + ); + + try { + log.info( + { + userId: user.id, + provider: account?.provider, + email: profile?.email, + isNewUser, + durationMs: Date.now() - startTime, + }, + "User signed in successfully", + ); + } catch (error) { + log.error( + { + userId: user.id, + provider: account?.provider, + durationMs: Date.now() - startTime, + error, + }, + "Failed to sign in", + ); + throw error; + } + }, + + async signOut(message) { + const startTime = Date.now(); + + try { + if ("session" in message) { + log.info( + { + sessionId: message.session?.sessionToken, + userId: message.session?.userId, + durationMs: Date.now() - startTime, + }, + "User signed out successfully (database session)", + ); + } else if ("token" in message) { + log.info( + { + token: message.token, + durationMs: Date.now() - startTime, + }, + "User signed out successfully (JWT session)", + ); + } + } catch (error) { + log.error( + { + durationMs: Date.now() - startTime, + error, + }, + "Failed to sign out", + ); + throw error; } }, }, From a3ef5d88e661460e58e36dbbd30c6aafbc131d04 Mon Sep 17 00:00:00 2001 From: cfngc4594 Date: Mon, 5 May 2025 18:21:08 +0800 Subject: [PATCH 06/58] refactor(auth)!: remove components and rewrite sign-in page --- src/actions/auth.ts | 78 ------------- src/app/(auth)/layout.tsx | 37 ------ src/app/(auth)/sign-in/page.tsx | 99 +++++++++++++++- src/app/(auth)/sign-up/page.tsx | 5 - src/components/credentials-sign-in-form.tsx | 121 ------------------- src/components/credentials-sign-up-form.tsx | 123 -------------------- src/components/github-sign-in-form.tsx | 27 ----- src/components/sign-in-form.tsx | 44 ------- src/components/sign-up-form.tsx | 44 ------- src/lib/zod.ts | 13 --- 10 files changed, 96 insertions(+), 495 deletions(-) delete mode 100644 src/actions/auth.ts delete mode 100644 src/app/(auth)/layout.tsx delete mode 100644 src/app/(auth)/sign-up/page.tsx delete mode 100644 src/components/credentials-sign-in-form.tsx delete mode 100644 src/components/credentials-sign-up-form.tsx delete mode 100644 src/components/github-sign-in-form.tsx delete mode 100644 src/components/sign-in-form.tsx delete mode 100644 src/components/sign-up-form.tsx delete mode 100644 src/lib/zod.ts diff --git a/src/actions/auth.ts b/src/actions/auth.ts deleted file mode 100644 index ceb5340..0000000 --- a/src/actions/auth.ts +++ /dev/null @@ -1,78 +0,0 @@ -"use server"; - -import bcrypt from "bcrypt"; -import prisma from "@/lib/prisma"; -import { signIn } from "@/lib/auth"; -import { authSchema } from "@/lib/zod"; -import { getTranslations } from "next-intl/server"; -import { CredentialsSignInFormValues } from "@/components/credentials-sign-in-form"; -import { CredentialsSignUpFormValues } from "@/components/credentials-sign-up-form"; - -const saltRounds = 10; - -export async function signInWithCredentials(formData: CredentialsSignInFormValues, redirectTo?: string) { - const t = await getTranslations("signInWithCredentials"); - - try { - // Parse credentials using authSchema for validation - const { email, password } = await authSchema.parseAsync(formData); - - // Find user by email - const user = await prisma.user.findUnique({ where: { email } }); - - // Check if the user exists - if (!user) { - throw new Error(t("userNotFound")); - } - - // Check if the user has a password - if (!user.password) { - throw new Error(t("invalidCredentials")); - } - - // Check if the password matches - const passwordMatch = await bcrypt.compare(password, user.password); - if (!passwordMatch) { - throw new Error(t("incorrectPassword")); - } - - await signIn("credentials", { ...formData, redirectTo, redirect: !!redirectTo }); - return { success: true }; - } catch (error) { - return { error: error instanceof Error ? error.message : t("signInFailedFallback") }; - } -} - -export async function signUpWithCredentials(formData: CredentialsSignUpFormValues) { - const t = await getTranslations("signUpWithCredentials"); - - try { - const validatedData = await authSchema.parseAsync(formData); - - // Check if user already exists - const existingUser = await prisma.user.findUnique({ where: { email: validatedData.email } }); - if (existingUser) { - throw new Error(t("userAlreadyExists")); - } - - // Hash password and create user - const pwHash = await bcrypt.hash(validatedData.password, saltRounds); - const user = await prisma.user.create({ - data: { email: validatedData.email, password: pwHash }, - }); - - // Assign admin role if first user - const userCount = await prisma.user.count(); - if (userCount === 1) { - await prisma.user.update({ where: { id: user.id }, data: { role: "ADMIN" } }); - } - - return { success: true }; - } catch (error) { - return { error: error instanceof Error ? error.message : t("registrationFailedFallback") }; - } -} - -export async function signInWithGithub(redirectTo?: string) { - await signIn("github", { redirectTo, redirect: !!redirectTo }); -} diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx deleted file mode 100644 index f62b2a5..0000000 --- a/src/app/(auth)/layout.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import Link from "next/link"; -import Image from "next/image"; -import { CodeIcon } from "lucide-react"; - -interface AuthLayoutProps { - children: React.ReactNode; -} - -export default async function AuthLayout({ - children -}: AuthLayoutProps) { - return ( -
-
-
- -
- -
- Judge4c - -
-
-
{children}
-
-
-
- Image -
-
- ); -} diff --git a/src/app/(auth)/sign-in/page.tsx b/src/app/(auth)/sign-in/page.tsx index f8c93d3..6293983 100644 --- a/src/app/(auth)/sign-in/page.tsx +++ b/src/app/(auth)/sign-in/page.tsx @@ -1,5 +1,98 @@ -import { SignInForm } from "@/components/sign-in-form"; +import Link from "next/link"; +import Image from "next/image"; +import { CodeIcon } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { providerMap, signIn } from "@/lib/auth"; +import { getTranslations } from "next-intl/server"; +import { FaGithub, FaGoogle } from "react-icons/fa"; -export default function SignInPage() { - return ; +interface ProviderIconProps { + providerId: string; +} + +const ProviderIcon = ({ providerId }: ProviderIconProps) => { + switch (providerId) { + case "github": + return ; + case "google": + return ; + default: + return null; + } +}; + +interface SignInPageProps { + searchParams: Promise<{ + callbackUrl: string | undefined; + }>; +} + +export default async function SignInPage({ searchParams }: SignInPageProps) { + const { callbackUrl } = await searchParams; + const t = await getTranslations("SignInForm"); + + return ( +
+
+
+ +
+ +
+ Judge4c + +
+
+
+
+
+

{t("title")}

+

+ {t("description")} +

+
+
+ + {t("or")} + +
+ {Object.values(providerMap).map((provider) => { + return ( +
{ + "use server"; + await signIn(provider.id, { + redirectTo: callbackUrl ?? "", + }); + }} + > + +
+ ); + })} +
+
+
+
+
+ Image +
+
+ ); } diff --git a/src/app/(auth)/sign-up/page.tsx b/src/app/(auth)/sign-up/page.tsx deleted file mode 100644 index b71518b..0000000 --- a/src/app/(auth)/sign-up/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { SignUpForm } from "@/components/sign-up-form"; - -export default function SignUpPage() { - return ; -} diff --git a/src/components/credentials-sign-in-form.tsx b/src/components/credentials-sign-in-form.tsx deleted file mode 100644 index 84e370d..0000000 --- a/src/components/credentials-sign-in-form.tsx +++ /dev/null @@ -1,121 +0,0 @@ -"use client"; - -import { z } from "zod"; -import { - Form, - FormField, - FormItem, - FormControl, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { toast } from "sonner"; -import { authSchema } from "@/lib/zod"; -import { useForm } from "react-hook-form"; -import { useTranslations } from "next-intl"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import { useState, useTransition } from "react"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { signInWithCredentials } from "@/actions/auth"; -import { EyeIcon, EyeOffIcon, MailIcon } from "lucide-react"; -import { useRouter, useSearchParams } from "next/navigation"; - -export type CredentialsSignInFormValues = z.infer; - -export function CredentialsSignInForm() { - const router = useRouter(); - const searchParams = useSearchParams(); - const redirectTo = searchParams.get("redirectTo"); - const t = useTranslations("CredentialsSignInForm"); - const [isPending, startTransition] = useTransition(); - const [isVisible, setIsVisible] = useState(false); - - const form = useForm({ - resolver: zodResolver(authSchema), - defaultValues: { - email: "", - password: "", - }, - }); - - const toggleVisibility = () => setIsVisible((prev) => !prev); - - const onSubmit = (data: CredentialsSignInFormValues) => { - startTransition(async () => { - const result = await signInWithCredentials(data); - - if (result?.error) { - toast.error(t("signInFailed"), { - description: result.error, - }); - } else { - toast.success(t("signInSuccess")); - router.push(redirectTo || "/"); - } - }); - }; - - return ( -
- - ( - - {t("email")} - -
- -
-
-
-
- -
- )} - /> - - ( - - {t("password")} - -
- - -
-
- -
- )} - /> - - - - - ); -} diff --git a/src/components/credentials-sign-up-form.tsx b/src/components/credentials-sign-up-form.tsx deleted file mode 100644 index af9750c..0000000 --- a/src/components/credentials-sign-up-form.tsx +++ /dev/null @@ -1,123 +0,0 @@ -"use client"; - -import { z } from "zod"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { toast } from "sonner"; -import { authSchema } from "@/lib/zod"; -import { useForm } from "react-hook-form"; -import { useTranslations } from "next-intl"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import { useState, useTransition } from "react"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { signUpWithCredentials } from "@/actions/auth"; -import { EyeIcon, EyeOffIcon, MailIcon } from "lucide-react"; -import { useRouter, useSearchParams } from "next/navigation"; - -export type CredentialsSignUpFormValues = z.infer; - -export function CredentialsSignUpForm() { - const router = useRouter(); - const searchParams = useSearchParams(); - const redirectTo = searchParams.get("redirectTo"); - const t = useTranslations("CredentialsSignUpForm"); - const [isPending, startTransition] = useTransition(); - const [isVisible, setIsVisible] = useState(false); - - const form = useForm({ - resolver: zodResolver(authSchema), - defaultValues: { - email: "", - password: "", - }, - }); - - const toggleVisibility = () => setIsVisible((prev) => !prev); - - const onSubmit = (data: CredentialsSignUpFormValues) => { - startTransition(async () => { - const result = await signUpWithCredentials(data); - - if (result?.error) { - toast.error(t("signUpFailed"), { - description: result.error, - }); - } else { - toast.success(t("signUpSuccess"), { - description: t("signUpSuccessDescription"), - }); - router.push(`/sign-in?${redirectTo}`) - } - }); - }; - - return ( -
- - ( - - {t("email")} - -
- -
-
-
-
- -
- )} - /> - - ( - - {t("password")} - -
- - -
-
- -
- )} - /> - - - - - ); -} diff --git a/src/components/github-sign-in-form.tsx b/src/components/github-sign-in-form.tsx deleted file mode 100644 index 30d2022..0000000 --- a/src/components/github-sign-in-form.tsx +++ /dev/null @@ -1,27 +0,0 @@ -"use client"; - -import { useTranslations } from "next-intl"; -import { Button } from "@/components/ui/button"; -import { signInWithGithub } from "@/actions/auth"; -import { useSearchParams } from "next/navigation"; - -export function GithubSignInForm() { - const t = useTranslations(); - const searchParams = useSearchParams(); - const redirectTo = searchParams.get("redirectTo"); - const signInAction = signInWithGithub.bind(null, redirectTo || "/"); - - return ( -
- -
- ); -} diff --git a/src/components/sign-in-form.tsx b/src/components/sign-in-form.tsx deleted file mode 100644 index db8213c..0000000 --- a/src/components/sign-in-form.tsx +++ /dev/null @@ -1,44 +0,0 @@ -"use client"; - -import { useTranslations } from "next-intl"; -import { useRouter, useSearchParams } from "next/navigation"; -import { GithubSignInForm } from "@/components/github-sign-in-form"; -import { CredentialsSignInForm } from "@/components/credentials-sign-in-form"; - -export function SignInForm() { - const router = useRouter(); - const searchParams = useSearchParams(); - const t = useTranslations("SignInForm"); - - const handleSignUp = () => { - const params = new URLSearchParams(searchParams.toString()); - router.push(`/sign-up?${params.toString()}`); - }; - - return ( -
-
-

{t("title")}

-

- {t("description")} -

-
- -
- - {t("or")} - -
- -
- {t("noAccount")}{" "} - -
-
- ); -} diff --git a/src/components/sign-up-form.tsx b/src/components/sign-up-form.tsx deleted file mode 100644 index 5a54453..0000000 --- a/src/components/sign-up-form.tsx +++ /dev/null @@ -1,44 +0,0 @@ -"use client"; - -import { useTranslations } from "next-intl"; -import { useRouter, useSearchParams } from "next/navigation"; -import { GithubSignInForm } from "@/components/github-sign-in-form"; -import { CredentialsSignUpForm } from "@/components/credentials-sign-up-form"; - -export function SignUpForm() { - const router = useRouter(); - const searchParams = useSearchParams(); - const t = useTranslations("SignUpForm"); - - const handleSignIn = () => { - const params = new URLSearchParams(searchParams.toString()); - router.push(`/sign-in?${params.toString()}`); - }; - - return ( -
-
-

{t("title")}

-

- {t("description")} -

-
- -
- - {t("or")} - -
- -
- {t("haveAccount")}{" "} - -
-
- ); -} diff --git a/src/lib/zod.ts b/src/lib/zod.ts deleted file mode 100644 index 89246d1..0000000 --- a/src/lib/zod.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { z } from "zod"; - -export const authSchema = z.object({ - email: z - .string() - .nonempty("Email is required") - .email("Invalid email"), - password: z - .string() - .nonempty("Password is required") - .min(8, "Password must be at least 8 characters") - .max(32, "Password must be less than 32 characters"), -}); From 500113fe8f9bafae277f676a3c3adf277d94d025 Mon Sep 17 00:00:00 2001 From: cfngc4594 Date: Mon, 5 May 2025 18:39:47 +0800 Subject: [PATCH 07/58] feat(user-avatar): refactor avatar component into user-avatar with improved structure --- messages/en.json | 7 +- messages/zh.json | 7 +- src/components/avatar-button.tsx | 77 -------------------- src/components/log-in-button.tsx | 26 ------- src/components/settings-button.tsx | 2 +- src/components/user-avatar.tsx | 108 +++++++++++++++++++++++++++++ 6 files changed, 117 insertions(+), 110 deletions(-) delete mode 100644 src/components/avatar-button.tsx delete mode 100644 src/components/log-in-button.tsx create mode 100644 src/components/user-avatar.tsx diff --git a/messages/en.json b/messages/en.json index 1357ffc..d1c0755 100644 --- a/messages/en.json +++ b/messages/en.json @@ -7,7 +7,7 @@ "Dark": "Dark" } }, - "AvatarButton": { + "UserAvatar": { "Settings": "Settings", "LogIn": "LogIn", "LogOut": "LogOut" @@ -109,7 +109,8 @@ "description": "Enter your email below to sign in to your account", "or": "Or", "noAccount": "Don't have an account?", - "signUp": "Sign up" + "signUp": "Sign up", + "oauth": "Sign in with {provider}" }, "signInWithCredentials": { "userNotFound": "User not found.", @@ -215,4 +216,4 @@ } } } -} \ No newline at end of file +} diff --git a/messages/zh.json b/messages/zh.json index 82ed24a..9758d5b 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -7,7 +7,7 @@ "Dark": "深色" } }, - "AvatarButton": { + "UserAvatar": { "Settings": "设置", "LogIn": "登录", "LogOut": "登出" @@ -109,7 +109,8 @@ "description": "请输入你的邮箱以登录账户", "or": "或者", "noAccount": "还没有账户?", - "signUp": "注册" + "signUp": "注册", + "oauth": "使用 {provider} 登录" }, "signInWithCredentials": { "userNotFound": "未找到用户。", @@ -215,4 +216,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/components/avatar-button.tsx b/src/components/avatar-button.tsx deleted file mode 100644 index ca9eb9c..0000000 --- a/src/components/avatar-button.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { LogOutIcon } from "lucide-react"; -import { auth, signOut } from "@/lib/auth"; -import { getTranslations } from "next-intl/server"; -import { Skeleton } from "@/components/ui/skeleton"; -import LogInButton from "@/components/log-in-button"; -import { Avatar, AvatarImage } from "@/components/ui/avatar"; -import { SettingsButton } from "@/components/settings-button"; - -const UserAvatar = ({ image, name }: { image: string; name: string }) => ( - - - - -); - -async function handleLogOut() { - "use server"; - await signOut(); -} - -export async function AvatarButton() { - const session = await auth(); - const t = await getTranslations("AvatarButton"); - const isLoggedIn = !!session?.user; - const image = session?.user?.image ?? "/shadcn.jpg"; - const name = session?.user?.name ?? "unknown"; - const email = session?.user?.email ?? "unknwon@example.com"; - - return ( - - - - - - {!isLoggedIn ? ( - - - - - ) : ( - <> - -
- -
- {name} - {email} -
-
-
- - - - - - {t("LogOut")} - - - - )} -
-
- ); -} diff --git a/src/components/log-in-button.tsx b/src/components/log-in-button.tsx deleted file mode 100644 index e7438a1..0000000 --- a/src/components/log-in-button.tsx +++ /dev/null @@ -1,26 +0,0 @@ -"use client"; - -import { LogIn } from "lucide-react"; -import { useTranslations } from "next-intl"; -import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; - -export default function LogInButton() { - const router = useRouter(); - const pathname = usePathname(); - const searchParams = useSearchParams(); - const t = useTranslations("AvatarButton"); - - const handleLogIn = () => { - const params = new URLSearchParams(searchParams.toString()); - params.set("redirectTo", pathname); - router.push(`/sign-in?${params.toString()}`); - }; - - return ( - - - {t("LogIn")} - - ); -} diff --git a/src/components/settings-button.tsx b/src/components/settings-button.tsx index 382fa6c..cb18ddc 100644 --- a/src/components/settings-button.tsx +++ b/src/components/settings-button.tsx @@ -6,7 +6,7 @@ import { useSettingsStore } from "@/stores/useSettingsStore"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; export function SettingsButton() { - const t = useTranslations("AvatarButton"); + const t = useTranslations("UserAvatar"); const { setDialogOpen } = useSettingsStore(); return ( diff --git a/src/components/user-avatar.tsx b/src/components/user-avatar.tsx new file mode 100644 index 0000000..e6c97dd --- /dev/null +++ b/src/components/user-avatar.tsx @@ -0,0 +1,108 @@ +import { cn } from "@/lib/utils"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { LogIn, LogOutIcon } from "lucide-react"; +import { auth, signIn, signOut } from "@/lib/auth"; +import { getTranslations } from "next-intl/server"; +import { Skeleton } from "@/components/ui/skeleton"; +import { SettingsButton } from "@/components/settings-button"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; + +const handleLogIn = async () => { + "use server"; + await signIn(); +}; + +const handleLogOut = async () => { + "use server"; + await signOut(); +}; + +interface UserAvatarIconProps { + image?: string | null; + name?: string | null; + className?: string; +} + +const UserAvatarIcon = ({ image, name, className }: UserAvatarIconProps) => { + return ( + + + {name?.charAt(0) ?? "U"} + + ); +}; + +interface UserProfileInfoProps { + name?: string | null; + email?: string | null; +} + +const UserProfileInfo = ({ name, email }: UserProfileInfoProps) => { + return ( +
+ {name ?? "undefined"} + {email ?? "undefined"} +
+ ); +}; + +const UserAvatar = async () => { + const session = await auth(); + const user = session?.user; + const isLoggedIn = !!user; + const t = await getTranslations("UserAvatar"); + + return ( + + + + + + {!isLoggedIn ? ( + + + + + {t("LogIn")} + + + ) : ( + <> + +
+ + +
+
+ + + + + + {t("LogOut")} + + + + )} +
+
+ ); +}; + +const UserAvatarSkeleton = () => { + return ; +}; + +export { UserAvatar, UserAvatarSkeleton }; From 3042af45753fe672545adcfe817a7ded7c186dae Mon Sep 17 00:00:00 2001 From: cfngc4594 Date: Tue, 6 May 2025 17:20:46 +0800 Subject: [PATCH 08/58] style(dockview): reduce tabs container height and center align items - Changed --dv-tabs-and-actions-container-height from 44px to 36px - Added align-items: center to .dv-tabs-container - Improved CSS formatting for better readability --- src/styles/dockview.css | 162 ++++++++++++++++++++++++++++------------ 1 file changed, 114 insertions(+), 48 deletions(-) diff --git a/src/styles/dockview.css b/src/styles/dockview.css index b2cc7b5..f2fb0c8 100644 --- a/src/styles/dockview.css +++ b/src/styles/dockview.css @@ -19,7 +19,10 @@ .dv-scrollable:hover .dv-scrollbar-horizontal, .dv-scrollable.dv-scrollable-resizing .dv-scrollbar-horizontal, .dv-scrollable.dv-scrollable-scrolling .dv-scrollbar-horizontal { - background-color: var(--dv-scrollbar-background-color, rgba(255, 255, 255, 0.25)); + background-color: var( + --dv-scrollbar-background-color, + rgba(255, 255, 255, 0.25) + ); } .dv-svg { @@ -33,7 +36,6 @@ .dockview-theme-abyss-spaced { --dv-paneview-active-outline-color: dodgerblue; --dv-tabs-and-actions-container-font-size: 13px; - --dv-tabs-and-actions-container-height: 35px; --dv-drag-over-background-color: hsl(var(--accent) / 50%); --dv-drag-over-border-color: transparent; --dv-tabs-container-scrollbar-color: #888; @@ -47,8 +49,7 @@ --dv-active-sash-transition-duration: 0.1s; --dv-active-sash-transition-delay: 0.5s; --dv-tab-font-size: 12px; - --dv-tab-margin: 0.5rem 0.25rem; - --dv-tabs-and-actions-container-height: 44px; + --dv-tabs-and-actions-container-height: 36px; --dv-border-radius: 24px; --dv-color-abyss-dark: hsl(var(--background)); --dv-color-abyss: hsl(var(--muted)); @@ -60,15 +61,25 @@ --dv-drag-over-border: 2px solid var(--dv-color-abyss-accent); --dv-group-view-background-color: var(--dv-color-abyss-dark); --dv-tabs-and-actions-container-background-color: var(--dv-color-abyss); - --dv-activegroup-visiblepanel-tab-background-color: var(--dv-color-abyss-lighter); - --dv-activegroup-hiddenpanel-tab-background-color: var(--dv-color-abyss-light); - --dv-inactivegroup-visiblepanel-tab-background-color: var(--dv-color-abyss-lighter); - --dv-inactivegroup-hiddenpanel-tab-background-color: var(--dv-color-abyss-light); + --dv-activegroup-visiblepanel-tab-background-color: var( + --dv-color-abyss-lighter + ); + --dv-activegroup-hiddenpanel-tab-background-color: var( + --dv-color-abyss-light + ); + --dv-inactivegroup-visiblepanel-tab-background-color: var( + --dv-color-abyss-lighter + ); + --dv-inactivegroup-hiddenpanel-tab-background-color: var( + --dv-color-abyss-light + ); --dv-tab-divider-color: transparent; --dv-activegroup-visiblepanel-tab-color: var(--dv-color-abyss-primary-text); --dv-activegroup-hiddenpanel-tab-color: var(--dv-color-abyss-secondary-text); --dv-inactivegroup-visiblepanel-tab-color: var(--dv-color-abyss-primary-text); - --dv-inactivegroup-hiddenpanel-tab-color: var(--dv-color-abyss-secondary-text); + --dv-inactivegroup-hiddenpanel-tab-color: var( + --dv-color-abyss-secondary-text + ); --dv-separator-border: transparent; --dv-paneview-header-border-color: rgb(51, 51, 51); --dv-active-sash-color: var(--dv-color-abyss-accent); @@ -143,7 +154,11 @@ .dv-drop-target-container .dv-drop-target-anchor { position: relative; border: var(--dv-drag-over-border); - transition: opacity var(--dv-transition-duration) ease-in, top var(--dv-transition-duration) ease-out, left var(--dv-transition-duration) ease-out, width var(--dv-transition-duration) ease-out, height var(--dv-transition-duration) ease-out; + transition: opacity var(--dv-transition-duration) ease-in, + top var(--dv-transition-duration) ease-out, + left var(--dv-transition-duration) ease-out, + width var(--dv-transition-duration) ease-out, + height var(--dv-transition-duration) ease-out; background-color: var(--dv-drag-over-background-color); opacity: 1; } @@ -153,7 +168,7 @@ --dv-transition-duration: 70ms; } -.dv-drop-target>.dv-drop-target-dropzone { +.dv-drop-target > .dv-drop-target-dropzone { position: absolute; left: 0px; top: 0px; @@ -163,31 +178,43 @@ pointer-events: none; } -.dv-drop-target>.dv-drop-target-dropzone>.dv-drop-target-selection { +.dv-drop-target > .dv-drop-target-dropzone > .dv-drop-target-selection { position: relative; box-sizing: border-box; height: 100%; width: 100%; border: var(--dv-drag-over-border); background-color: var(--dv-drag-over-background-color); - transition: top var(--dv-transition-duration) ease-out, left var(--dv-transition-duration) ease-out, width var(--dv-transition-duration) ease-out, height var(--dv-transition-duration) ease-out, opacity var(--dv-transition-duration) ease-out; + transition: top var(--dv-transition-duration) ease-out, + left var(--dv-transition-duration) ease-out, + width var(--dv-transition-duration) ease-out, + height var(--dv-transition-duration) ease-out, + opacity var(--dv-transition-duration) ease-out; will-change: transform; pointer-events: none; } -.dv-drop-target>.dv-drop-target-dropzone>.dv-drop-target-selection.dv-drop-target-top.dv-drop-target-small-vertical { +.dv-drop-target + > .dv-drop-target-dropzone + > .dv-drop-target-selection.dv-drop-target-top.dv-drop-target-small-vertical { border-top: 1px solid var(--dv-drag-over-border-color); } -.dv-drop-target>.dv-drop-target-dropzone>.dv-drop-target-selection.dv-drop-target-bottom.dv-drop-target-small-vertical { +.dv-drop-target + > .dv-drop-target-dropzone + > .dv-drop-target-selection.dv-drop-target-bottom.dv-drop-target-small-vertical { border-bottom: 1px solid var(--dv-drag-over-border-color); } -.dv-drop-target>.dv-drop-target-dropzone>.dv-drop-target-selection.dv-drop-target-left.dv-drop-target-small-horizontal { +.dv-drop-target + > .dv-drop-target-dropzone + > .dv-drop-target-selection.dv-drop-target-left.dv-drop-target-small-horizontal { border-left: 1px solid var(--dv-drag-over-border-color); } -.dv-drop-target>.dv-drop-target-dropzone>.dv-drop-target-selection.dv-drop-target-right.dv-drop-target-small-horizontal { +.dv-drop-target + > .dv-drop-target-dropzone + > .dv-drop-target-selection.dv-drop-target-right.dv-drop-target-small-horizontal { border-right: 1px solid var(--dv-drag-over-border-color); } @@ -209,22 +236,34 @@ position: relative; } -.dv-groupview.dv-active-group>.dv-tabs-and-actions-container .dv-tabs-container>.dv-tab.dv-active-tab { +.dv-groupview.dv-active-group + > .dv-tabs-and-actions-container + .dv-tabs-container + > .dv-tab.dv-active-tab { background-color: var(--dv-activegroup-visiblepanel-tab-background-color); color: var(--dv-activegroup-visiblepanel-tab-color); } -.dv-groupview.dv-active-group>.dv-tabs-and-actions-container .dv-tabs-container>.dv-tab.dv-inactive-tab { +.dv-groupview.dv-active-group + > .dv-tabs-and-actions-container + .dv-tabs-container + > .dv-tab.dv-inactive-tab { background-color: var(--dv-activegroup-hiddenpanel-tab-background-color); color: var(--dv-activegroup-hiddenpanel-tab-color); } -.dv-groupview.dv-inactive-group>.dv-tabs-and-actions-container .dv-tabs-container>.dv-tab.dv-active-tab { +.dv-groupview.dv-inactive-group + > .dv-tabs-and-actions-container + .dv-tabs-container + > .dv-tab.dv-active-tab { background-color: var(--dv-inactivegroup-visiblepanel-tab-background-color); color: var(--dv-inactivegroup-visiblepanel-tab-color); } -.dv-groupview.dv-inactive-group>.dv-tabs-and-actions-container .dv-tabs-container>.dv-tab.dv-inactive-tab { +.dv-groupview.dv-inactive-group + > .dv-tabs-and-actions-container + .dv-tabs-container + > .dv-tab.dv-inactive-tab { background-color: var(--dv-inactivegroup-hiddenpanel-tab-background-color); color: var(--dv-inactivegroup-hiddenpanel-tab-color); } @@ -250,7 +289,7 @@ outline: none; } -.dv-groupview>.dv-content-container { +.dv-groupview > .dv-content-container { flex-grow: 1; min-height: 0; outline: none; @@ -423,7 +462,7 @@ background-color: transparent !important; } -.dv-pane-container .dv-view:not(:first-child) .dv-pane>.dv-pane-header { +.dv-pane-container .dv-view:not(:first-child) .dv-pane > .dv-pane-header { border-top: 1px solid var(--dv-paneview-header-border-color); } @@ -441,12 +480,12 @@ align-items: center; } -.dv-pane-container .dv-view .dv-default-header>span { +.dv-pane-container .dv-view .dv-default-header > span { padding-left: 8px; flex-grow: 1; } -.dv-pane-container:first-of-type>.dv-pane>.dv-pane-header { +.dv-pane-container:first-of-type > .dv-pane > .dv-pane-header { border-top: none !important; } @@ -533,7 +572,7 @@ width: 100%; } -.dv-split-view-container.dv-splitview-disabled>.dv-sash-container>.dv-sash { +.dv-split-view-container.dv-splitview-disabled > .dv-sash-container > .dv-sash { pointer-events: none; } @@ -547,28 +586,38 @@ height: 100%; } -.dv-split-view-container.dv-horizontal>.dv-sash-container>.dv-sash { +.dv-split-view-container.dv-horizontal > .dv-sash-container > .dv-sash { height: 100%; width: 4px; } -.dv-split-view-container.dv-horizontal>.dv-sash-container>.dv-sash.dv-enabled { +.dv-split-view-container.dv-horizontal + > .dv-sash-container + > .dv-sash.dv-enabled { cursor: ew-resize; } -.dv-split-view-container.dv-horizontal>.dv-sash-container>.dv-sash.dv-disabled { +.dv-split-view-container.dv-horizontal + > .dv-sash-container + > .dv-sash.dv-disabled { cursor: default; } -.dv-split-view-container.dv-horizontal>.dv-sash-container>.dv-sash.dv-maximum { +.dv-split-view-container.dv-horizontal + > .dv-sash-container + > .dv-sash.dv-maximum { cursor: w-resize; } -.dv-split-view-container.dv-horizontal>.dv-sash-container>.dv-sash.dv-minimum { +.dv-split-view-container.dv-horizontal + > .dv-sash-container + > .dv-sash.dv-minimum { cursor: e-resize; } -.dv-split-view-container.dv-horizontal>.dv-view-container>.dv-view:not(:first-child)::before { +.dv-split-view-container.dv-horizontal + > .dv-view-container + > .dv-view:not(:first-child)::before { height: 100%; width: 1px; } @@ -577,32 +626,42 @@ width: 100%; } -.dv-split-view-container.dv-vertical>.dv-sash-container>.dv-sash { +.dv-split-view-container.dv-vertical > .dv-sash-container > .dv-sash { width: 100%; height: 4px; } -.dv-split-view-container.dv-vertical>.dv-sash-container>.dv-sash.dv-enabled { +.dv-split-view-container.dv-vertical + > .dv-sash-container + > .dv-sash.dv-enabled { cursor: ns-resize; } -.dv-split-view-container.dv-vertical>.dv-sash-container>.dv-sash.dv-disabled { +.dv-split-view-container.dv-vertical + > .dv-sash-container + > .dv-sash.dv-disabled { cursor: default; } -.dv-split-view-container.dv-vertical>.dv-sash-container>.dv-sash.dv-maximum { +.dv-split-view-container.dv-vertical + > .dv-sash-container + > .dv-sash.dv-maximum { cursor: n-resize; } -.dv-split-view-container.dv-vertical>.dv-sash-container>.dv-sash.dv-minimum { +.dv-split-view-container.dv-vertical + > .dv-sash-container + > .dv-sash.dv-minimum { cursor: s-resize; } -.dv-split-view-container.dv-vertical>.dv-view-container>.dv-view { +.dv-split-view-container.dv-vertical > .dv-view-container > .dv-view { width: 100%; } -.dv-split-view-container.dv-vertical>.dv-view-container>.dv-view:not(:first-child)::before { +.dv-split-view-container.dv-vertical + > .dv-view-container + > .dv-view:not(:first-child)::before { height: 1px; width: 100%; } @@ -647,7 +706,8 @@ position: absolute; } -.dv-split-view-container.dv-separator-border .dv-view:not(:first-child)::before { +.dv-split-view-container.dv-separator-border + .dv-view:not(:first-child)::before { content: " "; position: absolute; top: 0; @@ -739,11 +799,11 @@ cursor: pointer; } -.dv-tabs-overflow-dropdown-default>span { +.dv-tabs-overflow-dropdown-default > span { padding-left: 0.25rem; } -.dv-tabs-overflow-dropdown-default>svg { +.dv-tabs-overflow-dropdown-default > svg { transform: rotate(90deg); } @@ -752,6 +812,7 @@ height: 100%; overflow: auto; scrollbar-width: thin; + align-items: center; /* Track */ /* Handle */ } @@ -780,7 +841,7 @@ background: var(--dv-tabs-container-scrollbar-color); } -.dv-scrollable>.dv-tabs-container { +.dv-scrollable > .dv-tabs-container { overflow: hidden; } @@ -825,20 +886,25 @@ font-size: var(--dv-tabs-and-actions-container-font-size); } -.dv-tabs-and-actions-container.dv-single-tab.dv-full-width-single-tab .dv-scrollable { +.dv-tabs-and-actions-container.dv-single-tab.dv-full-width-single-tab + .dv-scrollable { flex-grow: 1; } -.dv-tabs-and-actions-container.dv-single-tab.dv-full-width-single-tab .dv-tabs-container { +.dv-tabs-and-actions-container.dv-single-tab.dv-full-width-single-tab + .dv-tabs-container { flex-grow: 1; } -.dv-tabs-and-actions-container.dv-single-tab.dv-full-width-single-tab .dv-tabs-container .dv-tab { +.dv-tabs-and-actions-container.dv-single-tab.dv-full-width-single-tab + .dv-tabs-container + .dv-tab { flex-grow: 1; padding: 0px; } -.dv-tabs-and-actions-container.dv-single-tab.dv-full-width-single-tab .dv-void-container { +.dv-tabs-and-actions-container.dv-single-tab.dv-full-width-single-tab + .dv-void-container { flex-grow: 0; } @@ -855,4 +921,4 @@ .dv-watermark { display: flex; height: 100%; -} \ No newline at end of file +} From c182452dd00ae7e89a9d6df5e2692aa109fa8880 Mon Sep 17 00:00:00 2001 From: cfngc4594 Date: Tue, 6 May 2025 18:37:39 +0800 Subject: [PATCH 09/58] refactor(stores): split dockview store into problem-specific store - Remove generic dockview store (`src/stores/dockview.tsx`) - Add problem-specific dockview store (`src/stores/problem-dockview.tsx`) - Remove submission-related state as it's no longer needed --- src/stores/dockview.ts | 34 ---------------------------------- src/stores/problem-dockview.ts | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 34 deletions(-) delete mode 100644 src/stores/dockview.ts create mode 100644 src/stores/problem-dockview.ts diff --git a/src/stores/dockview.ts b/src/stores/dockview.ts deleted file mode 100644 index b9323c0..0000000 --- a/src/stores/dockview.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { create } from "zustand"; -import type { DockviewApi } from "dockview"; -import { createJSONStorage, persist } from "zustand/middleware"; -import type { SubmissionWithTestcaseResult } from "@/types/prisma"; - -export type DockviewState = { - api: DockviewApi | null; - submission: SubmissionWithTestcaseResult | null; -}; - -export type DockviewActions = { - setApi: (api: DockviewApi) => void; - setSubmission: (submission: SubmissionWithTestcaseResult) => void; -}; - -export type DockviewStore = DockviewState & DockviewActions; - -export const useDockviewStore = create()( - persist( - (set) => ({ - api: null, - submission: null, - setApi: (api) => set({ api }), - setSubmission: (submission) => set({ submission }), - }), - { - name: "zustand:dockview", - storage: createJSONStorage(() => localStorage), - partialize: (state) => ({ - submission: state.submission, - }), - } - ) -); diff --git a/src/stores/problem-dockview.ts b/src/stores/problem-dockview.ts new file mode 100644 index 0000000..5fcb60a --- /dev/null +++ b/src/stores/problem-dockview.ts @@ -0,0 +1,20 @@ +import { create } from "zustand"; +import type { DockviewApi } from "dockview"; + +export type ProblemDockviewState = { + api: DockviewApi | null; +}; + +export type ProblemDockviewActions = { + setApi: (api: DockviewApi) => void; +}; + +export type ProblemDockviewStore = ProblemDockviewState & + ProblemDockviewActions; + +export const useProblemDockviewStore = create()( + (set) => ({ + api: null, + setApi: (api) => set({ api }), + }) +); From f464fb76366a79ebcb840d96414f0f9e2c923405 Mon Sep 17 00:00:00 2001 From: cfngc4594 Date: Tue, 6 May 2025 18:45:08 +0800 Subject: [PATCH 10/58] feat(dockview): refactor dockview component and add problem-specific implementation - Refactor Dockview component into more modular structure: - Extract layout persistence logic to custom hook - Extract component conversion logic to custom hook - Make storageKey optional - Improve type safety with PanelParams interface - Add better error handling and duplicate panel detection - Add new ProblemDockview wrapper component: - Integrates with problem-dockview store - Adds locale awareness - Provides standardized storage key - Update related type definitions and imports --- src/components/dockview.tsx | 182 ++++++++++++--------- src/features/problems/problem-dockview.tsx | 34 ++++ 2 files changed, 139 insertions(+), 77 deletions(-) create mode 100644 src/features/problems/problem-dockview.tsx diff --git a/src/components/dockview.tsx b/src/components/dockview.tsx index 40bd24b..601e1be 100644 --- a/src/components/dockview.tsx +++ b/src/components/dockview.tsx @@ -4,112 +4,140 @@ import type { AddPanelOptions, DockviewApi, DockviewReadyEvent, - IDockviewPanelHeaderProps, - IDockviewPanelProps, } from "dockview"; import "@/styles/dockview.css"; -import type { LucideIcon } from "lucide-react"; -import { useEffect, useMemo, useState } from "react"; import { DockviewReact, themeAbyssSpaced } from "dockview"; +import { useCallback, useEffect, useMemo, useState } from "react"; -interface PanelContent { - icon?: LucideIcon; - content?: React.ReactNode; - title?: string; +export interface PanelParams { autoAdd?: boolean; } interface DockviewProps { - storageKey: string; + storageKey?: string; onApiReady?: (api: DockviewApi) => void; - options: AddPanelOptions[]; + components: Record; + tabComponents: Record; + panelOptions: AddPanelOptions[]; } -export default function DockView({ storageKey, onApiReady, options }: DockviewProps) { - const [api, setApi] = useState(); - - const { components, tabComponents } = useMemo(() => { - const components: Record< - string, - React.FunctionComponent> - > = {}; - const tabComponents: Record< - string, - React.FunctionComponent> - > = {}; - - options.forEach((option) => { - const { id, params } = option; - - components[id] = () => { - const content = params?.content; - return <>{content}; - }; - - tabComponents[id] = () => { - const Icon = params?.icon; - return ( -
- {Icon && ( -
- ); - }; - }); - - return { components, tabComponents }; - }, [options]); - +/** + * Custom hook for handling dockview layout persistence + */ +const useLayoutPersistence = (api: DockviewApi | null, storageKey?: string) => { useEffect(() => { - if (!api) return; + if (!api || !storageKey) return; - const disposable = api.onDidLayoutChange(() => { - const layout = api.toJSON(); - localStorage.setItem(storageKey, JSON.stringify(layout)); - }); + const handleLayoutChange = () => { + try { + const layout = api.toJSON(); + localStorage.setItem(storageKey, JSON.stringify(layout)); + } catch (error) { + console.error("Failed to save layout:", error); + } + }; + const disposable = api.onDidLayoutChange(handleLayoutChange); return () => disposable.dispose(); }, [api, storageKey]); +}; - const onReady = (event: DockviewReadyEvent) => { - setApi(event.api); - onApiReady?.(event.api); +/** + * Converts React nodes to dockview component functions + */ +const useDockviewComponents = ( + components: Record, + tabComponents: Record +) => { + return useMemo( + () => ({ + dockviewComponents: Object.fromEntries( + Object.entries(components).map(([key, value]) => [key, () => value]) + ), + dockviewTabComponents: Object.fromEntries( + Object.entries(tabComponents).map(([key, value]) => [key, () => value]) + ), + }), + [components, tabComponents] + ); +}; - let success = false; - const serializedLayout = localStorage.getItem(storageKey); +const Dockview = ({ + storageKey, + onApiReady, + components, + tabComponents, + panelOptions: options, +}: DockviewProps) => { + const [api, setApi] = useState(null); + const { dockviewComponents, dockviewTabComponents } = useDockviewComponents( + components, + tabComponents + ); + + useLayoutPersistence(api, storageKey); + + const loadLayoutFromStorage = useCallback( + (api: DockviewApi, key: string): boolean => { + if (!key) return false; - if (serializedLayout) { try { - const layout = JSON.parse(serializedLayout); - event.api.fromJSON(layout); - success = true; + const serializedLayout = localStorage.getItem(key); + if (!serializedLayout) return false; + + api.fromJSON(JSON.parse(serializedLayout)); + return true; } catch (error) { console.error("Failed to load layout:", error); - localStorage.removeItem(storageKey); + localStorage.removeItem(key); + return false; } - } + }, + [] + ); - if (!success) { + const addDefaultPanels = useCallback( + (api: DockviewApi, options: AddPanelOptions[]) => { + const existingIds = new Set(); options.forEach((option) => { - const autoAdd = option.params?.autoAdd ?? true; - if (!autoAdd) return; - event.api.addPanel({ ...option }); + if (existingIds.has(option.id)) { + console.warn(`Duplicate panel ID detected: ${option.id}`); + return; + } + existingIds.add(option.id); + if (option.params?.autoAdd ?? true) { + api.addPanel(option); + } }); - } - }; + }, + [] + ); + + const handleReady = useCallback( + (event: DockviewReadyEvent) => { + setApi(event.api); + + const layoutLoaded = storageKey + ? loadLayoutFromStorage(event.api, storageKey) + : false; + + if (!layoutLoaded) { + addDefaultPanels(event.api, options); + } + + onApiReady?.(event.api); + }, + [storageKey, loadLayoutFromStorage, addDefaultPanels, onApiReady, options] + ); return ( ); -} +}; + +export { Dockview }; diff --git a/src/features/problems/problem-dockview.tsx b/src/features/problems/problem-dockview.tsx new file mode 100644 index 0000000..1e4da7e --- /dev/null +++ b/src/features/problems/problem-dockview.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { useLocale } from "next-intl"; +import type { AddPanelOptions } from "dockview"; +import { Dockview, type PanelParams } from "@/components/dockview"; +import { useProblemDockviewStore } from "@/stores/problem-dockview"; + +interface ProblemDockviewProps { + components: Record; + tabComponents: Record; + panelOptions: AddPanelOptions[]; +} + +const ProblemDockview = ({ + components, + tabComponents, + panelOptions, +}: ProblemDockviewProps) => { + const locale = useLocale(); + const { setApi } = useProblemDockviewStore(); + + return ( + + ); +}; + +export { ProblemDockview }; From 5f3eb72d0088c8852bbdf55a80369df4009ab733 Mon Sep 17 00:00:00 2001 From: cfngc4594 Date: Tue, 6 May 2025 19:38:50 +0800 Subject: [PATCH 11/58] refactor(problems): migrate description and solution to feature-based structure - Remove old parallel route implementations (@Description and @Solutions) - Add new feature-based components for problem description and solution - Create content and panel components for both features - Implement skeleton loading states - Use cached data fetching - Update MDX rendering and scroll area implementations --- .../problems/[id]/@Description/layout.tsx | 16 ------ .../(app)/problems/[id]/@Description/page.tsx | 42 -------------- .../(app)/problems/[id]/@Solutions/layout.tsx | 16 ------ .../(app)/problems/[id]/@Solutions/page.tsx | 43 -------------- .../description/components/content.tsx | 56 +++++++++++++++++++ .../problems/description/components/panel.tsx | 25 +++++++++ .../problems/solution/components/content.tsx | 56 +++++++++++++++++++ .../problems/solution/components/panel.tsx | 25 +++++++++ 8 files changed, 162 insertions(+), 117 deletions(-) delete mode 100644 src/app/(app)/problems/[id]/@Description/layout.tsx delete mode 100644 src/app/(app)/problems/[id]/@Description/page.tsx delete mode 100644 src/app/(app)/problems/[id]/@Solutions/layout.tsx delete mode 100644 src/app/(app)/problems/[id]/@Solutions/page.tsx create mode 100644 src/features/problems/description/components/content.tsx create mode 100644 src/features/problems/description/components/panel.tsx create mode 100644 src/features/problems/solution/components/content.tsx create mode 100644 src/features/problems/solution/components/panel.tsx diff --git a/src/app/(app)/problems/[id]/@Description/layout.tsx b/src/app/(app)/problems/[id]/@Description/layout.tsx deleted file mode 100644 index 0bef527..0000000 --- a/src/app/(app)/problems/[id]/@Description/layout.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Suspense } from "react"; -import { Loading } from "@/components/loading"; - -interface DescriptionLayoutProps { - children: React.ReactNode; -} - -export default function DescriptionLayout({ children }: DescriptionLayoutProps) { - return ( -
- }> - {children} - -
- ); -} diff --git a/src/app/(app)/problems/[id]/@Description/page.tsx b/src/app/(app)/problems/[id]/@Description/page.tsx deleted file mode 100644 index ad9c28a..0000000 --- a/src/app/(app)/problems/[id]/@Description/page.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import prisma from "@/lib/prisma"; -import { notFound } from "next/navigation"; -import MdxPreview from "@/components/mdx-preview"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import ProblemDescriptionFooter from "@/components/features/playground/problem/description/footer"; - -interface DescriptionPageProps { - params: Promise<{ id: string }>; -} - -export default async function DescriptionPage({ params }: DescriptionPageProps) { - const { id } = await params; - - if (!id) { - return notFound(); - } - - const problem = await prisma.problem.findUnique({ - where: { id }, - select: { - title: true, - description: true, - }, - }); - - if (!problem) { - return notFound(); - } - - return ( - <> -
-
- - - -
-
- - - ); -} diff --git a/src/app/(app)/problems/[id]/@Solutions/layout.tsx b/src/app/(app)/problems/[id]/@Solutions/layout.tsx deleted file mode 100644 index 90bcbeb..0000000 --- a/src/app/(app)/problems/[id]/@Solutions/layout.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Suspense } from "react"; -import { Loading } from "@/components/loading"; - -interface SolutionsLayoutProps { - children: React.ReactNode; -} - -export default async function SolutionsLayout({ children }: SolutionsLayoutProps) { - return ( -
- }> - {children} - -
- ); -} diff --git a/src/app/(app)/problems/[id]/@Solutions/page.tsx b/src/app/(app)/problems/[id]/@Solutions/page.tsx deleted file mode 100644 index e9c8e10..0000000 --- a/src/app/(app)/problems/[id]/@Solutions/page.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import prisma from "@/lib/prisma"; -import { notFound } from "next/navigation"; -import MdxPreview from "@/components/mdx-preview"; -import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; -import ProblemSolutionFooter from "@/components/features/playground/problem/solution/footer"; - -interface SolutionsPageProps { - params: Promise<{ id: string }>; -} - -export default async function SolutionsPage({ params }: SolutionsPageProps) { - const { id } = await params; - - if (!id) { - return notFound(); - } - - const problem = await prisma.problem.findUnique({ - where: { id }, - select: { - title: true, - solution: true, - }, - }); - - if (!problem) { - return notFound(); - } - - return ( - <> -
-
- - - - -
-
- - - ); -} diff --git a/src/features/problems/description/components/content.tsx b/src/features/problems/description/components/content.tsx new file mode 100644 index 0000000..2a090ee --- /dev/null +++ b/src/features/problems/description/components/content.tsx @@ -0,0 +1,56 @@ +import { getCachedProblem } from "@/lib/prisma"; +import { Skeleton } from "@/components/ui/skeleton"; +import { MdxRenderer } from "@/components/content/mdx-renderer"; +import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; + +interface DescriptionContentProps { + id: string; +} + +const DescriptionContent = async ({ id }: DescriptionContentProps) => { + const problem = await getCachedProblem(id); + + return ( + + + + + ); +}; + +const DescriptionContentSkeleton = () => { + return ( +
+ {/* Title skeleton */} + + + {/* Content skeletons */} + + + + + + + {/* Example section heading */} + + + {/* Example content */} + + + + {/* Code block skeleton */} +
+ +
+ + {/* More content */} + + +
+ ); +}; + +export { DescriptionContent, DescriptionContentSkeleton }; diff --git a/src/features/problems/description/components/panel.tsx b/src/features/problems/description/components/panel.tsx new file mode 100644 index 0000000..9ffd4dc --- /dev/null +++ b/src/features/problems/description/components/panel.tsx @@ -0,0 +1,25 @@ +import { Suspense } from "react"; +import { + DescriptionContent, + DescriptionContentSkeleton, +} from "@/features/problems/description/components/content"; + +interface DescriptionPanelProps { + id: string; +} + +const DescriptionPanel = ({ id }: DescriptionPanelProps) => { + return ( +
+
+
+ }> + + +
+
+
+ ); +}; + +export { DescriptionPanel }; diff --git a/src/features/problems/solution/components/content.tsx b/src/features/problems/solution/components/content.tsx new file mode 100644 index 0000000..4a62da2 --- /dev/null +++ b/src/features/problems/solution/components/content.tsx @@ -0,0 +1,56 @@ +import { getCachedProblem } from "@/lib/prisma"; +import { Skeleton } from "@/components/ui/skeleton"; +import { MdxRenderer } from "@/components/content/mdx-renderer"; +import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; + +interface SolutionContentProps { + id: string; +} + +const SolutionContent = async ({ id }: SolutionContentProps) => { + const problem = await getCachedProblem(id); + + return ( + + + + + ); +}; + +const SolutionContentSkeleton = () => { + return ( +
+ {/* Title skeleton */} + + + {/* Content skeletons */} + + + + + + + {/* Example section heading */} + + + {/* Example content */} + + + + {/* Code block skeleton */} +
+ +
+ + {/* More content */} + + +
+ ); +}; + +export { SolutionContent, SolutionContentSkeleton }; diff --git a/src/features/problems/solution/components/panel.tsx b/src/features/problems/solution/components/panel.tsx new file mode 100644 index 0000000..eba0fcc --- /dev/null +++ b/src/features/problems/solution/components/panel.tsx @@ -0,0 +1,25 @@ +import { Suspense } from "react"; +import { + SolutionContent, + SolutionContentSkeleton, +} from "@/features/problems/solution/components/content"; + +interface SolutionPanelProps { + id: string; +} + +const SolutionPanel = ({ id }: SolutionPanelProps) => { + return ( +
+
+
+ }> + + +
+
+
+ ); +}; + +export { SolutionPanel }; From 829370fafde260a278b3a2999915bc0970157dba Mon Sep 17 00:00:00 2001 From: cfngc4594 Date: Tue, 6 May 2025 19:47:02 +0800 Subject: [PATCH 12/58] chore(problems): move problem-dockview to components directory --- src/features/problems/{ => components}/problem-dockview.tsx | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/features/problems/{ => components}/problem-dockview.tsx (100%) diff --git a/src/features/problems/problem-dockview.tsx b/src/features/problems/components/problem-dockview.tsx similarity index 100% rename from src/features/problems/problem-dockview.tsx rename to src/features/problems/components/problem-dockview.tsx From aed942e7e297593014113a579412c75c1c98307f Mon Sep 17 00:00:00 2001 From: cfngc4594 Date: Tue, 6 May 2025 21:04:45 +0800 Subject: [PATCH 13/58] refactor(layouts): overhaul problem and problemset page structures - Simplify ProblemLayout to use children prop and remove ProblemStoreProvider - Replace PlaygroundHeader with dedicated ProblemHeader component - Streamline ProblemsetLayout with new ProblemsetHeader - Remove deprecated BackButton in favor of NavigateBackButton - Delete unused ProblemStoreProvider and related dependencies --- src/app/(app)/problems/[id]/layout.tsx | 79 ++----------------- src/app/(app)/problemset/layout.tsx | 14 +--- src/components/features/playground/header.tsx | 45 ----------- .../components/navigate-back-button.tsx} | 20 ++--- .../problems/components/problem-header.tsx | 33 ++++++++ .../components/problemset-header.tsx | 18 +++++ src/providers/problem-store-provider.tsx | 69 ---------------- 7 files changed, 73 insertions(+), 205 deletions(-) delete mode 100644 src/components/features/playground/header.tsx rename src/{components/back-button.tsx => features/problems/components/navigate-back-button.tsx} (76%) create mode 100644 src/features/problems/components/problem-header.tsx create mode 100644 src/features/problemset/components/problemset-header.tsx delete mode 100644 src/providers/problem-store-provider.tsx diff --git a/src/app/(app)/problems/[id]/layout.tsx b/src/app/(app)/problems/[id]/layout.tsx index 304f5b9..8ed15e9 100644 --- a/src/app/(app)/problems/[id]/layout.tsx +++ b/src/app/(app)/problems/[id]/layout.tsx @@ -1,90 +1,25 @@ -import prisma from "@/lib/prisma"; import { notFound } from "next/navigation"; -import { getUserLocale } from "@/i18n/locale"; -import ProblemPage from "@/app/(app)/problems/[id]/page"; -import { ProblemStoreProvider } from "@/providers/problem-store-provider"; -import { PlaygroundHeader } from "@/components/features/playground/header"; +import { ProblemHeader } from "@/features/problems/components/problem-header"; -interface ProblemProps { +interface ProblemLayoutProps { + children: React.ReactNode; params: Promise<{ id: string }>; - Description: React.ReactNode; - Solutions: React.ReactNode; - Submissions: React.ReactNode; - Details: React.ReactNode; - Code: React.ReactNode; - Testcase: React.ReactNode; - Bot: React.ReactNode; } export default async function ProblemLayout({ + children, params, - Description, - Solutions, - Submissions, - Details, - Code, - Testcase, - Bot, -}: ProblemProps) { +}: ProblemLayoutProps) { const { id } = await params; if (!id) { return notFound(); } - const [ - problem, - editorLanguageConfigs, - languageServerConfigs, - submissions, - ] = await Promise.all([ - prisma.problem.findUnique({ - where: { id }, - include: { - templates: true, - testcases: { - include: { - data: true, - }, - }, - }, - }), - prisma.editorLanguageConfig.findMany(), - prisma.languageServerConfig.findMany(), - prisma.submission.findMany({ - where: { problemId: id }, - }), - ]); - - if (!problem) { - return notFound(); - } - - const locale = await getUserLocale(); - return (
- - -
- -
-
+ + {children}
); } diff --git a/src/app/(app)/problemset/layout.tsx b/src/app/(app)/problemset/layout.tsx index b3c2b26..efed448 100644 --- a/src/app/(app)/problemset/layout.tsx +++ b/src/app/(app)/problemset/layout.tsx @@ -1,5 +1,4 @@ -import { Banner } from "@/components/banner"; -import { AvatarButton } from "@/components/avatar-button"; +import { ProblemsetHeader } from "@/features/problemset/components/problemset-header"; interface ProblemsetLayoutProps { children: React.ReactNode; @@ -7,14 +6,9 @@ interface ProblemsetLayoutProps { export default function ProblemsetLayout({ children }: ProblemsetLayoutProps) { return ( -
- -
- -
-
- {children} -
+
+ + {children}
); } diff --git a/src/components/features/playground/header.tsx b/src/components/features/playground/header.tsx deleted file mode 100644 index bd730a1..0000000 --- a/src/components/features/playground/header.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { cn } from "@/lib/utils"; -import { auth } from "@/lib/auth"; -import BackButton from "@/components/back-button"; -import { RunCodeButton } from "@/components/run-code"; -import { AvatarButton } from "@/components/avatar-button"; -import BotVisibilityToggle from "@/components/bot-visibility-toggle"; - -interface PlaygroundHeaderProps { - className?: string; -} - -export async function PlaygroundHeader({ - className, - ...props -}: PlaygroundHeaderProps) { - const session = await auth(); - - return ( -
- -
-
-
- -
-
-
-
- ); -} diff --git a/src/components/back-button.tsx b/src/features/problems/components/navigate-back-button.tsx similarity index 76% rename from src/components/back-button.tsx rename to src/features/problems/components/navigate-back-button.tsx index d894fd0..a6e144f 100644 --- a/src/components/back-button.tsx +++ b/src/features/problems/components/navigate-back-button.tsx @@ -1,20 +1,21 @@ import Link from "next/link"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { useTranslations } from "next-intl"; import { ArrowLeftIcon } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -interface BackButtonProps { +interface NavigateBackButtonProps { href: string; className?: string; } -export default function BackButton({ - href, - className, - ...props -}: BackButtonProps) { +const NavigateBackButton = ({ href, className }: NavigateBackButtonProps) => { const t = useTranslations(); return ( @@ -25,7 +26,6 @@ export default function BackButton({ variant="ghost" className={cn("h-8 w-auto p-2", className)} asChild - {...props} >
- } - > -
- + -
- + ], + remarkPlugins: [remarkGfm, remarkMath], + }, + }} + components={MdxComponents} + /> + ); -} +}; + +export { MdxRenderer }; From 6c9351ccd26d3a50230eee131a9612269b2fddcb Mon Sep 17 00:00:00 2001 From: cfngc4594 Date: Wed, 7 May 2025 13:32:26 +0800 Subject: [PATCH 16/58] feat(components): add TooltipButton component - A reusable button with tooltip functionality - Supports customizable delay, tooltip content, and className - Uses shadcn/ui Tooltip and Button components --- src/components/tooltip-button.tsx | 47 +++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/components/tooltip-button.tsx diff --git a/src/components/tooltip-button.tsx b/src/components/tooltip-button.tsx new file mode 100644 index 0000000..935a935 --- /dev/null +++ b/src/components/tooltip-button.tsx @@ -0,0 +1,47 @@ +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; +import { Button, ButtonProps } from "@/components/ui/button"; + +interface TooltipButtonProps extends ButtonProps { + children: React.ReactNode; + delayDuration?: number; + tooltipContent: string; + className?: string; +} + +const TooltipButton = ({ + children, + delayDuration = 0, + tooltipContent, + className, + ...props +}: TooltipButtonProps) => { + return ( + + + + + + + {tooltipContent} + + + + ); +}; + +export { TooltipButton }; From 3417d2ee4998a6ec7502fca49018f0b88208e870 Mon Sep 17 00:00:00 2001 From: cfngc4594 Date: Wed, 7 May 2025 13:42:08 +0800 Subject: [PATCH 17/58] refactor(editor): consolidate editor toolbar actions into unified structure - Moved all editor action buttons (copy, format, undo, redo, reset) from `src/components/features/playground/workspace/editor/components/` to new location `src/features/problems/code/components/toolbar/actions/` - Introduced shared `TooltipButton` component to reduce duplication - Created centralized `useProblemEditorActions` hook for common editor operations - Updated imports and exports through new index file - Maintained all existing functionality while improving code organization --- .../editor/components/copy-button.tsx | 75 ------------------- .../editor/components/format-button.tsx | 41 ---------- .../editor/components/redo-button.tsx | 41 ---------- .../editor/components/reset-button.tsx | 57 -------------- .../editor/components/undo-button.tsx | 41 ---------- .../toolbar/actions/copy-button.tsx | 56 ++++++++++++++ .../toolbar/actions/format-button.tsx | 24 ++++++ .../toolbar/actions/redo-button.tsx | 24 ++++++ .../toolbar/actions/reset-button.tsx | 43 +++++++++++ .../toolbar/actions/undo-button.tsx | 24 ++++++ .../problems/code/components/toolbar/index.ts | 5 ++ .../code/hooks/use-problem-editor-actions.ts | 55 ++++++++++++++ 12 files changed, 231 insertions(+), 255 deletions(-) delete mode 100644 src/components/features/playground/workspace/editor/components/copy-button.tsx delete mode 100644 src/components/features/playground/workspace/editor/components/format-button.tsx delete mode 100644 src/components/features/playground/workspace/editor/components/redo-button.tsx delete mode 100644 src/components/features/playground/workspace/editor/components/reset-button.tsx delete mode 100644 src/components/features/playground/workspace/editor/components/undo-button.tsx create mode 100644 src/features/problems/code/components/toolbar/actions/copy-button.tsx create mode 100644 src/features/problems/code/components/toolbar/actions/format-button.tsx create mode 100644 src/features/problems/code/components/toolbar/actions/redo-button.tsx create mode 100644 src/features/problems/code/components/toolbar/actions/reset-button.tsx create mode 100644 src/features/problems/code/components/toolbar/actions/undo-button.tsx create mode 100644 src/features/problems/code/components/toolbar/index.ts create mode 100644 src/features/problems/code/hooks/use-problem-editor-actions.ts diff --git a/src/components/features/playground/workspace/editor/components/copy-button.tsx b/src/components/features/playground/workspace/editor/components/copy-button.tsx deleted file mode 100644 index b229c86..0000000 --- a/src/components/features/playground/workspace/editor/components/copy-button.tsx +++ /dev/null @@ -1,75 +0,0 @@ -"use client"; - -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { cn } from "@/lib/utils"; -import { useState } from "react"; -import { Check, Copy } from "lucide-react"; -import { useTranslations } from "next-intl"; -import { Button } from "@/components/ui/button"; -import { useProblem } from "@/hooks/use-problem"; - -export function CopyButton() { - const { editor } = useProblem(); - const [copied, setCopied] = useState(false); - const t = useTranslations("WorkspaceEditorHeader.CopyButton"); - - const handleCopy = async () => { - try { - await navigator.clipboard.writeText(editor?.getValue() || ""); - setCopied(true); - setTimeout(() => setCopied(false), 1500); - } catch (err) { - console.error("Failed to copy text: ", err); - } - }; - - return ( - - - - - - - {t("TooltipContent")} - - - - ); -} diff --git a/src/components/features/playground/workspace/editor/components/format-button.tsx b/src/components/features/playground/workspace/editor/components/format-button.tsx deleted file mode 100644 index ab934b6..0000000 --- a/src/components/features/playground/workspace/editor/components/format-button.tsx +++ /dev/null @@ -1,41 +0,0 @@ -"use client"; - -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { Paintbrush } from "lucide-react"; -import { useTranslations } from "next-intl"; -import { Button } from "@/components/ui/button"; -import { useProblem } from "@/hooks/use-problem"; - -export function FormatButton() { - const { editor } = useProblem(); - const t = useTranslations("WorkspaceEditorHeader.FormatButton"); - - return ( - - - - - - - {t("TooltipContent")} - - - - ); -} diff --git a/src/components/features/playground/workspace/editor/components/redo-button.tsx b/src/components/features/playground/workspace/editor/components/redo-button.tsx deleted file mode 100644 index 92bcd63..0000000 --- a/src/components/features/playground/workspace/editor/components/redo-button.tsx +++ /dev/null @@ -1,41 +0,0 @@ -"use client"; - -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { Redo2 } from "lucide-react"; -import { useTranslations } from "next-intl"; -import { Button } from "@/components/ui/button"; -import { useProblem } from "@/hooks/use-problem"; - -export function RedoButton() { - const { editor } = useProblem(); - const t = useTranslations("WorkspaceEditorHeader.RedoButton"); - - return ( - - - - - - - {t("TooltipContent")} - - - - ); -} diff --git a/src/components/features/playground/workspace/editor/components/reset-button.tsx b/src/components/features/playground/workspace/editor/components/reset-button.tsx deleted file mode 100644 index 4f4b5d8..0000000 --- a/src/components/features/playground/workspace/editor/components/reset-button.tsx +++ /dev/null @@ -1,57 +0,0 @@ -"use client"; - -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { RotateCcw } from "lucide-react"; -import { useTranslations } from "next-intl"; -import { Button } from "@/components/ui/button"; -import { useProblem } from "@/hooks/use-problem"; - -export function ResetButton() { - const { editor, currentTemplate } = useProblem(); - const t = useTranslations("WorkspaceEditorHeader.ResetButton"); - - const handleReset = () => { - if (editor) { - const model = editor.getModel(); - if (model) { - const fullRange = model.getFullModelRange(); - editor.pushUndoStop(); - editor.executeEdits("reset-code", [ - { - range: fullRange, - text: currentTemplate, - forceMoveMarkers: true, - }, - ]); - editor.pushUndoStop(); - } - } - }; - - return ( - - - - - - - {t("TooltipContent")} - - - - ); -} diff --git a/src/components/features/playground/workspace/editor/components/undo-button.tsx b/src/components/features/playground/workspace/editor/components/undo-button.tsx deleted file mode 100644 index d7ec502..0000000 --- a/src/components/features/playground/workspace/editor/components/undo-button.tsx +++ /dev/null @@ -1,41 +0,0 @@ -"use client"; - -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { Undo2 } from "lucide-react"; -import { useTranslations } from "next-intl"; -import { Button } from "@/components/ui/button"; -import { useProblem } from "@/hooks/use-problem"; - -export function UndoButton() { - const { editor } = useProblem(); - const t = useTranslations("WorkspaceEditorHeader.UndoButton"); - - return ( - - - - - - - {t("TooltipContent")} - - - - ); -} diff --git a/src/features/problems/code/components/toolbar/actions/copy-button.tsx b/src/features/problems/code/components/toolbar/actions/copy-button.tsx new file mode 100644 index 0000000..3c97762 --- /dev/null +++ b/src/features/problems/code/components/toolbar/actions/copy-button.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import { useState } from "react"; +import { Check, Copy } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { TooltipButton } from "@/components/tooltip-button"; +import { useProblemEditorActions } from "@/features/problems/code/hooks/use-problem-editor-actions"; + +const CopyButton = () => { + const t = useTranslations("WorkspaceEditorHeader.CopyButton"); + const [copied, setCopied] = useState(false); + const { canExecute, handleCopy } = useProblemEditorActions(); + + const handleClick = async () => { + const success = await handleCopy(); + if (success) { + setCopied(true); + setTimeout(() => setCopied(false), 1500); + } + }; + + return ( + +
+
+
+
+
+ ); +}; + +export { CopyButton }; diff --git a/src/features/problems/code/components/toolbar/actions/format-button.tsx b/src/features/problems/code/components/toolbar/actions/format-button.tsx new file mode 100644 index 0000000..8add6e3 --- /dev/null +++ b/src/features/problems/code/components/toolbar/actions/format-button.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { Paintbrush } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { TooltipButton } from "@/components/tooltip-button"; +import { useProblemEditorActions } from "@/features/problems/code/hooks/use-problem-editor-actions"; + +const FormatButton = () => { + const t = useTranslations("WorkspaceEditorHeader.FormatButton"); + const { canExecute, handleFormat } = useProblemEditorActions(); + + return ( + + + ); +}; + +export { FormatButton }; diff --git a/src/features/problems/code/components/toolbar/actions/redo-button.tsx b/src/features/problems/code/components/toolbar/actions/redo-button.tsx new file mode 100644 index 0000000..af6411d --- /dev/null +++ b/src/features/problems/code/components/toolbar/actions/redo-button.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { Redo2 } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { TooltipButton } from "@/components/tooltip-button"; +import { useProblemEditorActions } from "@/features/problems/code/hooks/use-problem-editor-actions"; + +const RedoButton = () => { + const t = useTranslations("WorkspaceEditorHeader.RedoButton"); + const { canExecute, handleRedo } = useProblemEditorActions(); + + return ( + + + ); +}; + +export { RedoButton }; diff --git a/src/features/problems/code/components/toolbar/actions/reset-button.tsx b/src/features/problems/code/components/toolbar/actions/reset-button.tsx new file mode 100644 index 0000000..7f5ed2a --- /dev/null +++ b/src/features/problems/code/components/toolbar/actions/reset-button.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { useMemo } from "react"; +import { RotateCcw } from "lucide-react"; +import { useTranslations } from "next-intl"; +import type { Template } from "@/generated/client"; +import { TooltipButton } from "@/components/tooltip-button"; +import { useProblemEditorStore } from "@/stores/problem-editor-store"; +import { useProblemEditorActions } from "@/features/problems/code/hooks/use-problem-editor-actions"; + +interface ResetButtonProps { + templates: Template[]; +} + +const ResetButton = ({ templates }: ResetButtonProps) => { + const t = useTranslations("WorkspaceEditorHeader.ResetButton"); + const { language } = useProblemEditorStore(); + const { canExecute, handleReset } = useProblemEditorActions(); + + const currentTemplate = useMemo(() => { + return ( + templates.find((template) => template.language === language)?.template ?? + "" + ); + }, [language, templates]); + + const handleClick = () => { + handleReset(currentTemplate); + }; + + return ( + + + ); +}; + +export { ResetButton }; diff --git a/src/features/problems/code/components/toolbar/actions/undo-button.tsx b/src/features/problems/code/components/toolbar/actions/undo-button.tsx new file mode 100644 index 0000000..d10a579 --- /dev/null +++ b/src/features/problems/code/components/toolbar/actions/undo-button.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { Undo2 } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { TooltipButton } from "@/components/tooltip-button"; +import { useProblemEditorActions } from "@/features/problems/code/hooks/use-problem-editor-actions"; + +const UndoButton = () => { + const t = useTranslations("WorkspaceEditorHeader.UndoButton"); + const { canExecute, handleUndo } = useProblemEditorActions(); + + return ( + + + ); +}; + +export { UndoButton }; diff --git a/src/features/problems/code/components/toolbar/index.ts b/src/features/problems/code/components/toolbar/index.ts new file mode 100644 index 0000000..4ce7274 --- /dev/null +++ b/src/features/problems/code/components/toolbar/index.ts @@ -0,0 +1,5 @@ +export * from "./actions/copy-button"; +export * from "./actions/format-button"; +export * from "./actions/redo-button"; +export * from "./actions/reset-button"; +export * from "./actions/undo-button"; diff --git a/src/features/problems/code/hooks/use-problem-editor-actions.ts b/src/features/problems/code/hooks/use-problem-editor-actions.ts new file mode 100644 index 0000000..b17cd33 --- /dev/null +++ b/src/features/problems/code/hooks/use-problem-editor-actions.ts @@ -0,0 +1,55 @@ +import { useCallback } from "react"; +import { useProblemEditorStore } from "@/stores/problem-editor-store"; + +export const useProblemEditorActions = () => { + const { editor } = useProblemEditorStore(); + + const handleCopy = useCallback(async () => { + try { + await navigator.clipboard.writeText(editor?.getValue() || ""); + return true; + } catch (error) { + console.error("Failed to copy text: ", error); + return false; + } + }, [editor]); + + const handleFormat = useCallback(() => { + editor?.trigger("format", "editor.action.formatDocument", null); + }, [editor]); + + const handleUndo = useCallback(() => { + editor?.trigger("undo", "undo", null); + }, [editor]); + + const handleRedo = useCallback(() => { + editor?.trigger("redo", "redo", null); + }, [editor]); + + const handleReset = useCallback((template: string) => { + if (!editor) return; + + const model = editor.getModel(); + if (!model) return; + + const fullRange = model.getFullModelRange(); + editor.pushUndoStop(); + editor.executeEdits("reset-code", [ + { + range: fullRange, + text: template, + forceMoveMarkers: true, + }, + ]); + editor.pushUndoStop(); + }, [editor]); + + return { + handleCopy, + handleFormat, + handleUndo, + handleRedo, + handleReset, + canExecute: !!editor, + }; +}; From 1db666a2ab2028db80efa52e9a5edbf12a7a10e3 Mon Sep 17 00:00:00 2001 From: cfngc4594 Date: Wed, 7 May 2025 14:05:21 +0800 Subject: [PATCH 18/58] refactor(structure): reorganize page and component exports - Move root page from /(app) to / directory - Convert default exports to named exports in components - Rename MainView component to HeroSection for better semantics --- src/app/(app)/page.tsx | 17 ----------------- src/app/page.tsx | 15 +++++++++++++++ src/components/faqs.tsx | 8 +++++--- src/components/footer.tsx | 6 ++++-- src/components/header.tsx | 6 ++++-- .../{main-view.tsx => hero-section.tsx} | 14 ++++++-------- src/components/primary-features.tsx | 6 ++++-- 7 files changed, 38 insertions(+), 34 deletions(-) delete mode 100644 src/app/(app)/page.tsx create mode 100644 src/app/page.tsx rename src/components/{main-view.tsx => hero-section.tsx} (88%) diff --git a/src/app/(app)/page.tsx b/src/app/(app)/page.tsx deleted file mode 100644 index 6504f55..0000000 --- a/src/app/(app)/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import FAQs from "@/components/faqs"; -import { Header } from "@/components/header"; -import { Footer } from "@/components/footer"; -import { MainView } from "@/components/main-view"; -import { PrimaryFeatures } from "@/components/primary-features"; - -export default function HomePage() { - return ( - <> -
- - - -