-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathApp.tsx
More file actions
254 lines (230 loc) · 9.66 KB
/
Copy pathApp.tsx
File metadata and controls
254 lines (230 loc) · 9.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
import React, { useState, useEffect } from 'react';
import { TASK_POOL, MOCK_HISTORY } from './constants';
import { Task, ViewState, CheckInLog, TaskCategory } from './types';
import { generateRewardComment } from './services/geminiService';
import { Envelope } from './components/Envelope';
import { TaskCard } from './components/TaskCard';
import { CheckInForm } from './components/CheckInForm';
import { HistoryList } from './components/HistoryList';
import { Home, User, Layers, Filter } from 'lucide-react';
export default function App() {
const [view, setView] = useState<ViewState>('home');
const [activeTask, setActiveTask] = useState<Task | null>(null);
const [swapsLeft, setSwapsLeft] = useState(1);
const [isTaskAccepted, setIsTaskAccepted] = useState(false);
const [history, setHistory] = useState<CheckInLog[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
// New State for Category Selection
const [selectedCategory, setSelectedCategory] = useState<TaskCategory | 'ALL'>('ALL');
// Load history from local storage on mount
useEffect(() => {
const saved = localStorage.getItem('dq_history');
if (saved) {
setHistory(JSON.parse(saved));
} else {
setHistory(MOCK_HISTORY); // Seed with mock data for MVP
}
}, []);
const getRandomTask = (excludeIds: string[], categoryFilter: TaskCategory | 'ALL') => {
// 1. Filter by exclusion list
let candidates = TASK_POOL.filter(t => !excludeIds.includes(t.id));
// 2. Filter by category if selected
if (categoryFilter !== 'ALL') {
const categoryCandidates = candidates.filter(t => t.category === categoryFilter);
// Only apply filter if we actually have tasks in that category
if (categoryCandidates.length > 0) {
candidates = categoryCandidates;
} else {
// Fallback: If no tasks left in this category, we might alert user, but for MVP we fallback to random
// alert("该分类下的新任务已全部完成,为您随机抽取其他任务!");
}
}
if (candidates.length === 0) return TASK_POOL[0]; // Absolute fallback
const randomIndex = Math.floor(Math.random() * candidates.length);
return candidates[randomIndex];
};
const handleOpenEnvelope = () => {
// Ensure we don't pick a task completed recently
const completedIds = history.map(h => h.taskId);
const newTask = getRandomTask(completedIds, selectedCategory);
setActiveTask(newTask);
setView('task');
};
const handleSwap = () => {
if (swapsLeft > 0 && activeTask) {
// Swapping also respects the current category selection
const newTask = getRandomTask([activeTask.id, ...history.map(h=>h.taskId)], selectedCategory);
setActiveTask(newTask);
setSwapsLeft(prev => prev - 1);
}
};
const handleAccept = () => {
setIsTaskAccepted(true);
};
const handleGoToCheckIn = () => {
setView('checkin');
};
const handleCompleteTask = async (photo: File | null, comment: string) => {
if (!activeTask) return;
setIsSubmitting(true);
try {
// 1. Get AI encouragement
const aiReward = await generateRewardComment(activeTask.title, comment);
// 2. Create Record
const newLog: CheckInLog = {
id: Date.now().toString(),
taskId: activeTask.id,
taskTitle: activeTask.title,
photoUrl: photo ? URL.createObjectURL(photo) : null, // In a real app, upload to S3/Cloudinary
comment: comment,
aiRewardComment: aiReward,
timestamp: Date.now()
};
// 3. Update State
const updatedHistory = [newLog, ...history];
setHistory(updatedHistory);
localStorage.setItem('dq_history', JSON.stringify(updatedHistory));
// 4. Reset Daily State (Simulated)
setSwapsLeft(1);
setIsTaskAccepted(false);
setActiveTask(null);
// 5. Navigate
setView('profile');
} catch (e) {
console.error(e);
alert("连接有点问题,请稍后再试。");
} finally {
setIsSubmitting(false);
}
};
// Render Logic
const renderContent = () => {
switch(view) {
case 'home':
if (activeTask && isTaskAccepted) {
// If returning to home but already have an active mission
return (
<div className="flex flex-col items-center justify-center h-[70vh]">
<h2 className="text-2xl font-black mb-4">任务进行中</h2>
<button
onClick={() => setView('task')}
className="bg-neoyellow font-bold px-8 py-4 border-2 border-black shadow-neo rounded-xl transition-transform active:translate-y-1 active:shadow-none"
>
查看当前任务
</button>
</div>
);
}
return (
<div className="px-4 py-6 flex flex-col h-full">
<div className="mb-2">
<h1 className="text-4xl font-black mb-1">每日奇遇</h1>
<p className="text-gray-600 font-medium text-sm">选择你的心情,领取今日微型冒险。</p>
</div>
{/* Category Selector */}
<div className="mb-4">
<div className="flex gap-3 overflow-x-auto pb-4 pt-2 no-scrollbar scroll-smooth">
<button
onClick={() => setSelectedCategory('ALL')}
className={`
px-5 py-2 rounded-full border-2 border-black font-bold text-sm whitespace-nowrap transition-all flex-shrink-0
${selectedCategory === 'ALL'
? 'bg-black text-white shadow-neo-sm translate-y-[-2px]'
: 'bg-white text-black hover:bg-gray-100'}
`}
>
🎲 随机惊喜
</button>
{Object.values(TaskCategory).map((cat) => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={`
px-5 py-2 rounded-full border-2 border-black font-bold text-sm whitespace-nowrap transition-all flex-shrink-0
${selectedCategory === cat
? 'bg-neoyellow shadow-neo-sm translate-y-[-2px]'
: 'bg-white hover:bg-gray-100'}
`}
>
{cat}
</button>
))}
</div>
</div>
<div className="flex-grow flex flex-col justify-center pb-12">
<Envelope onOpen={handleOpenEnvelope} />
<p className="text-center text-xs font-bold text-gray-400 mt-8">
{selectedCategory === 'ALL'
? "未知是生活中最大的浪漫"
: `准备好去体验${selectedCategory}的乐趣了吗?`}
</p>
</div>
</div>
);
case 'task':
return activeTask ? (
<div className="px-4 py-8">
<div className="flex items-center mb-4">
<button onClick={() => setView('home')} className="font-bold underline text-sm">返回首页</button>
</div>
<TaskCard
task={activeTask}
onAccept={handleAccept}
onSwap={handleSwap}
swapsLeft={swapsLeft}
isAccepted={isTaskAccepted}
onCheckIn={handleGoToCheckIn}
/>
</div>
) : null;
case 'checkin':
return activeTask ? (
<div className="px-4 py-8">
<div className="flex items-center mb-4">
<button onClick={() => setView('task')} className="font-bold underline text-sm">返回任务</button>
</div>
<CheckInForm
taskTitle={activeTask.title}
onSubmit={handleCompleteTask}
isSubmitting={isSubmitting}
/>
</div>
) : null;
case 'profile':
return (
<div className="px-4 py-8">
<h1 className="text-3xl font-black mb-6">手账本</h1>
<HistoryList logs={history} />
</div>
);
}
};
return (
<div className="min-h-screen max-w-md mx-auto bg-neobg shadow-2xl relative overflow-hidden flex flex-col">
{/* Main Content Area */}
<main className="flex-grow overflow-y-auto overflow-x-hidden">
{renderContent()}
</main>
{/* Bottom Navigation */}
<nav className="border-t-2 border-black bg-white sticky bottom-0 z-50 pb-safe">
<div className="flex justify-around items-center p-4">
<button
onClick={() => setView('home')}
className={`flex flex-col items-center transition-colors ${view === 'home' || view === 'task' ? 'text-black' : 'text-gray-400'}`}
>
<Home className="mb-1" size={24} strokeWidth={2.5} />
<span className="text-xs font-bold">奇遇</span>
</button>
<div className="w-[1px] h-8 bg-gray-200"></div>
<button
onClick={() => setView('profile')}
className={`flex flex-col items-center transition-colors ${view === 'profile' ? 'text-black' : 'text-gray-400'}`}
>
<Layers className="mb-1" size={24} strokeWidth={2.5} />
<span className="text-xs font-bold">记录</span>
</button>
</div>
</nav>
</div>
);
}