From 2fa462ae60ac0dc56d5a891ad4f18b8b3a69e727 Mon Sep 17 00:00:00 2001 From: Snowykami Date: Thu, 18 Sep 2025 21:45:18 +0800 Subject: [PATCH] Refactor console layout and sidebar components; implement user authentication and loading states - Updated `RootLayout` to include user authentication logic and loading states. - Removed redundant user authentication logic from `Page` component. - Enhanced `AppSidebar` to fetch and display logged-in user information. - Replaced `GravatarAvatar` with new `Avatar` component for user profile images. - Added new pages for comment, file, post, and user management. - Introduced utility functions for generating Gravatar URLs and fallback avatars based on usernames. - Cleaned up unused imports and components across various files. --- web/package.json | 2 + web/pnpm-lock.yaml | 35 + web/src/app/console/comment/page.tsx | 3 + web/src/app/console/data.json | 614 ------------------ web/src/app/console/file/page.tsx | 3 + web/src/app/console/layout.tsx | 49 +- web/src/app/console/page.tsx | 46 +- web/src/app/console/post/page.tsx | 3 + web/src/app/console/user/page.tsx | 3 + web/src/components/blog-home/blog-home.tsx | 2 +- web/src/components/blog/blog-sidebar-card.tsx | 37 +- web/src/components/comment/comment-input.tsx | 9 +- web/src/components/comment/comment-item.tsx | 9 +- web/src/components/comment/index.tsx | 2 +- web/src/components/common/captcha/index.tsx | 1 - .../components/common/captcha/turnstile.tsx | 8 +- web/src/components/common/gravatar.tsx | 70 -- .../components/common/image-placeholder.tsx | 7 - .../components/common/markdown-codeblock.tsx | 2 +- web/src/components/common/pagination.tsx | 8 +- web/src/components/console/app-sidebar.tsx | 147 +---- web/src/components/console/nav-main.tsx | 14 +- web/src/components/console/nav-user.tsx | 25 +- .../layout/avatar-with-dropdown-menu.tsx | 37 +- web/src/components/layout/navbar-or-side.tsx | 2 +- web/src/components/login/login-form.tsx | 8 +- web/src/components/user/user-header.tsx | 9 +- web/src/contexts/device-context.tsx | 2 +- web/src/i18n/request.ts | 2 +- web/src/lib/clipboard.ts | 4 +- web/src/utils/common/gravatar.ts | 21 + web/src/utils/common/username.ts | 22 + 32 files changed, 253 insertions(+), 953 deletions(-) create mode 100644 web/src/app/console/comment/page.tsx delete mode 100644 web/src/app/console/data.json create mode 100644 web/src/app/console/file/page.tsx create mode 100644 web/src/app/console/post/page.tsx create mode 100644 web/src/app/console/user/page.tsx delete mode 100644 web/src/components/common/gravatar.tsx delete mode 100644 web/src/components/common/image-placeholder.tsx create mode 100644 web/src/utils/common/gravatar.ts create mode 100644 web/src/utils/common/username.ts diff --git a/web/package.json b/web/package.json index ef146ae..6d856f6 100644 --- a/web/package.json +++ b/web/package.json @@ -38,6 +38,7 @@ "field-conv": "^1.0.9", "highlight.js": "^11.11.1", "lucide-react": "^0.525.0", + "md5": "^2.3.0", "motion": "^12.23.12", "next": "15.4.1", "next-intl": "^4.3.4", @@ -56,6 +57,7 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/md5": "^2.3.5", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 4af77bd..1282d97 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -95,6 +95,9 @@ importers: lucide-react: specifier: ^0.525.0 version: 0.525.0(react@19.1.0) + md5: + specifier: ^2.3.0 + version: 2.3.0 motion: specifier: ^12.23.12 version: 12.23.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -144,6 +147,9 @@ importers: '@tailwindcss/postcss': specifier: ^4 version: 4.1.11 + '@types/md5': + specifier: ^2.3.5 + version: 2.3.5 '@types/node': specifier: ^20 version: 20.19.8 @@ -1216,6 +1222,9 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/md5@2.3.5': + resolution: {integrity: sha512-/i42wjYNgE6wf0j2bcTX6kuowmdL/6PE4IVitMpm2eYKBUuYCprdcWVK+xEF0gcV6ufMCRhtxmReGfc6hIK7Jw==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -1545,6 +1554,9 @@ packages: character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + charenc@0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + chownr@3.0.0: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} @@ -1590,6 +1602,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crypt@0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -2158,6 +2173,9 @@ packages: resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} + is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + is-bun-module@2.0.0: resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} @@ -2407,6 +2425,9 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + md5@2.3.0: + resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + mdast-util-from-markdown@2.0.2: resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} @@ -4269,6 +4290,8 @@ snapshots: '@types/json5@0.0.29': {} + '@types/md5@2.3.5': {} + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -4620,6 +4643,8 @@ snapshots: character-reference-invalid@2.0.1: {} + charenc@0.0.2: {} + chownr@3.0.0: {} class-variance-authority@0.7.1: @@ -4664,6 +4689,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypt@0.0.2: {} + csstype@3.1.3: {} d3-array@3.2.4: @@ -5429,6 +5456,8 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-buffer@1.1.6: {} + is-bun-module@2.0.0: dependencies: semver: 7.7.2 @@ -5653,6 +5682,12 @@ snapshots: math-intrinsics@1.1.0: {} + md5@2.3.0: + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: 1.1.6 + mdast-util-from-markdown@2.0.2: dependencies: '@types/mdast': 4.0.4 diff --git a/web/src/app/console/comment/page.tsx b/web/src/app/console/comment/page.tsx new file mode 100644 index 0000000..75d603b --- /dev/null +++ b/web/src/app/console/comment/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
评论管理
; +} \ No newline at end of file diff --git a/web/src/app/console/data.json b/web/src/app/console/data.json deleted file mode 100644 index ec08736..0000000 --- a/web/src/app/console/data.json +++ /dev/null @@ -1,614 +0,0 @@ -[ - { - "id": 1, - "header": "Cover page", - "type": "Cover page", - "status": "In Process", - "target": "18", - "limit": "5", - "reviewer": "Eddie Lake" - }, - { - "id": 2, - "header": "Table of contents", - "type": "Table of contents", - "status": "Done", - "target": "29", - "limit": "24", - "reviewer": "Eddie Lake" - }, - { - "id": 3, - "header": "Executive summary", - "type": "Narrative", - "status": "Done", - "target": "10", - "limit": "13", - "reviewer": "Eddie Lake" - }, - { - "id": 4, - "header": "Technical approach", - "type": "Narrative", - "status": "Done", - "target": "27", - "limit": "23", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 5, - "header": "Design", - "type": "Narrative", - "status": "In Process", - "target": "2", - "limit": "16", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 6, - "header": "Capabilities", - "type": "Narrative", - "status": "In Process", - "target": "20", - "limit": "8", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 7, - "header": "Integration with existing systems", - "type": "Narrative", - "status": "In Process", - "target": "19", - "limit": "21", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 8, - "header": "Innovation and Advantages", - "type": "Narrative", - "status": "Done", - "target": "25", - "limit": "26", - "reviewer": "Assign reviewer" - }, - { - "id": 9, - "header": "Overview of EMR's Innovative Solutions", - "type": "Technical content", - "status": "Done", - "target": "7", - "limit": "23", - "reviewer": "Assign reviewer" - }, - { - "id": 10, - "header": "Advanced Algorithms and Machine Learning", - "type": "Narrative", - "status": "Done", - "target": "30", - "limit": "28", - "reviewer": "Assign reviewer" - }, - { - "id": 11, - "header": "Adaptive Communication Protocols", - "type": "Narrative", - "status": "Done", - "target": "9", - "limit": "31", - "reviewer": "Assign reviewer" - }, - { - "id": 12, - "header": "Advantages Over Current Technologies", - "type": "Narrative", - "status": "Done", - "target": "12", - "limit": "0", - "reviewer": "Assign reviewer" - }, - { - "id": 13, - "header": "Past Performance", - "type": "Narrative", - "status": "Done", - "target": "22", - "limit": "33", - "reviewer": "Assign reviewer" - }, - { - "id": 14, - "header": "Customer Feedback and Satisfaction Levels", - "type": "Narrative", - "status": "Done", - "target": "15", - "limit": "34", - "reviewer": "Assign reviewer" - }, - { - "id": 15, - "header": "Implementation Challenges and Solutions", - "type": "Narrative", - "status": "Done", - "target": "3", - "limit": "35", - "reviewer": "Assign reviewer" - }, - { - "id": 16, - "header": "Security Measures and Data Protection Policies", - "type": "Narrative", - "status": "In Process", - "target": "6", - "limit": "36", - "reviewer": "Assign reviewer" - }, - { - "id": 17, - "header": "Scalability and Future Proofing", - "type": "Narrative", - "status": "Done", - "target": "4", - "limit": "37", - "reviewer": "Assign reviewer" - }, - { - "id": 18, - "header": "Cost-Benefit Analysis", - "type": "Plain language", - "status": "Done", - "target": "14", - "limit": "38", - "reviewer": "Assign reviewer" - }, - { - "id": 19, - "header": "User Training and Onboarding Experience", - "type": "Narrative", - "status": "Done", - "target": "17", - "limit": "39", - "reviewer": "Assign reviewer" - }, - { - "id": 20, - "header": "Future Development Roadmap", - "type": "Narrative", - "status": "Done", - "target": "11", - "limit": "40", - "reviewer": "Assign reviewer" - }, - { - "id": 21, - "header": "System Architecture Overview", - "type": "Technical content", - "status": "In Process", - "target": "24", - "limit": "18", - "reviewer": "Maya Johnson" - }, - { - "id": 22, - "header": "Risk Management Plan", - "type": "Narrative", - "status": "Done", - "target": "15", - "limit": "22", - "reviewer": "Carlos Rodriguez" - }, - { - "id": 23, - "header": "Compliance Documentation", - "type": "Legal", - "status": "In Process", - "target": "31", - "limit": "27", - "reviewer": "Sarah Chen" - }, - { - "id": 24, - "header": "API Documentation", - "type": "Technical content", - "status": "Done", - "target": "8", - "limit": "12", - "reviewer": "Raj Patel" - }, - { - "id": 25, - "header": "User Interface Mockups", - "type": "Visual", - "status": "In Process", - "target": "19", - "limit": "25", - "reviewer": "Leila Ahmadi" - }, - { - "id": 26, - "header": "Database Schema", - "type": "Technical content", - "status": "Done", - "target": "22", - "limit": "20", - "reviewer": "Thomas Wilson" - }, - { - "id": 27, - "header": "Testing Methodology", - "type": "Technical content", - "status": "In Process", - "target": "17", - "limit": "14", - "reviewer": "Assign reviewer" - }, - { - "id": 28, - "header": "Deployment Strategy", - "type": "Narrative", - "status": "Done", - "target": "26", - "limit": "30", - "reviewer": "Eddie Lake" - }, - { - "id": 29, - "header": "Budget Breakdown", - "type": "Financial", - "status": "In Process", - "target": "13", - "limit": "16", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 30, - "header": "Market Analysis", - "type": "Research", - "status": "Done", - "target": "29", - "limit": "32", - "reviewer": "Sophia Martinez" - }, - { - "id": 31, - "header": "Competitor Comparison", - "type": "Research", - "status": "In Process", - "target": "21", - "limit": "19", - "reviewer": "Assign reviewer" - }, - { - "id": 32, - "header": "Maintenance Plan", - "type": "Technical content", - "status": "Done", - "target": "16", - "limit": "23", - "reviewer": "Alex Thompson" - }, - { - "id": 33, - "header": "User Personas", - "type": "Research", - "status": "In Process", - "target": "27", - "limit": "24", - "reviewer": "Nina Patel" - }, - { - "id": 34, - "header": "Accessibility Compliance", - "type": "Legal", - "status": "Done", - "target": "18", - "limit": "21", - "reviewer": "Assign reviewer" - }, - { - "id": 35, - "header": "Performance Metrics", - "type": "Technical content", - "status": "In Process", - "target": "23", - "limit": "26", - "reviewer": "David Kim" - }, - { - "id": 36, - "header": "Disaster Recovery Plan", - "type": "Technical content", - "status": "Done", - "target": "14", - "limit": "17", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 37, - "header": "Third-party Integrations", - "type": "Technical content", - "status": "In Process", - "target": "25", - "limit": "28", - "reviewer": "Eddie Lake" - }, - { - "id": 38, - "header": "User Feedback Summary", - "type": "Research", - "status": "Done", - "target": "20", - "limit": "15", - "reviewer": "Assign reviewer" - }, - { - "id": 39, - "header": "Localization Strategy", - "type": "Narrative", - "status": "In Process", - "target": "12", - "limit": "19", - "reviewer": "Maria Garcia" - }, - { - "id": 40, - "header": "Mobile Compatibility", - "type": "Technical content", - "status": "Done", - "target": "28", - "limit": "31", - "reviewer": "James Wilson" - }, - { - "id": 41, - "header": "Data Migration Plan", - "type": "Technical content", - "status": "In Process", - "target": "19", - "limit": "22", - "reviewer": "Assign reviewer" - }, - { - "id": 42, - "header": "Quality Assurance Protocols", - "type": "Technical content", - "status": "Done", - "target": "30", - "limit": "33", - "reviewer": "Priya Singh" - }, - { - "id": 43, - "header": "Stakeholder Analysis", - "type": "Research", - "status": "In Process", - "target": "11", - "limit": "14", - "reviewer": "Eddie Lake" - }, - { - "id": 44, - "header": "Environmental Impact Assessment", - "type": "Research", - "status": "Done", - "target": "24", - "limit": "27", - "reviewer": "Assign reviewer" - }, - { - "id": 45, - "header": "Intellectual Property Rights", - "type": "Legal", - "status": "In Process", - "target": "17", - "limit": "20", - "reviewer": "Sarah Johnson" - }, - { - "id": 46, - "header": "Customer Support Framework", - "type": "Narrative", - "status": "Done", - "target": "22", - "limit": "25", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 47, - "header": "Version Control Strategy", - "type": "Technical content", - "status": "In Process", - "target": "15", - "limit": "18", - "reviewer": "Assign reviewer" - }, - { - "id": 48, - "header": "Continuous Integration Pipeline", - "type": "Technical content", - "status": "Done", - "target": "26", - "limit": "29", - "reviewer": "Michael Chen" - }, - { - "id": 49, - "header": "Regulatory Compliance", - "type": "Legal", - "status": "In Process", - "target": "13", - "limit": "16", - "reviewer": "Assign reviewer" - }, - { - "id": 50, - "header": "User Authentication System", - "type": "Technical content", - "status": "Done", - "target": "28", - "limit": "31", - "reviewer": "Eddie Lake" - }, - { - "id": 51, - "header": "Data Analytics Framework", - "type": "Technical content", - "status": "In Process", - "target": "21", - "limit": "24", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 52, - "header": "Cloud Infrastructure", - "type": "Technical content", - "status": "Done", - "target": "16", - "limit": "19", - "reviewer": "Assign reviewer" - }, - { - "id": 53, - "header": "Network Security Measures", - "type": "Technical content", - "status": "In Process", - "target": "29", - "limit": "32", - "reviewer": "Lisa Wong" - }, - { - "id": 54, - "header": "Project Timeline", - "type": "Planning", - "status": "Done", - "target": "14", - "limit": "17", - "reviewer": "Eddie Lake" - }, - { - "id": 55, - "header": "Resource Allocation", - "type": "Planning", - "status": "In Process", - "target": "27", - "limit": "30", - "reviewer": "Assign reviewer" - }, - { - "id": 56, - "header": "Team Structure and Roles", - "type": "Planning", - "status": "Done", - "target": "20", - "limit": "23", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 57, - "header": "Communication Protocols", - "type": "Planning", - "status": "In Process", - "target": "15", - "limit": "18", - "reviewer": "Assign reviewer" - }, - { - "id": 58, - "header": "Success Metrics", - "type": "Planning", - "status": "Done", - "target": "30", - "limit": "33", - "reviewer": "Eddie Lake" - }, - { - "id": 59, - "header": "Internationalization Support", - "type": "Technical content", - "status": "In Process", - "target": "23", - "limit": "26", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 60, - "header": "Backup and Recovery Procedures", - "type": "Technical content", - "status": "Done", - "target": "18", - "limit": "21", - "reviewer": "Assign reviewer" - }, - { - "id": 61, - "header": "Monitoring and Alerting System", - "type": "Technical content", - "status": "In Process", - "target": "25", - "limit": "28", - "reviewer": "Daniel Park" - }, - { - "id": 62, - "header": "Code Review Guidelines", - "type": "Technical content", - "status": "Done", - "target": "12", - "limit": "15", - "reviewer": "Eddie Lake" - }, - { - "id": 63, - "header": "Documentation Standards", - "type": "Technical content", - "status": "In Process", - "target": "27", - "limit": "30", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 64, - "header": "Release Management Process", - "type": "Planning", - "status": "Done", - "target": "22", - "limit": "25", - "reviewer": "Assign reviewer" - }, - { - "id": 65, - "header": "Feature Prioritization Matrix", - "type": "Planning", - "status": "In Process", - "target": "19", - "limit": "22", - "reviewer": "Emma Davis" - }, - { - "id": 66, - "header": "Technical Debt Assessment", - "type": "Technical content", - "status": "Done", - "target": "24", - "limit": "27", - "reviewer": "Eddie Lake" - }, - { - "id": 67, - "header": "Capacity Planning", - "type": "Planning", - "status": "In Process", - "target": "21", - "limit": "24", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 68, - "header": "Service Level Agreements", - "type": "Legal", - "status": "Done", - "target": "26", - "limit": "29", - "reviewer": "Assign reviewer" - } -] diff --git a/web/src/app/console/file/page.tsx b/web/src/app/console/file/page.tsx new file mode 100644 index 0000000..52395fa --- /dev/null +++ b/web/src/app/console/file/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
文件管理
; +} \ No newline at end of file diff --git a/web/src/app/console/layout.tsx b/web/src/app/console/layout.tsx index aae6e6c..60f06c4 100644 --- a/web/src/app/console/layout.tsx +++ b/web/src/app/console/layout.tsx @@ -1,11 +1,50 @@ -export default function RootLayout({ +"use client" +import { AppSidebar } from "@/components/console/app-sidebar" +import { SiteHeader } from "@/components/console/site-header" +import { + SidebarInset, + SidebarProvider, +} from "@/components/ui/sidebar" + +import { useToLogin } from "@/hooks/use-route" +import { useEffect, useState } from "react" +import { User } from "@/models/user" +import { getLoginUser } from "@/api/user" + +export default function ConsoleLayout({ children, }: Readonly<{ - children: React.ReactNode + children: React.ReactNode; }>) { + const [user, setUser] = useState(null); + const toLogin = useToLogin(); + + useEffect(() => { + getLoginUser().then(res => { + setUser(res.data); + }).catch(() => { + setUser(null); + toLogin(); + }); + }, [toLogin]); + if (user === null) { + return null; + } + return ( - <> - {children} - + + + + + {children} + + ) } diff --git a/web/src/app/console/page.tsx b/web/src/app/console/page.tsx index e59e1ba..fe67e77 100644 --- a/web/src/app/console/page.tsx +++ b/web/src/app/console/page.tsx @@ -1,45 +1,3 @@ -"use client" -import { AppSidebar } from "@/components/console/app-sidebar" -import { SiteHeader } from "@/components/console/site-header" -import { - SidebarInset, - SidebarProvider, -} from "@/components/ui/sidebar" - -import { useToLogin } from "@/hooks/use-route" -import { useEffect, useState } from "react" -import { User } from "@/models/user" -import { getLoginUser } from "@/api/user" - export default function Page() { - const [user, setUser] = useState(null); - const toLogin = useToLogin(); - - useEffect(() => { - getLoginUser().then(res => { - setUser(res.data); - }).catch(() => { - setUser(null); - toLogin(); - }); - }, []); - if (user === null) { - return null; - } - - return ( - - - - - - - ) -} + return
Console
; +} \ No newline at end of file diff --git a/web/src/app/console/post/page.tsx b/web/src/app/console/post/page.tsx new file mode 100644 index 0000000..d612c1f --- /dev/null +++ b/web/src/app/console/post/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
文章管理
; +} \ No newline at end of file diff --git a/web/src/app/console/user/page.tsx b/web/src/app/console/user/page.tsx new file mode 100644 index 0000000..b3f8732 --- /dev/null +++ b/web/src/app/console/user/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
用户管理
; +} \ No newline at end of file diff --git a/web/src/components/blog-home/blog-home.tsx b/web/src/components/blog-home/blog-home.tsx index f568bcc..86b9ed4 100644 --- a/web/src/components/blog-home/blog-home.tsx +++ b/web/src/components/blog-home/blog-home.tsx @@ -38,7 +38,7 @@ export default function BlogHome() { const [sortBy, setSortBy, isSortByLoaded] = useStoredState(QueryKey.SortBy, DEFAULT_SORTBY); useEffect(() => { - if (!isSortByLoaded) return; // wait for stored state loaded + if (!isSortByLoaded) return; setLoading(true); listPosts( { diff --git a/web/src/components/blog/blog-sidebar-card.tsx b/web/src/components/blog/blog-sidebar-card.tsx index 940ee38..8cd49e9 100644 --- a/web/src/components/blog/blog-sidebar-card.tsx +++ b/web/src/components/blog/blog-sidebar-card.tsx @@ -1,6 +1,5 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Heart, TrendingUp, Eye } from "lucide-react"; -import GravatarAvatar from "@/components/common/gravatar"; import { Badge } from "@/components/ui/badge"; import type { Label } from "@/models/label"; import type { Post } from "@/models/post"; @@ -9,6 +8,9 @@ import { useEffect, useState } from "react"; import { useTranslations } from "next-intl"; import Link from "next/link"; import { getPostHref } from "@/utils/common/post"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { getGravatarUrl } from "@/utils/common/gravatar"; +import { getFallbackAvatarFromUsername } from "@/utils/common/username"; // 侧边栏父组件,接收卡片组件列表 export default function Sidebar({ cards }: { cards: React.ReactNode[] }) { @@ -34,7 +36,10 @@ export function SidebarAbout({ config }: { config: typeof configType }) {
- + + + {getFallbackAvatarFromUsername(config.owner.name)} +

{config.owner.name}

{config.owner.motto}

@@ -66,23 +71,23 @@ export function SidebarHotPosts({ posts, sortType }: { posts: Post[], sortType:

- {post.title} -

-
- - - {post.viewCount} - - - - {post.likeCount} - + {post.title} + +
+ + + {post.viewCount} + + + + {post.likeCount} + +
-
- + - ))} + ))}
); diff --git a/web/src/components/comment/comment-input.tsx b/web/src/components/comment/comment-input.tsx index 3e28573..66f0f5f 100644 --- a/web/src/components/comment/comment-input.tsx +++ b/web/src/components/comment/comment-input.tsx @@ -3,11 +3,13 @@ import { User } from "@/models/user"; import { useTranslations } from "next-intl"; import { useState } from "react"; import { toast } from "sonner"; -import GravatarAvatar from "@/components/common/gravatar"; import { CircleUser } from "lucide-react"; import { Textarea } from "@/components/ui/textarea"; import { Checkbox } from "@/components/ui/checkbox" import { Label } from "@/components/ui/label"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { getGravatarUrl } from "@/utils/common/gravatar"; +import { getFirstCharFromUser } from "@/utils/common/username"; export function CommentInput( @@ -65,7 +67,10 @@ export function CommentInput(
clickToUserProfile(user.username) : clickToLogin} className="cursor-pointer flex-shrink-0 w-10 h-10 fade-in"> - {user && } + {user && + + {getFirstCharFromUser(user)} + } {!user && }
diff --git a/web/src/components/comment/comment-item.tsx b/web/src/components/comment/comment-item.tsx index b9d8926..423ad80 100644 --- a/web/src/components/comment/comment-item.tsx +++ b/web/src/components/comment/comment-item.tsx @@ -3,7 +3,6 @@ import { User } from "@/models/user"; import { useLocale, useTranslations } from "next-intl"; import { useState } from "react"; import { toast } from "sonner"; -import GravatarAvatar, { getGravatarByUser } from "@/components/common/gravatar"; import { Reply, Trash, Heart, Pencil, Lock } from "lucide-react"; import { Comment } from "@/models/comment"; import { TargetType } from "@/models/types"; @@ -13,6 +12,9 @@ import { CommentInput } from "./comment-input"; import { createComment, deleteComment, getComment, listComments, updateComment } from "@/api/comment"; import { OrderBy } from "@/models/common"; import { formatDateTime } from "@/utils/common/datetime"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { getGravatarUrl } from "@/utils/common/gravatar"; +import { getFirstCharFromUser } from "@/utils/common/username"; export function CommentItem( @@ -157,7 +159,10 @@ export function CommentItem(
clickToUserProfile(commentState.user.username)} className="cursor-pointer fade-in w-12 h-12"> - + + + {getFirstCharFromUser(commentState.user)} +
diff --git a/web/src/components/comment/index.tsx b/web/src/components/comment/index.tsx index 445cb21..deaa820 100644 --- a/web/src/components/comment/index.tsx +++ b/web/src/components/comment/index.tsx @@ -59,7 +59,7 @@ export function CommentSection( }).then(response => { setComments(response.data.comments); }); - }, []) + }, [page, targetId, targetType]); const onCommentSubmitted = ({ commentContent, isPrivate, showClientInfo }: { commentContent: string, isPrivate: boolean, showClientInfo: boolean }) => { createComment({ diff --git a/web/src/components/common/captcha/index.tsx b/web/src/components/common/captcha/index.tsx index 174df66..2210a52 100644 --- a/web/src/components/common/captcha/index.tsx +++ b/web/src/components/common/captcha/index.tsx @@ -3,7 +3,6 @@ import { useEffect } from "react"; import { GoogleReCaptcha, GoogleReCaptchaProvider } from "react-google-recaptcha-v3"; import HCaptcha from "@hcaptcha/react-hcaptcha"; -import { Turnstile } from "@marsidev/react-turnstile"; import { CaptchaProvider } from "@/models/captcha"; import "./captcha.css"; import { TurnstileWidget } from "./turnstile"; diff --git a/web/src/components/common/captcha/turnstile.tsx b/web/src/components/common/captcha/turnstile.tsx index 2a2ecf2..f2d6c21 100644 --- a/web/src/components/common/captcha/turnstile.tsx +++ b/web/src/components/common/captcha/turnstile.tsx @@ -52,13 +52,13 @@ export function TurnstileWidget(props: CaptchaProps) { const handleSuccess = (token: string) => { setStatus('success'); - props.onSuccess(token); + return props.onSuccess && props.onSuccess(token); }; const handleError = (error: string) => { setStatus('error'); setError(error); - props.onError && props.onError(error); + return props.onError && props.onError(error); }; useEffect(() => { @@ -66,11 +66,11 @@ export function TurnstileWidget(props: CaptchaProps) { if (status === 'loading') { setStatus('error'); setError('timeout'); - props.onError && props.onError('timeout'); + return props.onError && props.onError('timeout'); } }, TURNSTILE_TIMEOUT * 1000); return () => clearTimeout(timer); - }) + }, [status, props]); return (
diff --git a/web/src/components/common/gravatar.tsx b/web/src/components/common/gravatar.tsx deleted file mode 100644 index f4ccfd8..0000000 --- a/web/src/components/common/gravatar.tsx +++ /dev/null @@ -1,70 +0,0 @@ -"use client"; - -import React from "react"; -import Image from "next/image"; -import crypto from "crypto"; - -// 生成 Gravatar URL 的函数 -function getGravatarUrl(email: string, size: number = 200, defaultType: string = "identicon"): string { - const hash = crypto.createHash('md5').update(email.toLowerCase().trim()).digest('hex'); - return `https://www.gravatar.com/avatar/${hash}?s=${size}&d=${defaultType}`; -} - -interface GravatarAvatarProps { - email: string; - size?: number; - className?: string; - alt?: string; - url?: string; - defaultType?: 'mm' | 'identicon' | 'monsterid' | 'wavatar' | 'retro' | 'robohash' | 'blank'; -} - -const GravatarAvatar: React.FC = ({ - email, - size = 200, - className = "", - alt = "avatar", - url, - defaultType = "identicon" -}) => { - // 把尺寸控制交给父组件的 wrapper(父组件通过 tailwind 的 w-.. h-.. 控制) - const gravatarUrl = url && url.trim() !== "" ? url : getGravatarUrl(email, size , defaultType); - - return ( -
- {alt} -
- ); -}; - -// 用户类型定义(如果还没有的话) -interface User { - email?: string; - displayName?: string; - name?: string; - avatarUrl?: string; -} - -export function getGravatarByUser({user, className="", size=640}:{user?: User, className?: string, size?: number}): React.ReactElement { - if (!user) { - return ; - } - return ( - - ); -} - -export default GravatarAvatar; diff --git a/web/src/components/common/image-placeholder.tsx b/web/src/components/common/image-placeholder.tsx deleted file mode 100644 index a743816..0000000 --- a/web/src/components/common/image-placeholder.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function ImagePlaceholder() { - return ( -
- Image Placeholder -
- ); -} \ No newline at end of file diff --git a/web/src/components/common/markdown-codeblock.tsx b/web/src/components/common/markdown-codeblock.tsx index 3fd993e..78a33a4 100644 --- a/web/src/components/common/markdown-codeblock.tsx +++ b/web/src/components/common/markdown-codeblock.tsx @@ -46,7 +46,7 @@ export default function CodeBlock(props: React.ComponentPropsWithoutRef<"pre">) codeContent = extractText(child.props.children); } - async function handleCopy(e: React.MouseEvent) { + async function handleCopy() { try { const ok = await copyToClipboard(codeContent); if (ok) toast.success(t("copy_success")); diff --git a/web/src/components/common/pagination.tsx b/web/src/components/common/pagination.tsx index 1ec5c77..726da68 100644 --- a/web/src/components/common/pagination.tsx +++ b/web/src/components/common/pagination.tsx @@ -21,15 +21,10 @@ export function PaginationController({ buttons?: number onPageChange?: (page: number) => void } & React.HTMLAttributes) { - // normalize buttons const btns = Math.max(5, buttons ?? 7); const buttonsToShow = totalPages < btns ? totalPages : btns; - // rely on shadcn buttonVariants and PaginationLink's isActive prop for styling const [currentPage, setCurrentPage] = useState(() => Math.min(Math.max(1, initialPage ?? 1), Math.max(1, totalPages))); - const [direction, setDirection] = useState(0) // 1 = forward (right->left), -1 = backward - - // sync when initialPage or totalPages props change useEffect(() => { const p = Math.min(Math.max(1, initialPage ?? 1), Math.max(1, totalPages)); setCurrentPage(p); @@ -37,10 +32,9 @@ export function PaginationController({ const handleSetPage = useCallback((p: number) => { const next = Math.min(Math.max(1, Math.floor(p)), Math.max(1, totalPages)); - setDirection(next > currentPage ? 1 : next < currentPage ? -1 : 0); setCurrentPage(next); if (typeof onPageChange === 'function') onPageChange(next); - }, [onPageChange, totalPages, currentPage]); + }, [onPageChange, totalPages]); // helper to render page link const renderPage = (pageNum: number) => ( diff --git a/web/src/components/console/app-sidebar.tsx b/web/src/components/console/app-sidebar.tsx index 9c43737..8356f8d 100644 --- a/web/src/components/console/app-sidebar.tsx +++ b/web/src/components/console/app-sidebar.tsx @@ -1,29 +1,16 @@ "use client" - -import * as React from "react" +import { useEffect, useState } from "react" import { - IconCamera, IconChartBar, IconDashboard, - IconDatabase, - IconFileAi, - IconFileDescription, - IconFileWord, IconFolder, - IconHelp, IconInnerShadowTop, IconListDetails, - IconReport, - IconSearch, - IconSettings, IconUsers, } from "@tabler/icons-react" -import { NavDocuments } from "@/components/console/nav-documents" import { NavMain } from "@/components/console/nav-main" -import { NavSecondary } from "@/components/console/nav-secondary" import { NavUser } from "@/components/console/nav-user" -import { metadata } from '../../app/layout'; import { Sidebar, SidebarContent, @@ -34,125 +21,53 @@ import { SidebarMenuItem, } from "@/components/ui/sidebar" import config from "@/config" +import Link from "next/link" +import { getLoginUser } from "@/api/user" +import { User } from "@/models/user" const data = { - user: { - name: "shadcn", - email: "m@example.com", - avatar: "/avatars/shadcn.jpg", - }, navMain: [ { - title: "Dashboard", - url: "#", + title: "大石坝", + url: "/console", icon: IconDashboard, }, { - title: "Lifecycle", - url: "#", + title: "文章管理", + url: "/console/post", icon: IconListDetails, }, { - title: "Analytics", - url: "#", + title: "评论管理", + url: "/console/comment", icon: IconChartBar, }, { - title: "Projects", - url: "#", + title: "文件管理", + url: "/console/file", icon: IconFolder, }, { - title: "Team", - url: "#", + title: "用户管理", + url: "/console/user", icon: IconUsers, }, - ], - navClouds: [ - { - title: "Capture", - icon: IconCamera, - isActive: true, - url: "#", - items: [ - { - title: "Active Proposals", - url: "#", - }, - { - title: "Archived", - url: "#", - }, - ], - }, - { - title: "Proposal", - icon: IconFileDescription, - url: "#", - items: [ - { - title: "Active Proposals", - url: "#", - }, - { - title: "Archived", - url: "#", - }, - ], - }, - { - title: "Prompts", - icon: IconFileAi, - url: "#", - items: [ - { - title: "Active Proposals", - url: "#", - }, - { - title: "Archived", - url: "#", - }, - ], - }, - ], - navSecondary: [ - { - title: "Settings", - url: "#", - icon: IconSettings, - }, - { - title: "Get Help", - url: "#", - icon: IconHelp, - }, - { - title: "Search", - url: "#", - icon: IconSearch, - }, - ], - documents: [ - { - name: "Data Library", - url: "#", - icon: IconDatabase, - }, - { - name: "Reports", - url: "#", - icon: IconReport, - }, - { - name: "Word Assistant", - url: "#", - icon: IconFileWord, - }, - ], + ] } export function AppSidebar({ ...props }: React.ComponentProps) { + const [loginUser, setLoginUser] = useState(null); + + useEffect(() => { + getLoginUser().then(resp => { + setLoginUser(resp.data); + }); + }, []) + + if (!loginUser) { + return null; // 或者返回一个加载指示器 + } + return ( @@ -162,21 +77,19 @@ export function AppSidebar({ ...props }: React.ComponentProps) { asChild className="data-[slot=sidebar-menu-button]:!p-1.5" > - + {config.metadata.name} - + - - - + ) diff --git a/web/src/components/console/nav-main.tsx b/web/src/components/console/nav-main.tsx index 4ea34d8..fa8a3c4 100644 --- a/web/src/components/console/nav-main.tsx +++ b/web/src/components/console/nav-main.tsx @@ -1,8 +1,7 @@ "use client" -import { IconCirclePlusFilled, IconMail, type Icon } from "@tabler/icons-react" +import { type Icon } from "@tabler/icons-react" -import { Button } from "@/components/ui/button" import { SidebarGroup, SidebarGroupContent, @@ -10,6 +9,7 @@ import { SidebarMenuButton, SidebarMenuItem, } from "@/components/ui/sidebar" +import Link from "next/link" export function NavMain({ items, @@ -26,10 +26,12 @@ export function NavMain({ {items.map((item) => ( - - {item.icon && } - {item.title} - + + + {item.icon && } + {item.title} + + ))} diff --git a/web/src/components/console/nav-user.tsx b/web/src/components/console/nav-user.tsx index 7c49dc7..bb38069 100644 --- a/web/src/components/console/nav-user.tsx +++ b/web/src/components/console/nav-user.tsx @@ -28,15 +28,14 @@ import { SidebarMenuItem, useSidebar, } from "@/components/ui/sidebar" +import { User } from "@/models/user" +import { getGravatarFromUser } from "@/utils/common/gravatar" +import { getFallbackAvatarFromUsername } from "@/utils/common/username" export function NavUser({ user, }: { - user: { - name: string - email: string - avatar: string - } + user: User }) { const { isMobile } = useSidebar() @@ -49,12 +48,12 @@ export function NavUser({ size="lg" className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground" > - - - CN + + + {getFallbackAvatarFromUsername(user.nickname || user.username)}
- {user.name} + {user.nickname}({user.username}) {user.email} @@ -70,12 +69,12 @@ export function NavUser({ >
- - - CN + + + {getFallbackAvatarFromUsername(user.nickname || user.username)}
- {user.name} + {user.nickname}({user.username}) {user.email} diff --git a/web/src/components/layout/avatar-with-dropdown-menu.tsx b/web/src/components/layout/avatar-with-dropdown-menu.tsx index 1897ccc..f7e9869 100644 --- a/web/src/components/layout/avatar-with-dropdown-menu.tsx +++ b/web/src/components/layout/avatar-with-dropdown-menu.tsx @@ -5,15 +5,9 @@ import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, - DropdownMenuPortal, DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" -import GravatarAvatar from "../common/gravatar" import { User } from "@/models/user"; import { useEffect, useState } from "react"; import { getLoginUser, userLogout } from "@/api/user"; @@ -21,6 +15,9 @@ import Link from "next/link"; import { toast } from "sonner"; import { useToLogin } from "@/hooks/use-route"; import { CircleUser } from "lucide-react"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { getGravatarFromUser } from "@/utils/common/gravatar"; +import { getFallbackAvatarFromUsername } from "@/utils/common/username"; export function AvatarWithDropdownMenu() { const [user, setUser] = useState(null); @@ -44,7 +41,10 @@ export function AvatarWithDropdownMenu() { @@ -58,29 +58,6 @@ export function AvatarWithDropdownMenu() { - - Team - - Invite users - - - Email - Message - - More... - - - - - New Team - ⌘+T - - - - GitHub - Support - API - {user ? `Logout (${user.username})` : "Login"} diff --git a/web/src/components/layout/navbar-or-side.tsx b/web/src/components/layout/navbar-or-side.tsx index 4e5590f..ece03ff 100644 --- a/web/src/components/layout/navbar-or-side.tsx +++ b/web/src/components/layout/navbar-or-side.tsx @@ -46,7 +46,7 @@ const navbarMenuComponents = [ ] export function Navbar() { - const { navbarAdditionalClassName, setMode, mode } = useDevice() + const { navbarAdditionalClassName} = useDevice() return (