Skip to content

Commit bd49abb

Browse files
CumuloCumuloclaude
andcommitted
add registration, discovery, and profile setup features
Implement full user registration/login flow with JWT auth, discovery page with soul cards and match requests, and a dual-column glassmorphism profile setup page with categorized interest tags. Restyle discovery cards with CSS classes for cleaner hover effects and font-weight hierarchy. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2ddeb8a commit bd49abb

39 files changed

Lines changed: 3455 additions & 129 deletions

client/src/App.tsx

Lines changed: 104 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
1-
import { useState, useMemo } from 'react';
1+
import { useState, useMemo, useEffect } from 'react';
22
import { motion } from 'motion/react';
33
import { ChatPage } from './pages/ChatPage';
4+
import { LoginPage } from './pages/LoginPage';
5+
import { RegisterPage } from './pages/RegisterPage';
6+
import { ProfileSetupPage } from './pages/ProfileSetupPage';
7+
import { DiscoveryPage } from './pages/DiscoveryPage';
8+
import { useAuthStore } from './stores/authStore';
49
import { useDevData, type DevUser, type DevRelationship } from './hooks/useDevData';
510

611
const DEV_MODE = import.meta.env.DEV;
712

8-
type Identity = 'CLIENT' | 'WINGMAN';
9-
1013
function App() {
1114
if (DEV_MODE) {
1215
return (
1316
<>
1417
<AmbientBackground />
1518
<NoiseOverlay />
16-
<DevLoginPage />
19+
<DevApp />
1720
</>
1821
);
1922
}
@@ -22,28 +25,40 @@ function App() {
2225
<>
2326
<AmbientBackground />
2427
<NoiseOverlay />
25-
<ProdLoginPage />
28+
<ProdApp />
2629
</>
2730
);
2831
}
2932

30-
// ─── DEV Mode: Selection-based Login ───────────────────────────
33+
// ─── DEV Mode: Selection-based Login (preserved) ──────────────
3134

32-
function DevLoginPage() {
35+
function DevApp() {
3336
const { users, relationships, loading } = useDevData();
34-
const [identity, setIdentity] = useState<Identity | null>(null);
37+
const [identity, setIdentity] = useState<'CLIENT' | 'WINGMAN' | null>(null);
3538
const [selectedUser, setSelectedUser] = useState<DevUser | null>(null);
3639
const [selectedRel, setSelectedRel] = useState<DevRelationship | null>(null);
3740
const [started, setStarted] = useState(false);
41+
const [useRealAuth, setUseRealAuth] = useState(false);
42+
43+
// Use authStore page state for new pages
44+
const page = useAuthStore((s) => s.page);
45+
const authUser = useAuthStore((s) => s.user);
46+
const chatRelationshipId = useAuthStore((s) => s.chatRelationshipId);
47+
const fetchMe = useAuthStore((s) => s.fetchMe);
48+
const exitChat = useAuthStore((s) => s.exitChat);
49+
const setPage = useAuthStore((s) => s.setPage);
50+
51+
// Try to resume session
52+
useEffect(() => {
53+
fetchMe();
54+
}, [fetchMe]);
3855

39-
// Filter users by selected identity
4056
const filteredUsers = useMemo(() => {
4157
if (!identity) return [];
4258
if (identity === 'CLIENT') return users.filter((u) => u.roles.includes('CLIENT'));
4359
return users.filter((u) => u.roles.includes('WINGMAN'));
4460
}, [users, identity]);
4561

46-
// Filter relationships by selected user
4762
const filteredRels = useMemo(() => {
4863
if (!selectedUser) return [];
4964
if (identity === 'CLIENT') {
@@ -56,17 +71,13 @@ function DevLoginPage() {
5671
);
5772
}, [relationships, selectedUser, identity]);
5873

59-
// Compute ChatPage props
6074
const chatUserId = selectedUser?.id ?? '';
61-
const chatRelationshipId = selectedRel?.id ?? '';
75+
const chatRelationshipIdDev = selectedRel?.id ?? '';
6276

63-
// 当事人:wingmanId = 己方军师 ID,privateChatTargetId = 同上
64-
// 军师:wingmanId = undefined(不控制模式),privateChatTargetId = 己方当事人 ID
6577
const { chatWingmanId, chatPrivateTargetId } = useMemo(() => {
6678
if (!selectedRel || !selectedUser) return { chatWingmanId: undefined, chatPrivateTargetId: undefined };
6779

6880
if (identity === 'CLIENT') {
69-
// 判断当事人是 user1 还是 user2,取对应 side 的军师
7081
const side = selectedRel.user1.id === selectedUser.id ? 1 : 2;
7182
const assignment = selectedRel.assignments.find((a) => a.side === side);
7283
return {
@@ -75,7 +86,6 @@ function DevLoginPage() {
7586
};
7687
}
7788

78-
// WINGMAN:私聊对象是己方当事人
7989
const assignment = selectedRel.assignments.find((a) => a.userId === selectedUser.id);
8090
const clientUser = assignment?.side === 1 ? selectedRel.user1 : selectedRel.user2;
8191
return {
@@ -84,14 +94,40 @@ function DevLoginPage() {
8494
};
8595
}, [identity, selectedRel, selectedUser]);
8696

87-
if (started && chatUserId && chatRelationshipId) {
97+
// ── All hooks above this line. Early returns below. ──
98+
99+
// If in chat mode (from discovery acceptance)
100+
if (page === 'chat' && authUser && chatRelationshipId) {
101+
return (
102+
<ChatPage
103+
userId={authUser.id}
104+
relationshipId={chatRelationshipId}
105+
onExit={exitChat}
106+
/>
107+
);
108+
}
109+
110+
// If authenticated, show production-style pages
111+
if (authUser && page !== 'login' && page !== 'register') {
112+
if (page === 'profile-setup') return <ProfileSetupPage />;
113+
if (page === 'discovery') return <DiscoveryPage />;
114+
}
115+
116+
// If user chose real auth flow
117+
if (useRealAuth) {
118+
if (page === 'register') return <><AmbientBackground /><NoiseOverlay /><RegisterPage /></>;
119+
if (page === 'discovery' && authUser) return <><AmbientBackground /><NoiseOverlay /><DiscoveryPage /></>;
120+
return <><AmbientBackground /><NoiseOverlay /><LoginPage /></>;
121+
}
122+
123+
if (started && chatUserId && chatRelationshipIdDev) {
88124
return (
89125
<>
90126
<AmbientBackground />
91127
<NoiseOverlay />
92128
<ChatPage
93129
userId={chatUserId}
94-
relationshipId={chatRelationshipId}
130+
relationshipId={chatRelationshipIdDev}
95131
wingmanId={chatWingmanId}
96132
privateChatTargetId={chatPrivateTargetId}
97133
onExit={() => {
@@ -140,6 +176,15 @@ function DevLoginPage() {
140176
>
141177
DEV MODE
142178
</motion.span>
179+
<div className="mt-4">
180+
<button
181+
onClick={() => { setUseRealAuth(true); setPage('login'); }}
182+
className="text-xs transition-colors"
183+
style={{ color: '#8ca0ff' }}
184+
>
185+
使用注册/登录 →
186+
</button>
187+
</div>
143188
</motion.div>
144189

145190
{loading ? (
@@ -170,12 +215,11 @@ function DevLoginPage() {
170215
</div>
171216
</StepSection>
172217

173-
{/* Step 2: User */}
174218
{identity && (
175219
<StepSection label="2. 选择用户">
176220
<div className="space-y-2">
177221
{filteredUsers.map((u) => (
178-
<UserCard
222+
<DevUserCard
179223
key={u.id}
180224
user={u}
181225
selected={selectedUser?.id === u.id}
@@ -190,7 +234,6 @@ function DevLoginPage() {
190234
</StepSection>
191235
)}
192236

193-
{/* Step 3: Relationship */}
194237
{selectedUser && (
195238
<StepSection label="3. 选择聊天室">
196239
<div className="space-y-2">
@@ -209,7 +252,6 @@ function DevLoginPage() {
209252
</StepSection>
210253
)}
211254

212-
{/* Enter button */}
213255
{selectedRel && (
214256
<motion.button
215257
onClick={() => setStarted(true)}
@@ -243,6 +285,45 @@ function DevLoginPage() {
243285
);
244286
}
245287

288+
// ─── Prod Mode: Auth-based routing ───────────────────────────
289+
290+
function ProdApp() {
291+
const page = useAuthStore((s) => s.page);
292+
const user = useAuthStore((s) => s.user);
293+
const loading = useAuthStore((s) => s.loading);
294+
const chatRelationshipId = useAuthStore((s) => s.chatRelationshipId);
295+
const fetchMe = useAuthStore((s) => s.fetchMe);
296+
const exitChat = useAuthStore((s) => s.exitChat);
297+
298+
useEffect(() => {
299+
fetchMe();
300+
}, [fetchMe]);
301+
302+
if (loading) {
303+
return (
304+
<div className="min-h-screen flex items-center justify-center">
305+
<p className="text-sm" style={{ color: '#9e98aa' }}>加载中...</p>
306+
</div>
307+
);
308+
}
309+
310+
if (page === 'chat' && user && chatRelationshipId) {
311+
return (
312+
<ChatPage
313+
userId={user.id}
314+
relationshipId={chatRelationshipId}
315+
onExit={exitChat}
316+
/>
317+
);
318+
}
319+
320+
if (page === 'register') return <RegisterPage />;
321+
if (page === 'profile-setup') return <ProfileSetupPage />;
322+
if (page === 'discovery' && user) return <DiscoveryPage />;
323+
324+
return <LoginPage />;
325+
}
326+
246327
// ─── Shared UI Components ──────────────────────────────────────
247328

248329
function StepSection({ label, children }: { label: string; children: React.ReactNode }) {
@@ -273,7 +354,7 @@ function SelectCard({ selected, onClick, label, desc }: {
273354
);
274355
}
275356

276-
function UserCard({ user, selected, onClick, isWingman }: {
357+
function DevUserCard({ user, selected, onClick, isWingman }: {
277358
user: DevUser; selected: boolean; onClick: () => void; isWingman: boolean;
278359
}) {
279360
const icon = isWingman ? '\uD83C\uDFAF' : '\uD83D\uDC64';
@@ -345,94 +426,6 @@ function RelationshipCard({ rel, selected, onClick }: {
345426
);
346427
}
347428

348-
// ─── Prod Mode: Placeholder for future login ───────────────────
349-
350-
function ProdLoginPage() {
351-
const [userId, setUserId] = useState('');
352-
const [relationshipId, setRelationshipId] = useState('');
353-
const [started, setStarted] = useState(false);
354-
355-
if (started && userId && relationshipId) {
356-
return (
357-
<>
358-
<AmbientBackground />
359-
<NoiseOverlay />
360-
<ChatPage
361-
userId={userId}
362-
relationshipId={relationshipId}
363-
onExit={() => setStarted(false)}
364-
/>
365-
</>
366-
);
367-
}
368-
369-
return (
370-
<div className="min-h-screen flex items-center justify-center px-4 relative">
371-
<div className="w-full max-w-sm">
372-
<div className="text-center mb-10">
373-
<h1
374-
className="text-ink text-[56px] font-light tracking-wide"
375-
style={{ fontFamily: 'var(--font-serif)', letterSpacing: '0.02em' }}
376-
>
377-
Violet
378-
</h1>
379-
<p className="mt-2 text-sm font-light" style={{ color: '#5a627a' }}>
380-
对话即心跳 — 校园恋爱代聊平台
381-
</p>
382-
</div>
383-
<div className="glass rounded-[28px] p-6 space-y-4">
384-
<div>
385-
<label className="block text-xs font-light mb-1.5" style={{ color: '#7a829a' }}>
386-
用户 ID
387-
</label>
388-
<input
389-
type="text"
390-
value={userId}
391-
onChange={(e) => setUserId(e.target.value)}
392-
placeholder="输入你的用户 ID"
393-
className="w-full h-11 px-4 rounded-2xl text-sm outline-none transition-all"
394-
style={{
395-
background: 'rgba(255, 255, 255, 0.5)',
396-
color: '#3a405a',
397-
border: '1px solid rgba(140, 160, 255, 0.15)',
398-
}}
399-
/>
400-
</div>
401-
<div>
402-
<label className="block text-xs font-light mb-1.5" style={{ color: '#7a829a' }}>
403-
关系 ID
404-
</label>
405-
<input
406-
type="text"
407-
value={relationshipId}
408-
onChange={(e) => setRelationshipId(e.target.value)}
409-
placeholder="输入关系 ID"
410-
className="w-full h-11 px-4 rounded-2xl text-sm outline-none transition-all"
411-
style={{
412-
background: 'rgba(255, 255, 255, 0.5)',
413-
color: '#3a405a',
414-
border: '1px solid rgba(140, 160, 255, 0.15)',
415-
}}
416-
/>
417-
</div>
418-
<button
419-
onClick={() => setStarted(true)}
420-
disabled={!userId.trim() || !relationshipId.trim()}
421-
className="w-full h-11 rounded-2xl text-sm font-medium transition-all disabled:opacity-30"
422-
style={{
423-
background: '#8ca0ff',
424-
color: '#ffffff',
425-
boxShadow: '0 8px 24px rgba(140, 160, 255, 0.35)',
426-
}}
427-
>
428-
进入聊天
429-
</button>
430-
</div>
431-
</div>
432-
</div>
433-
);
434-
}
435-
436429
// ─── Background Components ─────────────────────────────────────
437430

438431
function NoiseOverlay() {

0 commit comments

Comments
 (0)