diff --git a/client/package-lock.json b/client/package-lock.json index 3dd7584..d23ac36 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -69,7 +69,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -719,7 +718,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.28.6.tgz", "integrity": "sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, @@ -1603,7 +1601,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-module-imports": "^7.28.6", @@ -3346,7 +3343,6 @@ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -3824,7 +3820,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", @@ -3878,7 +3873,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -4248,7 +4242,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4347,7 +4340,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5287,7 +5279,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -7122,7 +7113,6 @@ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -9873,7 +9863,6 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^27.5.1", "import-local": "^3.0.2", @@ -10771,7 +10760,6 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -12227,7 +12215,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -13362,7 +13349,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -13744,7 +13730,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -13876,7 +13861,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -13901,7 +13885,6 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -14399,7 +14382,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", "license": "MIT", - "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -14642,7 +14624,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -16026,7 +16007,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -16205,7 +16185,6 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "license": "(MIT OR CC0-1.0)", - "peer": true, "engines": { "node": ">=10" }, @@ -16642,7 +16621,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -16714,7 +16692,6 @@ "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", "license": "MIT", - "peer": true, "dependencies": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -17128,7 +17105,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", diff --git a/client/package.json b/client/package.json index 40d2ca4..7054add 100644 --- a/client/package.json +++ b/client/package.json @@ -41,5 +41,14 @@ }, "devDependencies": { "nodemon": "^3.1.14" + }, + "jest": { + "moduleNameMapper": { + "react-router-dom": "/node_modules/react-router-dom/dist/index.js", + "react-router/dom": "/node_modules/react-router/dist/development/dom-export.js" + }, + "transformIgnorePatterns": [ + "/node_modules/(?!(axios)/)" + ] } } diff --git a/client/src/App.js b/client/src/App.js index 6cbd04d..d8ff992 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -1,62 +1,56 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, lazy, Suspense } from 'react'; import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import './App.css'; import Navbar from './components/Navbar'; import Login from './components/Login'; -import Home from './components/Home'; -import Section from './components/Section'; -import Post from './components/Post'; -import Admin from './components/Admin'; -import Profile from './components/Profile'; + +const Home = lazy(() => import('./components/Home')); +const Section = lazy(() => import('./components/Section')); +const Post = lazy(() => import('./components/Post')); +const Admin = lazy(() => import('./components/Admin')); +const Profile = lazy(() => import('./components/Profile')); +const UserProfile = lazy(() => import('./components/UserProfile')); function App() { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); - useEffect(() => { const token = localStorage.getItem('token'); const userData = localStorage.getItem('user'); - if (token && userData) { - setUser(JSON.parse(userData)); - } + if (token && userData) { setUser(JSON.parse(userData)); } setLoading(false); }, []); - const handleLogin = (token, userData) => { localStorage.setItem('token', token); localStorage.setItem('user', JSON.stringify(userData)); setUser(userData); }; - const handleUserUpdate = (updatedUser) => { localStorage.setItem('user', JSON.stringify(updatedUser)); setUser(updatedUser); }; - const handleLogout = () => { localStorage.removeItem('token'); localStorage.removeItem('user'); setUser(null); }; - - if (loading) { - return
加载中...
; - } - + if (loading) { return
加载中...
; } return ( - - : } /> - : } /> - : } /> - : } /> - : } /> - {user?.role === 'admin' && } />} - } /> - + 加载中...}> + + : } /> + : } /> + : } /> + : } /> + : } /> + : } /> + {user?.role === 'admin' && } />} + } /> + + ); } - export default App; diff --git a/client/src/App.test.js b/client/src/App.test.js index 1f03afe..06bedf0 100644 --- a/client/src/App.test.js +++ b/client/src/App.test.js @@ -1,8 +1,8 @@ -import { render, screen } from '@testing-library/react'; +import { render } from '@testing-library/react'; import App from './App'; -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); +test('App renders without crashing', () => { + Storage.prototype.getItem = jest.fn(() => null); + const { container } = render(); + expect(container).toBeTruthy(); }); diff --git a/client/src/components/Admin.js b/client/src/components/Admin.js index c70c75f..fea58ac 100644 --- a/client/src/components/Admin.js +++ b/client/src/components/Admin.js @@ -11,6 +11,7 @@ export default function Admin({ user }) { useEffect(() => { fetchData(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeTab]); const fetchData = async () => { diff --git a/client/src/components/Home.css b/client/src/components/Home.css index d954e85..cd82941 100644 --- a/client/src/components/Home.css +++ b/client/src/components/Home.css @@ -166,4 +166,64 @@ .modal-buttons button:hover { transform: translateY(-2px); -} \ No newline at end of file +} +.home-controls { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1.5rem; + flex-wrap: wrap; +} + +.tabs { + display: flex; + gap: 0.5rem; +} + +.tab-btn { + padding: 0.5rem 1.25rem; + border: 2px solid #667eea; + border-radius: 20px; + background: white; + color: #667eea; + cursor: pointer; + font-size: 14px; + font-weight: 600; + transition: all 0.2s; +} + +.tab-btn.active, +.tab-btn:hover { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-color: transparent; +} + +.search-bar { + flex: 1; + max-width: 400px; +} + +.search-input { + width: 100%; + padding: 0.6rem 1rem; + border: 1px solid #ddd; + border-radius: 20px; + font-size: 14px; + outline: none; + transition: border-color 0.2s, box-shadow 0.2s; + box-sizing: border-box; +} + +.search-input:focus { + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.error-message { + background: #fee; + color: #c00; + padding: 0.75rem 1rem; + border-radius: 4px; + margin-bottom: 1rem; +} diff --git a/client/src/components/Home.js b/client/src/components/Home.js index 1108676..82dab9c 100644 --- a/client/src/components/Home.js +++ b/client/src/components/Home.js @@ -5,64 +5,85 @@ import './Home.css'; export default function Home({ user }) { const [sections, setSections] = useState([]); + const [subscribedSections, setSubscribedSections] = useState([]); + const [activeTab, setActiveTab] = useState('all'); const [showModal, setShowModal] = useState(false); const [name, setName] = useState(''); const [description, setDescription] = useState(''); + const [searchQuery, setSearchQuery] = useState(''); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); - useEffect(() => { - fetchSections(); - }, []); + useEffect(() => { fetchSections(); fetchSubscribed(); }, []); const fetchSections = async () => { try { const response = await axios.get('/api/sections'); setSections(response.data); - } catch (err) { - setError('加载分区失败'); - } finally { - setLoading(false); - } + } catch (err) { setError('加载分区失败'); } + finally { setLoading(false); } + }; + + const fetchSubscribed = async () => { + try { + const token = localStorage.getItem('token'); + const response = await axios.get('/api/sections/subscribed', { headers: { Authorization: `Bearer ${token}` } }); + setSubscribedSections(response.data); + } catch (err) { /* not critical */ } }; const handleCreateSection = async (e) => { e.preventDefault(); setError(''); - try { const token = localStorage.getItem('token'); - await axios.post('/api/sections', { name, description }, { - headers: { Authorization: `Bearer ${token}` } - }); - - setName(''); - setDescription(''); - setShowModal(false); - - if (user.role === 'admin') { - fetchSections(); - } else { - alert('分区创建成功!需要管理员审核后才能显示。'); - } - } catch (err) { - setError(err.response?.data?.message || '创建失败'); - } + await axios.post('/api/sections', { name, description }, { headers: { Authorization: `Bearer ${token}` } }); + setName(''); setDescription(''); setShowModal(false); + if (user.role === 'admin') { fetchSections(); } + else { alert('分区创建成功!需要管理员审核后才能显示。'); } + } catch (err) { setError(err.response?.data?.message || '创建失败'); } }; - if (loading) return
加载中...
; + const filteredSections = () => { + const source = activeTab === 'subscribed' ? subscribedSections : sections; + if (!searchQuery.trim()) return source; + const q = searchQuery.toLowerCase(); + return source.filter(s => + s.name?.toLowerCase().includes(q) || s.description?.toLowerCase().includes(q) + ); + }; + if (loading) return
加载中...
; return (

分区列表

- +
- {error &&
{error}
} +
+
+ + +
+
+ setSearchQuery(e.target.value)} + className="search-input" + /> +
+
+ {error &&
{error}
} {showModal && (
@@ -70,22 +91,11 @@ export default function Home({ user }) {
- setName(e.target.value)} - required - placeholder="输入分区名称" - /> + setName(e.target.value)} required placeholder="输入分区名称" />
-