Lianjx commited on
Commit
459775e
·
verified ·
1 Parent(s): 7f4eaca

Upload 71 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. App.tsx +1064 -0
  2. README.md +49 -6
  3. components/._AboutModal.tsx +0 -0
  4. components/._AdminDashboard.tsx +0 -0
  5. components/._AnalysisModal.tsx +0 -0
  6. components/._AuthModal.tsx +0 -0
  7. components/._ErrorBoundary.tsx +0 -0
  8. components/._ExitIntentModal.tsx +0 -0
  9. components/._Features.tsx +0 -0
  10. components/._FeedbackModal.tsx +0 -0
  11. components/._FilterSelector.tsx +0 -0
  12. components/._FloatingConcierge.tsx +0 -0
  13. components/._Header.tsx +0 -0
  14. components/._ImageUploader.tsx +0 -0
  15. components/._RedPacketModal.tsx +0 -0
  16. components/._ResultViewer.tsx +0 -0
  17. components/._ShareModal.tsx +0 -0
  18. components/._SlashModal.tsx +0 -0
  19. components/._StyleSelector.tsx +0 -0
  20. components/._Testimonials.tsx +0 -0
  21. components/._UserCenter.tsx +0 -0
  22. components/AboutModal.tsx +103 -0
  23. components/AdminDashboard.tsx +360 -0
  24. components/AnalysisModal.tsx +86 -0
  25. components/AuthModal.tsx +242 -0
  26. components/ErrorBoundary.tsx +52 -0
  27. components/ExitIntentModal.tsx +78 -0
  28. components/Features.tsx +60 -0
  29. components/FeedbackModal.tsx +146 -0
  30. components/FilterSelector.tsx +102 -0
  31. components/FloatingConcierge.tsx +82 -0
  32. components/Header.tsx +139 -0
  33. components/ImageUploader.tsx +128 -0
  34. components/RedPacketModal.tsx +135 -0
  35. components/ResultViewer.tsx +762 -0
  36. components/ShareModal.tsx +143 -0
  37. components/SlashModal.tsx +135 -0
  38. components/StyleSelector.tsx +1318 -0
  39. components/Testimonials.tsx +72 -0
  40. components/UserCenter.tsx +232 -0
  41. constants/._translations.ts +0 -0
  42. constants/translations.ts +965 -0
  43. index.css +48 -0
  44. index.html +45 -0
  45. index.tsx +32 -0
  46. manifest.json +24 -0
  47. metadata.json +5 -0
  48. nginx.conf +0 -0
  49. package.json +32 -0
  50. postcss.config.js +6 -0
App.tsx ADDED
@@ -0,0 +1,1064 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React, { useState, useRef, useEffect, Suspense } from 'react';
3
+ import { Header } from './components/Header';
4
+ import { ImageUploader } from './components/ImageUploader';
5
+ import { StyleSelector, STYLES } from './components/StyleSelector';
6
+ import { ResultViewer } from './components/ResultViewer';
7
+ import { FilterSelector, FILTERS } from './components/FilterSelector';
8
+ import { Features } from './components/Features';
9
+ import { Testimonials } from './components/Testimonials';
10
+ // Lazy load AdminDashboard
11
+ const AdminDashboard = React.lazy(() => import('./components/AdminDashboard').then(module => ({ default: module.AdminDashboard })));
12
+ import { UserCenter } from './components/UserCenter';
13
+ import { AuthModal } from './components/AuthModal';
14
+ import { AboutModal } from './components/AboutModal';
15
+ import { RedPacketModal } from './components/RedPacketModal';
16
+ import { SlashModal } from './components/SlashModal';
17
+ import { ShareModal } from './components/ShareModal';
18
+ import { AnalysisModal } from './components/AnalysisModal';
19
+ import { FeedbackModal } from './components/FeedbackModal';
20
+ import { generateStyledWeddingImage } from './services/geminiService';
21
+ import { crmService } from './services/crmService';
22
+ import { WeddingStyle, LeadData, UserAccount, FeedbackItem } from './types';
23
+ import { TRANSLATIONS } from './constants/translations';
24
+ import { Loader2, Wand2, AlertCircle, Layers, Sparkles, PenTool, Image as ImageIcon, Users, Calendar, X, CheckCircle, MapPin, Phone, MessageCircle, ScanFace, Timer, Bell, Lock, StopCircle, Maximize } from 'lucide-react';
25
+ // @ts-ignore
26
+ import confetti from 'canvas-confetti';
27
+ import { useUserStore, useUIStore, useGenerationStore } from './store';
28
+ import { FloatingConcierge } from './components/FloatingConcierge';
29
+ import { ExitIntentModal } from './components/ExitIntentModal';
30
+ import { ipService } from './services/ipService';
31
+
32
+ // Simple Toast Component
33
+ const Toast = ({ msg, onClose }: { msg: string, onClose: () => void }) => (
34
+ <div className="fixed top-24 left-1/2 -translate-x-1/2 z-[200] animate-fade-in-down pointer-events-none">
35
+ <div className="bg-gray-900/90 backdrop-blur text-white text-sm font-medium px-4 py-3 rounded-full shadow-2xl flex items-center gap-2 border border-white/10">
36
+ <CheckCircle className="w-4 h-4 text-green-400" />
37
+ {msg}
38
+ </div>
39
+ </div>
40
+ );
41
+
42
+ const App: React.FC = () => {
43
+ // --- ZUSTAND STORES ---
44
+ const {
45
+ currentUser, allUsers, leads, feedback, logs,
46
+ login, register, logout, updateUser, resetPassword, addPoints,
47
+ addLead, updateLeadStatus, deleteLead, addFeedback, addLog
48
+ } = useUserStore();
49
+
50
+ const {
51
+ language, adminConfig, modals, toastMsg,
52
+ setLanguage, setAdminConfig, toggleModal, showToast, closeAllModals
53
+ } = useUIStore();
54
+
55
+ const {
56
+ uploadedImages, selectedStyle, customStyleImage,
57
+ filter, blurAmount, compositionMode, resolution, subjectType, customPrompt,
58
+ results, status, progress, errorMsg,
59
+ isScanning, scanStep, recommendedStyleIds, analysisResult,
60
+ setUploadedImages, setSelectedStyle, setCustomStyleImage,
61
+ setGenerationConfig, setStatus, setProgress, setErrorMsg, addResult, setResults, resetGeneration,
62
+ startScanning, setScanStep, setAnalysisData, stopScanning
63
+ } = useGenerationStore();
64
+
65
+ const t = TRANSLATIONS[language];
66
+
67
+ // Lead Capture Local State (Ephemeral)
68
+ const [formState, setFormState] = useState<'idle'|'submitting'|'success'>('idle');
69
+ const [formData, setFormData] = useState({ name: '', phone: '', wechat: '', date: '', budget: '', service: '' });
70
+
71
+ // PDD Viral Local State
72
+ const [slashingStyle, setSlashingStyle] = useState<WeddingStyle | null>(null);
73
+
74
+ // Ticker State
75
+ const [tickerVisible, setTickerVisible] = useState(false);
76
+ const [tickerData, setTickerData] = useState({ name: '', style: '', time: '' });
77
+
78
+ // Countdown State
79
+ const [countdown, setCountdown] = useState("02:14:59");
80
+ const [rotatingTip, setRotatingTip] = useState(t.genTips[0]);
81
+
82
+ // Refs
83
+ const abortControllerRef = useRef<AbortController | null>(null);
84
+ const resultRef = useRef<HTMLDivElement>(null);
85
+ const adminTriggerCount = useRef(0);
86
+
87
+ // --- EFFECTS ---
88
+
89
+ // IP Tracking
90
+ useEffect(() => {
91
+ ipService.trackVisit();
92
+ }, []);
93
+
94
+ // Failsafe: Remove loading spinner when App mounts
95
+ useEffect(() => {
96
+ const spinner = document.getElementById('loading-spinner');
97
+ if (spinner) {
98
+ spinner.style.opacity = '0';
99
+ setTimeout(() => spinner.remove(), 500);
100
+ }
101
+ }, []);
102
+
103
+ useEffect(() => {
104
+ if (navigator.hardwareConcurrency && navigator.hardwareConcurrency < 4) {
105
+ document.body.classList.add('low-power');
106
+ }
107
+ if (selectedStyle) {
108
+ document.title = `${t.title} - ${selectedStyle.name}`;
109
+ } else {
110
+ document.title = t.title + " - AI Fitting Room";
111
+ }
112
+ }, [selectedStyle, t.title]);
113
+
114
+ // Initial Red Packet check
115
+ useEffect(() => {
116
+ const hasSeenRP = localStorage.getItem('seen_rp_' + new Date().toDateString());
117
+ if (!hasSeenRP) {
118
+ setTimeout(() => {
119
+ if (!currentUser && !modals.auth) toggleModal('redPacket', true);
120
+ localStorage.setItem('seen_rp_' + new Date().toDateString(), 'true');
121
+ }, 2000);
122
+ }
123
+ }, []);
124
+
125
+ // Rotating Tips
126
+ useEffect(() => {
127
+ if (status === 'generating') {
128
+ let i = 0;
129
+ const interval = setInterval(() => {
130
+ i = (i + 1) % t.genTips.length;
131
+ setRotatingTip(t.genTips[i]);
132
+ }, 4000);
133
+ return () => clearInterval(interval);
134
+ }
135
+ }, [status, language]);
136
+
137
+ // Countdown
138
+ useEffect(() => {
139
+ const timer = setInterval(() => {
140
+ const now = new Date();
141
+ const targetTime = new Date(adminConfig.promoEnds).getTime() > now.getTime()
142
+ ? new Date(adminConfig.promoEnds)
143
+ : new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59);
144
+ const diff = targetTime.getTime() - now.getTime();
145
+ const hours = Math.floor((diff / (1000 * 60 * 60)));
146
+ const minutes = Math.floor((diff / (1000 * 60)) % 60);
147
+ const seconds = Math.floor((diff / 1000) % 60);
148
+ setCountdown(`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`);
149
+ }, 1000);
150
+ return () => clearInterval(timer);
151
+ }, [adminConfig.promoEnds]);
152
+
153
+ // Ticker
154
+ useEffect(() => {
155
+ const names = ['李小姐', '王女士', 'Ms. Chen', 'Sarah', '张小姐', 'Jessica', '刘女士', 'Emily'];
156
+ const showTicker = () => {
157
+ const randomName = names[Math.floor(Math.random() * names.length)];
158
+ const randomStyle = STYLES[Math.floor(Math.random() * STYLES.length)];
159
+ const styleName = (TRANSLATIONS[language].styles as any)[randomStyle.id] || randomStyle.name;
160
+ const time = Math.floor(Math.random() * 5) + 1;
161
+ const timeStr = time === 1 ? TRANSLATIONS[language].tickerJustNow : `${time} ${TRANSLATIONS[language].tickerMinsAgo}`;
162
+ setTickerData({ name: randomName, style: styleName, time: timeStr });
163
+ setTickerVisible(true);
164
+ setTimeout(() => setTickerVisible(false), 5000);
165
+ };
166
+ const interval = setInterval(showTicker, 15000);
167
+ return () => clearInterval(interval);
168
+ }, [language]);
169
+
170
+ // --- HANDLERS ---
171
+
172
+ const handleLogin = (phone: string, password?: string) => {
173
+ const result = login(phone, password);
174
+ if (result.success) {
175
+ showToast(result.msg);
176
+ toggleModal('auth', false);
177
+ const user = allUsers.find(u => u.phone === phone);
178
+ if (user && (user.role === 'admin' || user.role === 'staff')) {
179
+ setTimeout(() => toggleModal('admin', true), 500);
180
+ }
181
+ } else {
182
+ alert(result.msg);
183
+ }
184
+ };
185
+
186
+ const handleRegister = (phone: string, name: string, pass: string) => {
187
+ const result = register(phone, name, pass);
188
+ if (result.success) {
189
+ showToast(result.msg);
190
+ } else {
191
+ showToast(result.msg);
192
+ }
193
+ };
194
+
195
+ const handlePasswordReset = async (phone: string, newPass: string) => {
196
+ return resetPassword(phone, newPass);
197
+ };
198
+
199
+ const handleAdminLoginSuccess = () => {
200
+ const adminUser: UserAccount = {
201
+ id: 'ADMIN_SUPER',
202
+ name: 'Administrator',
203
+ phone: '000-0000',
204
+ points: 999999,
205
+ isVip: true,
206
+ joinDate: Date.now(),
207
+ history: [],
208
+ role: 'admin'
209
+ };
210
+ // We hack the currentUser in store for session only
211
+ useUserStore.setState({ currentUser: adminUser });
212
+ showToast("Admin Mode Activated");
213
+ };
214
+
215
+ const trackStyleUsage = (styleId: string) => {
216
+ if (!currentUser) return;
217
+ const stats = currentUser.styleStats || {};
218
+ stats[styleId] = (stats[styleId] || 0) + 1;
219
+ updateUser(currentUser.id, { styleStats: stats });
220
+
221
+ // Log generation event
222
+ addLog({
223
+ styleId,
224
+ styleName: (TRANSLATIONS[language].styles as any)[styleId] || styleId,
225
+ userId: currentUser.id,
226
+ timestamp: Date.now(),
227
+ ip: ipService.currentIp,
228
+ location: ipService.currentLocation,
229
+ device: /Mobi|Android/i.test(navigator.userAgent) ? 'Mobile' : 'Desktop'
230
+ });
231
+ };
232
+
233
+ const handleBookClick = () => toggleModal('consult', true);
234
+
235
+ const handleAdminTrigger = () => {
236
+ adminTriggerCount.current += 1;
237
+ if (adminTriggerCount.current >= 5) {
238
+ toggleModal('admin', true);
239
+ adminTriggerCount.current = 0;
240
+ }
241
+ };
242
+
243
+ const handleSlashClick = (style: WeddingStyle) => {
244
+ setSlashingStyle(style);
245
+ toggleModal('slash', true);
246
+ };
247
+
248
+ const handleSlashUnlock = () => {
249
+ if (currentUser) {
250
+ updateUser(currentUser.id, { isVip: true });
251
+ showToast(t.pddSlashSuccess);
252
+ } else {
253
+ showToast("Please login first!");
254
+ toggleModal('auth', true);
255
+ }
256
+ };
257
+
258
+ const handleFeedbackSubmit = async (data: Omit<FeedbackItem, 'id' | 'timestamp'>) => {
259
+ const newItem: FeedbackItem = {
260
+ id: Date.now().toString(),
261
+ timestamp: Date.now(),
262
+ ...data
263
+ };
264
+
265
+ addFeedback(newItem);
266
+
267
+ await new Promise(resolve => setTimeout(resolve, 800));
268
+ showToast(t.toastFeedback);
269
+ toggleModal('feedback', false);
270
+ };
271
+
272
+ const handleFormSubmit = async (e: React.FormEvent) => {
273
+ e.preventDefault();
274
+ setFormState('submitting');
275
+
276
+ let preferredStyleName = 'Unknown';
277
+ let genCount = 0;
278
+ if (currentUser && currentUser.styleStats) {
279
+ const entries = Object.entries(currentUser.styleStats);
280
+ if (entries.length > 0) {
281
+ genCount = entries.reduce((sum, [_, count]) => sum + (count as number), 0);
282
+ const [topId, _] = entries.reduce((max, curr) => (curr[1] as number) > (max[1] as number) ? curr : max);
283
+ const topStyleObj = STYLES.find(s => s.id === topId);
284
+ if (topStyleObj) preferredStyleName = (TRANSLATIONS[language].styles as any)[topId] || topStyleObj.name;
285
+ }
286
+ }
287
+
288
+ const newLead: LeadData = {
289
+ id: Date.now().toString(),
290
+ userId: currentUser?.id,
291
+ name: formData.name,
292
+ phone: formData.phone,
293
+ wechat: formData.wechat,
294
+ service: formData.service,
295
+ budget: formData.budget,
296
+ date: formData.date,
297
+ timestamp: Date.now(),
298
+ status: 'new',
299
+ preferredStyle: preferredStyleName,
300
+ generationCount: genCount,
301
+ syncStatus: 'pending'
302
+ };
303
+
304
+ // CRM Sync Logic
305
+ try {
306
+ const syncResult = await crmService.syncLead(newLead, adminConfig);
307
+ if (syncResult.success) {
308
+ newLead.syncStatus = 'synced';
309
+ newLead.crmId = syncResult.crmId;
310
+ } else {
311
+ newLead.syncStatus = 'failed';
312
+ }
313
+ } catch (e) {
314
+ console.error("CRM Sync failed", e);
315
+ newLead.syncStatus = 'failed';
316
+ }
317
+
318
+ addLead(newLead);
319
+
320
+ setTimeout(() => {
321
+ setFormState('success');
322
+ if (currentUser && !currentUser.isVip) {
323
+ addPoints(adminConfig.pointsBook || 100, 'Booking Inquiry');
324
+ updateUser(currentUser.id, { isVip: true });
325
+ }
326
+ confetti({ particleCount: 100, spread: 70, origin: { y: 0.6 } });
327
+ setTimeout(() => {
328
+ toggleModal('consult', false);
329
+ setFormState('idle');
330
+ setFormData({ name: '', phone: '', wechat: '', date: '', budget: '', service: '' });
331
+ }, 3000);
332
+ }, 1500);
333
+ };
334
+
335
+ const handleImagesSelect = (base64Images: string[]) => {
336
+ setUploadedImages(base64Images);
337
+ setResults({});
338
+ setStatus('idle');
339
+ setErrorMsg(null);
340
+ setAnalysisData([], { faceShape: '', skinTone: '', bestVibe: '' });
341
+
342
+ if (base64Images.length > 0) {
343
+ startScanning();
344
+
345
+ setTimeout(() => setScanStep(1), 1000);
346
+ setTimeout(() => {
347
+ if (language === 'zh') {
348
+ setAnalysisData([], {
349
+ faceShape: "标准鹅蛋脸",
350
+ skinTone: "暖白皮 / 象牙色",
351
+ bestVibe: "法式浪漫 & 韩式唯美"
352
+ });
353
+ } else {
354
+ setAnalysisData([], {
355
+ faceShape: "Oval (Goose Egg)",
356
+ skinTone: "Warm Porcelain",
357
+ bestVibe: "Romantic & Elegant"
358
+ });
359
+ }
360
+ setScanStep(2);
361
+ }, 2500);
362
+
363
+ setTimeout(() => {
364
+ stopScanning();
365
+ const hotStyles = STYLES.filter(s => s.tags?.includes('hot'));
366
+ const otherStyles = STYLES.filter(s => !s.tags?.includes('hot'));
367
+ const randomHot = hotStyles.sort(() => 0.5 - Math.random()).slice(0, 1);
368
+ const randomOthers = otherStyles.sort(() => 0.5 - Math.random()).slice(0, 2);
369
+ const finalRecs = [...randomHot, ...randomOthers].map(s => s.id);
370
+ setAnalysisData(finalRecs, {
371
+ faceShape: language === 'zh' ? "标准鹅蛋脸" : "Oval",
372
+ skinTone: language === 'zh' ? "暖白皮" : "Warm",
373
+ bestVibe: language === 'zh' ? "法式浪漫" : "Romantic"
374
+ });
375
+
376
+ toggleModal('analysis', true);
377
+ }, 3500);
378
+ }
379
+ };
380
+
381
+ const handleStyleSelect = (style: WeddingStyle) => {
382
+ if (currentUser?.role === 'admin') {
383
+ setSelectedStyle(style);
384
+ setCustomStyleImage(null);
385
+ return;
386
+ }
387
+ if (style.isLocked && (!currentUser || !currentUser.isVip)) {
388
+ if (!currentUser) toggleModal('auth', true);
389
+ else toggleModal('consult', true);
390
+ return;
391
+ }
392
+ setSelectedStyle(style);
393
+ setCustomStyleImage(null);
394
+ };
395
+
396
+ const handleCustomStyleSelect = (base64Style: string) => {
397
+ setCustomStyleImage(base64Style);
398
+ const customStyle: WeddingStyle = {
399
+ id: 'custom',
400
+ name: t.customStyle,
401
+ prompt: 'Custom style from reference image',
402
+ description: 'Using your uploaded photo as style guide',
403
+ coverColor: 'bg-rose-100',
404
+ icon: <ImageIcon className="w-5 h-5 text-rose-500" />,
405
+ isCustom: true
406
+ };
407
+ setSelectedStyle(customStyle);
408
+ };
409
+
410
+ const handleLuckySelect = () => {
411
+ const isVip = currentUser?.isVip || currentUser?.role === 'admin' || false;
412
+ // Filter available styles: if not VIP, exclude locked styles
413
+ const availableStyles = STYLES.filter(s => !s.isLocked || isVip);
414
+
415
+ if (availableStyles.length > 0) {
416
+ const randomStyle = availableStyles[Math.floor(Math.random() * availableStyles.length)];
417
+ handleStyleSelect(randomStyle);
418
+
419
+ // Show Toast
420
+ const styleName = (TRANSLATIONS[language].styles as any)[randomStyle.id] || randomStyle.name;
421
+ showToast(`✨ Lucky Pick: ${styleName}!`);
422
+
423
+ // Trigger confetti for fun
424
+ confetti({
425
+ particleCount: 50,
426
+ spread: 60,
427
+ origin: { y: 0.7 },
428
+ colors: ['#fbbf24', '#f43f5e']
429
+ });
430
+ }
431
+ };
432
+
433
+ const handleReset = () => {
434
+ resetGeneration();
435
+ window.scrollTo({ top: 0, behavior: 'smooth' });
436
+ };
437
+
438
+ const generateSingleStyle = async (images: string[], style: WeddingStyle, customPromptText: string) => {
439
+ try {
440
+ const referenceImage = style.id === 'custom' ? customStyleImage : null;
441
+ const generated = await generateStyledWeddingImage(images, style.prompt, customPromptText, referenceImage, compositionMode, resolution, subjectType);
442
+
443
+ if (style.id !== 'custom') trackStyleUsage(style.id);
444
+
445
+ const dateKey = new Date().toDateString();
446
+ const hasGenToday = localStorage.getItem('gen_today_' + dateKey);
447
+ if (!hasGenToday) {
448
+ addPoints(50, 'Daily First Look');
449
+ localStorage.setItem('gen_today_' + dateKey, 'true');
450
+ }
451
+
452
+ return {
453
+ styleId: style.id,
454
+ imageUrl: generated,
455
+ timestamp: Date.now(),
456
+ config: {
457
+ customInstruction: customPromptText,
458
+ filter: filter,
459
+ blurAmount: blurAmount,
460
+ compositionMode: compositionMode,
461
+ resolution: resolution,
462
+ subjectType: subjectType
463
+ }
464
+ };
465
+ } catch (e) {
466
+ console.error(`Failed to generate style ${style.name}`, e);
467
+ throw e;
468
+ }
469
+ };
470
+
471
+ const handleGenerateSingle = async () => {
472
+ if (uploadedImages.length === 0 || !selectedStyle) return;
473
+ setStatus('generating');
474
+ setErrorMsg(null);
475
+ setProgress({ current: 0, total: 1, statusMsg: "Initializing AI Designer..." });
476
+ setTimeout(() => { resultRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, 100);
477
+
478
+ try {
479
+ setTimeout(() => setProgress({ current: 0, total: 1, statusMsg: "Analyzing facial features..." }), 500);
480
+ setTimeout(() => setProgress({ current: 0, total: 1, statusMsg: "Applying wedding style..." }), 1500);
481
+
482
+ const result = await generateSingleStyle(uploadedImages, selectedStyle, customPrompt);
483
+ if (result) {
484
+ addResult(result);
485
+ setStatus('success');
486
+ setProgress(null);
487
+ } else {
488
+ throw new Error("Generation failed");
489
+ }
490
+ } catch (error) {
491
+ setStatus('error');
492
+ setErrorMsg("Failed to generate image. Please try again.");
493
+ setProgress(null);
494
+ }
495
+ };
496
+
497
+ const stopGeneration = () => {
498
+ if (abortControllerRef.current) {
499
+ abortControllerRef.current.abort();
500
+ abortControllerRef.current = null;
501
+ }
502
+ setStatus('idle');
503
+ setProgress(null);
504
+ showToast("Generation stopped.");
505
+ };
506
+
507
+ const handleGenerateAll = () => {
508
+ if (uploadedImages.length === 0) return;
509
+ setStatus('generating');
510
+ setErrorMsg(null);
511
+ showToast("Starting Batch Generation...");
512
+ setProgress({ current: 0, total: STYLES.length, statusMsg: "Initializing Batch Engine..." });
513
+
514
+ setTimeout(async () => {
515
+ try {
516
+ resultRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
517
+
518
+ const isSuperAdmin = currentUser?.role === 'admin';
519
+ const isVip = currentUser?.isVip || isSuperAdmin;
520
+
521
+ if (!isSuperAdmin && !isVip) {
522
+ const hasLocked = STYLES.some(s => s.isLocked);
523
+ if(hasLocked) {
524
+ setStatus('idle');
525
+ setProgress(null);
526
+ if(!currentUser) toggleModal('auth', true);
527
+ else toggleModal('consult', true);
528
+ return;
529
+ }
530
+ }
531
+
532
+ abortControllerRef.current = new AbortController();
533
+ const presetStyles = STYLES;
534
+ const total = presetStyles.length;
535
+ let successCount = 0;
536
+
537
+ const baseDelay = resolution === 'ultra' ? 6000 : resolution === 'high' ? 4000 : 2500;
538
+
539
+ for (let i = 0; i < presetStyles.length; i++) {
540
+ if (abortControllerRef.current?.signal.aborted) break;
541
+
542
+ const style = presetStyles[i];
543
+ const styleName = (TRANSLATIONS[language].styles as any)[style.id] || style.name;
544
+
545
+ if (results[style.id]) {
546
+ successCount++;
547
+ setProgress({ current: i + 1, total, statusMsg: `Skipping ${styleName} (Done)` });
548
+ continue;
549
+ }
550
+
551
+ let attempts = 0;
552
+ let success = false;
553
+ while (attempts < 3 && !success) {
554
+ if (abortControllerRef.current?.signal.aborted) break;
555
+ try {
556
+ setProgress({ current: i + 1, total, statusMsg: `Designing: ${styleName}...` });
557
+ const result = await generateSingleStyle(uploadedImages, style, customPrompt);
558
+ if (result) {
559
+ addResult(result);
560
+ successCount++;
561
+ success = true;
562
+ }
563
+ } catch (e: any) {
564
+ attempts++;
565
+ console.warn(`Failed ${styleName} attempt ${attempts}`, e);
566
+ const isRateLimit = e?.message?.includes('429');
567
+ const waitTime = isRateLimit ? 5000 * attempts : 1500;
568
+ setProgress({ current: i + 1, total, statusMsg: `Cooling down (Retrying)...` });
569
+ await new Promise(r => setTimeout(r, waitTime));
570
+ }
571
+ }
572
+
573
+ if (i < presetStyles.length - 1) {
574
+ setProgress({ current: i + 1, total, statusMsg: `Success! Preparing next style...` });
575
+ await new Promise(r => setTimeout(r, baseDelay));
576
+ }
577
+ }
578
+
579
+ setStatus(successCount > 0 ? 'success' : 'error');
580
+ if (successCount === 0) setErrorMsg("Batch generation failed. Please check network.");
581
+
582
+ } catch (err) {
583
+ console.error(err);
584
+ setStatus('error');
585
+ setErrorMsg("System error during batch.");
586
+ } finally {
587
+ abortControllerRef.current = null;
588
+ setProgress(null);
589
+ }
590
+ }, 100);
591
+ };
592
+
593
+ const hasResults = Object.keys(results).length > 0;
594
+ const activeFilterCss = FILTERS.find(f => f.id === filter)?.css || 'none';
595
+ const hasUploaded = uploadedImages.length > 0;
596
+
597
+ return (
598
+ <div className="min-h-screen bg-white font-sans text-gray-900 selection:bg-rose-100 selection:text-rose-900 flex flex-col relative overflow-x-hidden">
599
+
600
+ {toastMsg && <Toast msg={toastMsg} onClose={() => {}} />}
601
+
602
+ <FloatingConcierge language={language} config={adminConfig} />
603
+ <ExitIntentModal language={language} onClose={() => {}} />
604
+
605
+ <Suspense fallback={null}>
606
+ <AdminDashboard
607
+ isVisible={modals.admin}
608
+ onClose={() => toggleModal('admin', false)}
609
+ language={language}
610
+ leads={leads}
611
+ feedback={feedback}
612
+ config={adminConfig}
613
+ allUsers={allUsers}
614
+ currentUser={currentUser}
615
+ onUpdateConfig={setAdminConfig}
616
+ onUpdateLeadStatus={updateLeadStatus}
617
+ onDeleteLead={deleteLead}
618
+ onUpdateUser={updateUser}
619
+ showToast={showToast}
620
+ onAdminLoginSuccess={handleAdminLoginSuccess}
621
+ logs={logs}
622
+ />
623
+ </Suspense>
624
+
625
+ {currentUser && (
626
+ <UserCenter
627
+ isVisible={modals.userCenter}
628
+ onClose={() => toggleModal('userCenter', false)}
629
+ language={language}
630
+ user={currentUser}
631
+ leads={leads}
632
+ config={adminConfig}
633
+ onRedeem={() => {
634
+ const cost = adminConfig.pointsVipCost || 100;
635
+ if (currentUser.points >= cost) {
636
+ updateUser(currentUser.id, { points: currentUser.points - cost, isVip: true });
637
+ showToast("VIP Unlocked!");
638
+ }
639
+ }}
640
+ showToast={showToast}
641
+ onAddPoints={addPoints}
642
+ />
643
+ )}
644
+
645
+ <AuthModal
646
+ isVisible={modals.auth}
647
+ onClose={() => toggleModal('auth', false)}
648
+ language={language}
649
+ onLogin={handleLogin}
650
+ onRegister={handleRegister}
651
+ onResetPassword={handlePasswordReset}
652
+ />
653
+
654
+ <AboutModal
655
+ isVisible={modals.about}
656
+ onClose={() => toggleModal('about', false)}
657
+ language={language}
658
+ config={adminConfig}
659
+ />
660
+
661
+ <AnalysisModal
662
+ isVisible={modals.analysis}
663
+ onClose={() => toggleModal('analysis', false)}
664
+ language={language}
665
+ result={analysisResult}
666
+ onUnlock={() => toggleModal('analysis', false)}
667
+ />
668
+
669
+ <RedPacketModal
670
+ isVisible={modals.redPacket}
671
+ onClose={() => toggleModal('redPacket', false)}
672
+ language={language}
673
+ adminConfig={adminConfig}
674
+ user={currentUser}
675
+ onUpdateUser={(bal) => currentUser && updateUser(currentUser.id, { redPacketBalance: bal })}
676
+ onOpenShare={() => toggleModal('share', true)}
677
+ />
678
+
679
+ <SlashModal
680
+ isVisible={modals.slash}
681
+ onClose={() => toggleModal('slash', false)}
682
+ style={slashingStyle}
683
+ onUnlock={handleSlashUnlock}
684
+ language={language}
685
+ adminConfig={adminConfig}
686
+ user={currentUser}
687
+ onUpdateUser={(prog) => currentUser && updateUser(currentUser.id, { slashProgress: prog })}
688
+ onOpenShare={() => toggleModal('share', true)}
689
+ />
690
+
691
+ <ShareModal
692
+ isVisible={modals.share}
693
+ onClose={() => toggleModal('share', false)}
694
+ language={language}
695
+ config={adminConfig.shareConfig}
696
+ showToast={showToast}
697
+ onShareSuccess={() => addPoints(adminConfig.pointsShare || 10, 'Viral Share')}
698
+ />
699
+
700
+ <FeedbackModal
701
+ isVisible={modals.feedback}
702
+ onClose={() => toggleModal('feedback', false)}
703
+ language={language}
704
+ user={currentUser}
705
+ onSubmit={handleFeedbackSubmit}
706
+ />
707
+
708
+ {isScanning && (
709
+ <div className="fixed inset-0 z-[70] bg-black/95 flex flex-col items-center justify-center text-white p-4">
710
+ <div className="relative w-64 h-64 sm:w-80 sm:h-80 border-2 border-rose-500/50 rounded-full flex items-center justify-center overflow-hidden mb-8 shadow-[0_0_50px_rgba(244,63,94,0.4)]">
711
+ <div className="absolute inset-0 bg-gradient-to-b from-transparent via-rose-500/20 to-transparent w-full h-full animate-scan" style={{animationDuration: '1.5s'}}></div>
712
+ <div className="absolute inset-0 border-4 border-rose-500 rounded-full animate-ping opacity-20"></div>
713
+ {uploadedImages[0] && <img src={uploadedImages[0]} className="w-full h-full object-cover opacity-50 grayscale" />}
714
+ <ScanFace className="absolute w-16 h-16 text-rose-500 animate-pulse" />
715
+ <div className="absolute top-10 left-10 w-2 h-2 bg-rose-400 rounded-full animate-ping"></div>
716
+ <div className="absolute bottom-10 right-10 w-2 h-2 bg-rose-400 rounded-full animate-ping" style={{animationDelay: '0.3s'}}></div>
717
+ </div>
718
+ <div className="text-center space-y-2">
719
+ <h2 className="text-2xl font-bold font-serif text-rose-300 tracking-wider">
720
+ {scanStep === 0 ? t.aiScanning : scanStep === 1 ? t.aiAnalyzing : t.aiMatching}
721
+ </h2>
722
+ <p className="text-gray-400 text-sm font-mono tracking-widest">SYSTEM ANALYSIS v2.4.0</p>
723
+ </div>
724
+ <div className="mt-8 w-64 h-1 bg-gray-800 rounded-full overflow-hidden">
725
+ <div className="h-full bg-rose-500 transition-all duration-[3500ms] ease-linear w-full shadow-[0_0_10px_#f43f5e]"></div>
726
+ </div>
727
+ </div>
728
+ )}
729
+
730
+ <div className={`fixed bottom-24 left-4 z-40 bg-white/90 backdrop-blur border border-rose-100 shadow-xl rounded-full px-4 py-3 flex items-center gap-3 transition-all duration-500 transform ${tickerVisible ? 'translate-y-0 opacity-100' : 'translate-y-8 opacity-0 pointer-events-none'}`}>
731
+ <div className="w-8 h-8 rounded-full bg-rose-100 flex items-center justify-center">
732
+ <Bell className="w-4 h-4 text-rose-500 animate-swing" />
733
+ </div>
734
+ <div className="text-xs">
735
+ <p className="font-bold text-gray-800">
736
+ <span className="text-rose-600">{tickerData.name}</span> {t.tickerBooked}
737
+ </p>
738
+ <p className="text-gray-500 flex items-center gap-1">
739
+ {tickerData.style} • {tickerData.time}
740
+ </p>
741
+ </div>
742
+ </div>
743
+
744
+ {adminConfig.showBanner && (
745
+ <div className="bg-gradient-to-r from-rose-600 to-rose-500 text-white text-xs sm:text-sm py-2 px-4 flex items-center justify-center gap-2 sm:gap-4 font-medium relative animate-fade-in flex-wrap shadow-md z-[60]">
746
+ <span className="text-center drop-shadow-sm">{adminConfig.promoText}</span>
747
+ <div className="flex items-center gap-1.5 bg-rose-800/40 px-2 py-0.5 rounded-lg border border-rose-400/30">
748
+ <Timer className="w-3.5 h-3.5 text-rose-200" />
749
+ <span className="font-mono font-bold text-rose-100 tracking-wide">{t.promoEnds} {countdown}</span>
750
+ </div>
751
+ <button onClick={() => setAdminConfig({...adminConfig, showBanner: false})} className="absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-rose-700/50 rounded-full transition-colors">
752
+ <X className="w-3 h-3" />
753
+ </button>
754
+ </div>
755
+ )}
756
+
757
+ <Header
758
+ language={language}
759
+ onLanguageChange={setLanguage}
760
+ onBookClick={handleBookClick}
761
+ user={currentUser}
762
+ config={adminConfig}
763
+ onOpenUserCenter={() => toggleModal('userCenter', true)}
764
+ onLoginClick={() => toggleModal('auth', true)}
765
+ onOpenAdmin={() => toggleModal('admin', true)}
766
+ onOpenAbout={() => toggleModal('about', true)}
767
+ onOpenFeedback={() => toggleModal('feedback', true)}
768
+ />
769
+
770
+ <button
771
+ onClick={handleBookClick}
772
+ className="fixed bottom-6 right-6 z-40 bg-gradient-to-r from-rose-500 to-rose-600 text-white rounded-full p-4 shadow-xl shadow-rose-300 transition-transform hover:scale-110 active:scale-95 animate-bounce-slow group"
773
+ >
774
+ <MessageCircle className="w-6 h-6" />
775
+ <span className="absolute right-full mr-3 top-1/2 -translate-y-1/2 bg-gray-900 text-white text-xs font-bold px-3 py-1.5 rounded-lg whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity shadow-lg">
776
+ {t.floatConsult}
777
+ </span>
778
+ <span className="absolute -top-1 -right-1 flex h-3 w-3">
779
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
780
+ <span className="relative inline-flex rounded-full h-3 w-3 bg-red-500"></span>
781
+ </span>
782
+ </button>
783
+
784
+ {modals.consult && (
785
+ <div className="fixed inset-0 z-[60] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in">
786
+ <div className="bg-white rounded-2xl shadow-2xl max-w-md w-full overflow-hidden relative">
787
+ <button onClick={() => toggleModal('consult', false)} className="absolute top-4 right-4 text-gray-400 hover:text-gray-600 p-1">
788
+ <X className="w-5 h-5" />
789
+ </button>
790
+
791
+ <div className="p-6">
792
+ {formState === 'success' ? (
793
+ <div className="flex flex-col items-center justify-center py-8 text-center space-y-4">
794
+ <div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center text-green-600 mb-2">
795
+ <CheckCircle className="w-10 h-10" />
796
+ </div>
797
+ <h3 className="text-xl font-bold text-gray-800">{t.formSuccess}</h3>
798
+ <p className="text-gray-500 text-sm">We will contact you shortly.</p>
799
+ </div>
800
+ ) : (
801
+ <>
802
+ <div className="text-center mb-6">
803
+ {(!currentUser?.isVip) ? (
804
+ <div className="w-12 h-12 bg-amber-100 rounded-full flex items-center justify-center text-amber-600 mx-auto mb-3 animate-pulse">
805
+ <Lock className="w-6 h-6" />
806
+ </div>
807
+ ) : (
808
+ <div className="w-12 h-12 bg-rose-100 rounded-full flex items-center justify-center text-rose-600 mx-auto mb-3">
809
+ <Calendar className="w-6 h-6" />
810
+ </div>
811
+ )}
812
+ <h3 className="text-xl font-bold text-gray-900">{(!currentUser?.isVip) ? t.vipUnlockTitle : t.consultTitle}</h3>
813
+ <p className="text-sm text-gray-500 mt-1">{(!currentUser?.isVip) ? t.vipUnlockDesc : t.consultDesc}</p>
814
+ </div>
815
+
816
+ <form onSubmit={handleFormSubmit} className="space-y-4">
817
+ <div className="grid grid-cols-2 gap-4">
818
+ <div>
819
+ <label className="block text-xs font-bold text-gray-700 mb-1">{t.formName} *</label>
820
+ <input required type="text" value={formData.name} onChange={e => setFormData({...formData, name: e.target.value})} className="w-full px-3 py-2 text-sm rounded-lg border border-gray-200 focus:border-rose-500 outline-none" />
821
+ </div>
822
+ <div>
823
+ <label className="block text-xs font-bold text-gray-700 mb-1">{t.formPhone} *</label>
824
+ <input required type="tel" value={formData.phone} onChange={e => setFormData({...formData, phone: e.target.value})} className="w-full px-3 py-2 text-sm rounded-lg border border-gray-200 focus:border-rose-500 outline-none" />
825
+ </div>
826
+ </div>
827
+ <div>
828
+ <label className="block text-xs font-bold text-gray-700 mb-1">{t.formWeChat}</label>
829
+ <input type="text" value={formData.wechat} onChange={e => setFormData({...formData, wechat: e.target.value})} className="w-full px-3 py-2 text-sm rounded-lg border border-gray-200 focus:border-rose-500 outline-none" />
830
+ </div>
831
+ <div>
832
+ <label className="block text-xs font-bold text-gray-700 mb-1">{t.formService}</label>
833
+ <select value={formData.service} onChange={e => setFormData({...formData, service: e.target.value})} className="w-full px-3 py-2 text-sm rounded-lg border border-gray-200 bg-white focus:border-rose-500 outline-none">
834
+ <option value="">- Select -</option>
835
+ <option value="wedding">{t.service1}</option>
836
+ <option value="art">{t.service2}</option>
837
+ <option value="family">{t.service3}</option>
838
+ </select>
839
+ </div>
840
+ <div>
841
+ <label className="block text-xs font-bold text-gray-700 mb-1">{t.formBudget}</label>
842
+ <select value={formData.budget} onChange={e => setFormData({...formData, budget: e.target.value})} className="w-full px-3 py-2 text-sm rounded-lg border border-gray-200 bg-white focus:border-rose-500 outline-none">
843
+ <option value="">- Select -</option>
844
+ <option value="1">{t.budget1}</option>
845
+ <option value="2">{t.budget2}</option>
846
+ <option value="3">{t.budget3}</option>
847
+ <option value="4">{t.budget4}</option>
848
+ </select>
849
+ </div>
850
+ <div>
851
+ <label className="block text-xs font-bold text-gray-700 mb-1">{t.formDate}</label>
852
+ <input type="date" value={formData.date} onChange={e => setFormData({...formData, date: e.target.value})} className="w-full px-3 py-2 text-sm rounded-lg border border-gray-200 focus:border-rose-500 outline-none" />
853
+ </div>
854
+ <button type="submit" disabled={formState === 'submitting'} className="w-full py-3 rounded-xl bg-gray-900 text-white font-bold shadow-lg hover:bg-black transition-all flex items-center justify-center gap-2 mt-4">
855
+ {formState === 'submitting' && <Loader2 className="w-4 h-4 animate-spin" />}
856
+ {t.formSubmit}
857
+ </button>
858
+ </form>
859
+ </>
860
+ )}
861
+ </div>
862
+ </div>
863
+ </div>
864
+ )}
865
+
866
+ <main className="max-w-7xl mx-auto px-4 sm:px-6 py-8 sm:py-12 flex-grow w-full">
867
+ <div className="text-center mb-8 sm:mb-12 max-w-2xl mx-auto animate-fade-in">
868
+ <h2 className="font-serif text-3xl sm:text-5xl font-bold text-gray-900 mb-4 leading-tight">
869
+ {t.heroTitle}
870
+ </h2>
871
+ <p className="text-sm sm:text-lg text-gray-600">
872
+ {t.heroDesc}
873
+ </p>
874
+ </div>
875
+
876
+ <div className="grid lg:grid-cols-12 gap-8 lg:gap-12 items-start">
877
+ <div className="lg:col-span-5 space-y-8">
878
+ <section className="space-y-4">
879
+ <div className="flex items-center gap-3 mb-2">
880
+ <span className="flex items-center justify-center w-8 h-8 rounded-full bg-rose-100 text-rose-600 font-bold text-sm">1</span>
881
+ <h3 className="text-lg sm:text-xl font-bold">{t.step1}</h3>
882
+ </div>
883
+ <ImageUploader onImagesSelect={handleImagesSelect} currentImages={uploadedImages} disabled={status === 'generating'} language={language} />
884
+ </section>
885
+
886
+ <section className={`space-y-4 transition-opacity duration-500 ${!hasUploaded ? 'opacity-40 pointer-events-none' : 'opacity-100'}`}>
887
+ <div className="flex items-center gap-3 mb-2 justify-between">
888
+ <div className="flex items-center gap-3">
889
+ <span className="flex items-center justify-center w-8 h-8 rounded-full bg-rose-100 text-rose-600 font-bold text-sm">2</span>
890
+ <h3 className="text-lg sm:text-xl font-bold">{t.step2}</h3>
891
+ </div>
892
+ {selectedStyle && <button onClick={() => { setSelectedStyle(null); setCustomStyleImage(null); }} className="text-xs text-rose-500 hover:underline">{t.clearSelection}</button>}
893
+ </div>
894
+ <div className="max-h-[350px] overflow-y-auto pr-2 custom-scrollbar">
895
+ <StyleSelector
896
+ selectedStyle={selectedStyle}
897
+ onSelect={handleStyleSelect}
898
+ onCustomSelect={handleCustomStyleSelect}
899
+ onLuckySelect={handleLuckySelect}
900
+ onSlashClick={handleSlashClick}
901
+ disabled={status === 'generating'}
902
+ language={language}
903
+ recommendedStyleIds={recommendedStyleIds}
904
+ isVipUnlocked={currentUser?.isVip}
905
+ resolution={resolution}
906
+ onResolutionChange={(res) => setGenerationConfig({ resolution: res })}
907
+ />
908
+ </div>
909
+ {customStyleImage && (
910
+ <div className="flex items-center gap-3 p-3 bg-rose-50 rounded-xl border border-rose-100">
911
+ <div className="w-12 h-12 rounded-lg overflow-hidden border border-rose-200 shrink-0"><img src={customStyleImage} alt="Reference" className="w-full h-full object-cover" /></div>
912
+ <div className="flex-1"><p className="text-sm font-bold text-gray-800">{t.usingCustom}</p><p className="text-xs text-gray-500">{t.usingCustomDesc}</p></div>
913
+ <button onClick={() => { setCustomStyleImage(null); setSelectedStyle(null); }} className="p-2 hover:bg-rose-100 rounded-full text-rose-500"><Sparkles className="w-4 h-4" /></button>
914
+ </div>
915
+ )}
916
+ </section>
917
+
918
+ <section className={`space-y-4 transition-opacity duration-500 ${!hasUploaded ? 'opacity-40 pointer-events-none' : 'opacity-100'}`}>
919
+ <div className="flex items-center gap-3 mb-2">
920
+ <span className="flex items-center justify-center w-8 h-8 rounded-full bg-rose-100 text-rose-600 font-bold text-sm">3</span>
921
+ <h3 className="text-lg sm:text-xl font-bold">{t.step3}</h3>
922
+ </div>
923
+ <div className="bg-gray-50 rounded-2xl border border-gray-200 p-5 space-y-5">
924
+ <div>
925
+ <div className="flex items-center gap-2 mb-2"><Users className="w-4 h-4 text-gray-500" /><label className="text-sm font-bold text-gray-700">{t.subjType}</label></div>
926
+ <div className="grid grid-cols-3 gap-2">
927
+ <button onClick={() => setGenerationConfig({ subjectType: 'female' })} className={`px-2 py-2 text-xs rounded-lg border font-medium transition-all ${subjectType === 'female' ? 'bg-rose-50 border-rose-500 text-rose-700' : 'bg-white border-gray-200 text-gray-600 hover:bg-gray-50'}`}>{t.subjFemale}</button>
928
+ <button onClick={() => setGenerationConfig({ subjectType: 'male' })} className={`px-2 py-2 text-xs rounded-lg border font-medium transition-all ${subjectType === 'male' ? 'bg-rose-50 border-rose-500 text-rose-700' : 'bg-white border-gray-200 text-gray-600 hover:bg-gray-50'}`}>{t.subjMale}</button>
929
+ <button onClick={() => setGenerationConfig({ subjectType: 'couple' })} className={`px-2 py-2 text-xs rounded-lg border font-medium transition-all ${subjectType === 'couple' ? 'bg-rose-50 border-rose-500 text-rose-700' : 'bg-white border-gray-200 text-gray-600 hover:bg-gray-50'}`}>{t.subjCouple}</button>
930
+ <button onClick={() => setGenerationConfig({ subjectType: 'child' })} className={`px-2 py-2 text-xs rounded-lg border font-medium transition-all ${subjectType === 'child' ? 'bg-rose-50 border-rose-500 text-rose-700' : 'bg-white border-gray-200 text-gray-600 hover:bg-gray-50'}`}>{t.subjChild}</button>
931
+ <button onClick={() => setGenerationConfig({ subjectType: 'family' })} className={`px-2 py-2 text-xs rounded-lg border font-medium transition-all ${subjectType === 'family' ? 'bg-rose-50 border-rose-500 text-rose-700' : 'bg-white border-gray-200 text-gray-600 hover:bg-gray-50'}`}>{t.subjFamily}</button>
932
+ </div>
933
+ </div>
934
+
935
+ <div className="border-t border-gray-200 my-4"></div>
936
+ <div><FilterSelector selectedFilter={filter} onSelect={(f) => setGenerationConfig({ filter: f })} blurAmount={blurAmount} onBlurChange={(b) => setGenerationConfig({ blurAmount: b })} disabled={status === 'generating'} language={language} /></div>
937
+
938
+ <div className="border-t border-gray-200 my-4"></div>
939
+
940
+ {(uploadedImages.length > 1 || subjectType === 'family' || subjectType === 'couple') && (
941
+ <div className="animate-fade-in">
942
+ <div className="flex items-center gap-2 mb-3"><Users className="w-4 h-4 text-gray-500" /><label className="text-sm font-bold text-gray-700">{t.groupComp}</label></div>
943
+ <div className="grid grid-cols-3 gap-2">
944
+ <button onClick={() => setGenerationConfig({ compositionMode: 'classic' })} className={`px-2 py-2 text-xs rounded-lg border font-medium transition-all ${compositionMode === 'classic' ? 'bg-rose-50 border-rose-500 text-rose-700' : 'bg-white border-gray-200 text-gray-600 hover:bg-gray-50'}`}>{t.compClassic}</button>
945
+ <button onClick={() => setGenerationConfig({ compositionMode: 'dynamic' })} className={`px-2 py-2 text-xs rounded-lg border font-medium transition-all ${compositionMode === 'dynamic' ? 'bg-rose-50 border-rose-500 text-rose-700' : 'bg-white border-gray-200 text-gray-600 hover:bg-gray-50'}`}>{t.compDynamic}</button>
946
+ <button onClick={() => setGenerationConfig({ compositionMode: 'cinematic' })} className={`px-2 py-2 text-xs rounded-lg border font-medium transition-all ${compositionMode === 'cinematic' ? 'bg-rose-50 border-rose-500 text-rose-700' : 'bg-white border-gray-200 text-gray-600 hover:bg-gray-50'}`}>{t.compCinematic}</button>
947
+ </div>
948
+ <div className="border-t border-gray-200 my-4"></div>
949
+ </div>
950
+ )}
951
+
952
+ <div>
953
+ <div className="flex items-center gap-2 mb-2"><PenTool className="w-4 h-4 text-gray-500" /><label className="text-sm font-bold text-gray-700">{t.customInstruct}</label></div>
954
+ <textarea value={customPrompt} onChange={(e) => setGenerationConfig({ customPrompt: e.target.value })} placeholder={uploadedImages.length > 1 ? "Describe how you want the group to be arranged..." : t.customInstructPlaceholder} className="w-full px-4 py-3 text-sm rounded-xl border border-gray-200 focus:border-rose-500 focus:ring-2 focus:ring-rose-200 outline-none transition-all resize-none h-24 bg-white shadow-sm placeholder:text-gray-400" disabled={status === 'generating'} />
955
+ <p className="text-[10px] text-gray-500 mt-2 flex items-center gap-1.5"><Sparkles className="w-3 h-3 text-rose-400" />{t.customInstructHint}</p>
956
+ </div>
957
+ </div>
958
+ </section>
959
+
960
+ <div className={`pt-4 flex flex-col gap-3 transition-opacity duration-500 ${!hasUploaded ? 'opacity-50' : 'opacity-100'}`}>
961
+ {errorMsg && <div className="p-3 bg-red-50 text-red-600 rounded-lg flex items-center gap-2 text-sm"><AlertCircle className="w-4 h-4 shrink-0" />{errorMsg}</div>}
962
+ {status === 'generating' && progress && (
963
+ <div className="space-y-3 bg-white p-4 rounded-xl shadow-lg border border-rose-100 relative overflow-hidden">
964
+ <div className="absolute top-0 right-0 w-32 h-32 bg-rose-500/5 rounded-full blur-2xl animate-pulse"></div>
965
+
966
+ <div className="flex items-center gap-3">
967
+ <div className="w-10 h-10 rounded-full bg-rose-50 flex items-center justify-center shrink-0">
968
+ <Loader2 className="w-5 h-5 text-rose-500 animate-spin" />
969
+ </div>
970
+ <div className="flex-1">
971
+ <p className="text-sm font-bold text-gray-800 animate-fade-in">{progress.statusMsg}</p>
972
+ <p className="text-xs text-rose-400 italic mt-0.5">{rotatingTip}</p>
973
+ </div>
974
+ <div className="text-right">
975
+ <span className="text-xl font-bold text-gray-900">{Math.round((progress.current / progress.total) * 100)}%</span>
976
+ </div>
977
+ </div>
978
+
979
+ <div className="h-2 w-full bg-gray-100 rounded-full overflow-hidden mt-2">
980
+ <div className="h-full bg-gradient-to-r from-rose-400 to-rose-600 transition-all duration-300 relative" style={{width: `${(progress.current / progress.total) * 100}%`}}>
981
+ <div className="absolute top-0 right-0 h-full w-2 bg-white/50 animate-pulse"></div>
982
+ </div>
983
+ </div>
984
+
985
+ <button onClick={stopGeneration} className="w-full mt-2 text-[10px] text-gray-400 hover:text-red-500 flex items-center justify-center gap-1">
986
+ <StopCircle className="w-3 h-3" /> Stop Generation
987
+ </button>
988
+ </div>
989
+ )}
990
+
991
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
992
+ <button onClick={handleGenerateSingle} disabled={!hasUploaded || !selectedStyle || status === 'generating'} className={`flex items-center justify-center gap-2 px-6 py-3 rounded-xl font-bold text-white shadow-lg transition-all duration-200 ${!hasUploaded || !selectedStyle || status === 'generating' ? 'bg-gray-300 text-gray-500 cursor-not-allowed shadow-none' : 'bg-gray-900 hover:bg-black hover:scale-[1.02]'}`}>
993
+ <Wand2 className="w-4 h-4" />
994
+ <span>{selectedStyle?.id === 'custom' ? t.genCustom : t.genSelected}</span>
995
+ </button>
996
+ <button onClick={handleGenerateAll} disabled={!hasUploaded || status === 'generating'} className={`relative overflow-hidden group flex items-center justify-center gap-2 px-6 py-3 rounded-xl font-bold text-white shadow-lg shadow-rose-200 transition-all duration-200 ${!hasUploaded || status === 'generating' ? 'bg-rose-200 text-white cursor-not-allowed shadow-none opacity-70' : 'bg-gradient-to-r from-rose-500 to-rose-600 hover:scale-[1.02] hover:shadow-rose-300'}`}>
997
+ <Layers className="w-4 h-4" />
998
+ <span>{t.genAll}</span>
999
+ </button>
1000
+ </div>
1001
+ </div>
1002
+ </div>
1003
+
1004
+ <div className="lg:col-span-7" ref={resultRef}>
1005
+ {hasResults ? (
1006
+ <ResultViewer
1007
+ originalImages={uploadedImages}
1008
+ results={results}
1009
+ initialSelectedId={selectedStyle?.id}
1010
+ onReset={handleReset}
1011
+ activeFilter={activeFilterCss}
1012
+ blurAmount={blurAmount}
1013
+ language={language}
1014
+ onBookClick={handleBookClick}
1015
+ onShareClick={() => toggleModal('share', true)}
1016
+ />
1017
+ ) : (
1018
+ <div className="h-full min-h-[400px] rounded-3xl border-2 border-dashed border-gray-100 flex flex-col items-center justify-center text-center p-8 bg-gray-50/50">
1019
+ <div className="w-16 h-16 bg-white rounded-2xl flex items-center justify-center shadow-sm mb-4"><Sparkles className="w-8 h-8 text-gray-300" /></div>
1020
+ <h3 className="text-xl font-serif font-bold text-gray-300">{t.emptyGallery}</h3>
1021
+ <p className="text-gray-400 mt-2 max-w-xs text-sm">{t.emptyGalleryDesc}</p>
1022
+ </div>
1023
+ )}
1024
+ </div>
1025
+ </div>
1026
+ </main>
1027
+
1028
+ <Features language={language} />
1029
+ <Testimonials language={language} />
1030
+
1031
+ <footer className="bg-gray-900 text-white py-8 border-t border-gray-800">
1032
+ <div className="max-w-7xl mx-auto px-4 flex flex-col md:flex-row items-center justify-between gap-6">
1033
+ <div className="text-center md:text-left">
1034
+ <h3 className="font-serif font-bold text-xl">{t.posterBrand}</h3>
1035
+ <div className="flex gap-4 justify-center md:justify-start mt-2">
1036
+ <button onClick={() => toggleModal('about', true)} className="text-xs text-gray-400 hover:text-rose-400 transition-colors">
1037
+ {t.aboutUsBtn}
1038
+ </button>
1039
+ <p className="text-xs text-gray-500 cursor-default select-none active:text-gray-400" onClick={handleAdminTrigger}>
1040
+ {t.footerRights}
1041
+ </p>
1042
+ </div>
1043
+ </div>
1044
+
1045
+ <div className="flex flex-col md:flex-row items-center gap-6 md:gap-12">
1046
+ <div className="flex flex-col gap-2 text-sm text-gray-300 md:text-right">
1047
+ <p className="flex items-center gap-2 justify-center md:justify-end"><MapPin className="w-4 h-4 text-rose-500" /> {adminConfig.footerAddress}</p>
1048
+ <p className="flex items-center gap-2 justify-center md:justify-end"><Phone className="w-4 h-4 text-rose-500" /> {adminConfig.contactPhone}</p>
1049
+ </div>
1050
+
1051
+ <div className="flex flex-col items-center gap-2 group cursor-pointer" onClick={() => window.open(adminConfig.qrCodeUrl || 'https://romantic-life.com/book', '_blank')}>
1052
+ <div className="bg-white p-2 rounded-xl shadow-lg border-2 border-rose-500/30 group-hover:border-rose-500 group-hover:scale-105 transition-all duration-300">
1053
+ <img src={adminConfig.qrCodeUrl || "https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=https://romantic-life.com/book"} alt="Book QR" className="w-20 h-20 sm:w-24 sm:h-24 object-cover" />
1054
+ </div>
1055
+ <span className="text-[10px] text-rose-200 uppercase font-bold tracking-wider group-hover:text-rose-400 transition-colors">{t.posterTitle}</span>
1056
+ </div>
1057
+ </div>
1058
+ </div>
1059
+ </footer>
1060
+ </div>
1061
+ );
1062
+ };
1063
+
1064
+ export default App;
README.md CHANGED
@@ -1,11 +1,54 @@
1
  ---
2
- title: AIWeddingFittingRoom
3
- emoji: 💻
4
- colorFrom: gray
5
- colorTo: green
6
  sdk: docker
7
  pinned: false
8
- short_description: AI Wedding Fitting Room
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Romantic Life - AI Wedding Fitting Room
3
+ emoji: 👰‍♀️
4
+ colorFrom: pink
5
+ colorTo: red
6
  sdk: docker
7
  pinned: false
8
+ app_port: 7860
9
  ---
10
 
11
+ # 厦门浪漫一生 - AI 婚纱试衣间 (Romantic Life AI)
12
+
13
+ 本项目是一个基于 React + Vite + Google Gemini API 的 AI 婚纱摄影试衣间应用。
14
+
15
+ ## 🌐 域名访问故障排查 (Troubleshooting)
16
+
17
+ 如果您上传后无法通过 `xmlove520.dpdns.org` 访问,请按以下步骤检查:
18
+
19
+ 1. **检查 HTTPS**:
20
+ * 如果您没有为该域名购买/配置 SSL 证书,请**务必使用 `http://`** 而不是 `https://` 访问。
21
+ * 尝试访问: `http://xmlove520.dpdns.org`
22
+
23
+ 2. **检查端口**:
24
+ * 家庭宽带通常屏蔽 80/443 端口。您可能需要访问带端口的地址,例如 `http://xmlove520.dpdns.org:8080` (具体端口取决于您的映射设置)。
25
+
26
+ 3. **检查构建**:
27
+ * 确保您上传的是 `npm run build` 生成的 `dist` 文件夹中的内容,而不是源码。
28
+ * 确保 `dist` 文件夹中包含 `index.html`。
29
+
30
+ ## 🚀 部署指南
31
+
32
+ ### 选项 A: Hugging Face Spaces (Docker)
33
+
34
+ 1. 创建 Space (SDK: Docker, Template: Blank)。
35
+ 2. 上传所有文件(包括 Dockerfile, nginx.conf, package.json 等)。
36
+ 3. 系统会自动构建并运行在 `https://huggingface.co/spaces/您的用户名/您的Space名`。
37
+
38
+ ### 选项 B: Windows IIS / 空间 (dpdns.org)
39
+
40
+ 1. 运行 `npm run build`。
41
+ 2. 将 `dist` 文件夹内的所有文件上传到您的服务器根目录。
42
+ 3. **注意**:`web.config` 文件已包含在构建中,这对于 IIS 服务器至关重要,它能防止刷新页面 404。
43
+
44
+ ## ✨ 功能特性
45
+
46
+ * **60+ 婚纱风格**: 涵盖韩式、法式、中式、古风、二次元等。
47
+ * **AI 智能换装**: 基于 Google Gemini Vision 模型。
48
+ * **后台管理**: 完整的客资管理(CRM)与系统设置面板。
49
+
50
+ ## 🛠️ 技术栈
51
+
52
+ * **Frontend**: React 18, TypeScript, Vite
53
+ * **State**: Zustand
54
+ * **AI**: Google Gemini Pro Vision
components/._AboutModal.tsx ADDED
Binary file (4.1 kB). View file
 
components/._AdminDashboard.tsx ADDED
Binary file (4.1 kB). View file
 
components/._AnalysisModal.tsx ADDED
Binary file (4.1 kB). View file
 
components/._AuthModal.tsx ADDED
Binary file (4.1 kB). View file
 
components/._ErrorBoundary.tsx ADDED
Binary file (4.1 kB). View file
 
components/._ExitIntentModal.tsx ADDED
Binary file (4.1 kB). View file
 
components/._Features.tsx ADDED
Binary file (4.1 kB). View file
 
components/._FeedbackModal.tsx ADDED
Binary file (4.1 kB). View file
 
components/._FilterSelector.tsx ADDED
Binary file (4.1 kB). View file
 
components/._FloatingConcierge.tsx ADDED
Binary file (4.1 kB). View file
 
components/._Header.tsx ADDED
Binary file (4.1 kB). View file
 
components/._ImageUploader.tsx ADDED
Binary file (4.1 kB). View file
 
components/._RedPacketModal.tsx ADDED
Binary file (4.1 kB). View file
 
components/._ResultViewer.tsx ADDED
Binary file (4.1 kB). View file
 
components/._ShareModal.tsx ADDED
Binary file (4.1 kB). View file
 
components/._SlashModal.tsx ADDED
Binary file (4.1 kB). View file
 
components/._StyleSelector.tsx ADDED
Binary file (4.1 kB). View file
 
components/._Testimonials.tsx ADDED
Binary file (4.1 kB). View file
 
components/._UserCenter.tsx ADDED
Binary file (4.1 kB). View file
 
components/AboutModal.tsx ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React from 'react';
3
+ import { X, Camera, Heart, MapPin, Award } from 'lucide-react';
4
+ import { Language, AdminConfig } from '../types';
5
+ import { TRANSLATIONS } from '../constants/translations';
6
+
7
+ interface AboutModalProps {
8
+ isVisible: boolean;
9
+ onClose: () => void;
10
+ language: Language;
11
+ config?: AdminConfig; // Optional to handle undefined initially
12
+ }
13
+
14
+ export const AboutModal: React.FC<AboutModalProps> = ({ isVisible, onClose, language, config }) => {
15
+ const t = TRANSLATIONS[language];
16
+
17
+ if (!isVisible) return null;
18
+
19
+ // Prefer dynamic config content, fallback to translation file
20
+ const story = config?.aboutStory || t.aboutStoryContent;
21
+ const philosophy = config?.aboutPhilosophy || t.aboutPhilosophyContent;
22
+ const location = config?.aboutLocation || t.aboutLocationContent;
23
+ const address = config?.footerAddress || t.footerAddress;
24
+
25
+ return (
26
+ <div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in">
27
+ <div className="bg-white rounded-2xl shadow-2xl w-full max-w-2xl overflow-hidden relative flex flex-col max-h-[90vh]">
28
+ {/* Header with Image Background */}
29
+ <div className="relative h-48 bg-gray-900 flex items-center justify-center overflow-hidden shrink-0">
30
+ <div className="absolute inset-0 bg-gradient-to-r from-rose-900 to-gray-900 opacity-90"></div>
31
+ {/* Decorative patterns */}
32
+ <div className="absolute top-0 left-0 w-full h-full opacity-20" style={{backgroundImage: 'radial-gradient(circle at 20% 50%, white 1px, transparent 1px)', backgroundSize: '20px 20px'}}></div>
33
+
34
+ <button
35
+ onClick={onClose}
36
+ className="absolute top-4 right-4 p-2 bg-black/30 text-white rounded-full hover:bg-black/50 transition-colors z-20"
37
+ >
38
+ <X className="w-5 h-5" />
39
+ </button>
40
+
41
+ <div className="relative z-10 text-center px-6">
42
+ <div className="w-16 h-16 bg-white rounded-full flex items-center justify-center text-rose-600 mx-auto mb-3 shadow-lg">
43
+ <Camera className="w-8 h-8" />
44
+ </div>
45
+ <h2 className="text-3xl font-serif font-bold text-white tracking-wide">{t.aboutTitle}</h2>
46
+ <p className="text-rose-200 text-sm mt-1 uppercase tracking-widest">{t.subtitle}</p>
47
+ </div>
48
+ </div>
49
+
50
+ {/* Content */}
51
+ <div className="p-6 sm:p-8 overflow-y-auto space-y-8 bg-gray-50/50">
52
+ {/* Brand Story */}
53
+ <div className="flex gap-4">
54
+ <div className="w-10 h-10 rounded-full bg-rose-100 flex items-center justify-center text-rose-600 shrink-0 mt-1">
55
+ <Award className="w-5 h-5" />
56
+ </div>
57
+ <div>
58
+ <h3 className="text-lg font-bold text-gray-900 mb-2">{t.aboutStory}</h3>
59
+ <p className="text-gray-600 leading-relaxed text-sm text-justify whitespace-pre-wrap">
60
+ {story}
61
+ </p>
62
+ </div>
63
+ </div>
64
+
65
+ {/* Philosophy */}
66
+ <div className="flex gap-4">
67
+ <div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 shrink-0 mt-1">
68
+ <Heart className="w-5 h-5" />
69
+ </div>
70
+ <div>
71
+ <h3 className="text-lg font-bold text-gray-900 mb-2">{t.aboutPhilosophy}</h3>
72
+ <p className="text-gray-600 leading-relaxed text-sm text-justify whitespace-pre-wrap">
73
+ {philosophy}
74
+ </p>
75
+ </div>
76
+ </div>
77
+
78
+ {/* Location */}
79
+ <div className="flex gap-4">
80
+ <div className="w-10 h-10 rounded-full bg-amber-100 flex items-center justify-center text-amber-600 shrink-0 mt-1">
81
+ <MapPin className="w-5 h-5" />
82
+ </div>
83
+ <div>
84
+ <h3 className="text-lg font-bold text-gray-900 mb-2">{t.aboutLocation}</h3>
85
+ <p className="text-gray-600 leading-relaxed text-sm text-justify whitespace-pre-wrap">
86
+ {location}
87
+ </p>
88
+ <div className="mt-3 p-3 bg-white rounded-lg border border-gray-200 text-xs text-gray-500 font-mono flex items-center gap-2">
89
+ <MapPin className="w-3 h-3 text-rose-500" />
90
+ {address}
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </div>
95
+
96
+ {/* Footer */}
97
+ <div className="p-4 bg-white border-t border-gray-100 text-center">
98
+ <p className="text-xs text-gray-400">{t.footerRights}</p>
99
+ </div>
100
+ </div>
101
+ </div>
102
+ );
103
+ };
components/AdminDashboard.tsx ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { X, Users, Settings, Download, Save, LogIn, Cloud, RefreshCw, ChevronDown, ChevronUp, Link as LinkIcon, Image as ImageIcon, MessageSquare } from 'lucide-react';
3
+ import { Language, LeadData, AdminConfig, UserAccount, FeedbackItem } from '../types';
4
+ import { TRANSLATIONS } from '../constants/translations';
5
+
6
+ interface AdminDashboardProps {
7
+ isVisible: boolean;
8
+ onClose: () => void;
9
+ language: Language;
10
+ leads: LeadData[];
11
+ feedback?: FeedbackItem[];
12
+ config: AdminConfig;
13
+ allUsers: UserAccount[];
14
+ currentUser?: UserAccount;
15
+ onUpdateConfig: (newConfig: AdminConfig) => void;
16
+ onUpdateLeadStatus: (id: string, status: LeadData['status']) => void;
17
+ onDeleteLead: (id: string) => void;
18
+ onUpdateUser: (id: string, updates: Partial<UserAccount>) => void;
19
+ showToast: (msg: string) => void;
20
+ onAdminLoginSuccess: () => void;
21
+ }
22
+
23
+ const CollapsibleSection = ({ title, children, defaultOpen = false }: { title: string, children?: React.ReactNode, defaultOpen?: boolean }) => {
24
+ const [isOpen, setIsOpen] = useState(defaultOpen);
25
+ return (
26
+ <div className="border border-gray-200 rounded-xl overflow-hidden mb-4 shadow-sm">
27
+ <button
28
+ onClick={() => setIsOpen(!isOpen)}
29
+ className="w-full flex justify-between items-center p-4 bg-gray-50 hover:bg-gray-100 transition-colors font-bold text-gray-800"
30
+ >
31
+ {title}
32
+ {isOpen ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
33
+ </button>
34
+ {isOpen && <div className="p-4 bg-white animate-fade-in">{children}</div>}
35
+ </div>
36
+ );
37
+ };
38
+
39
+ export const AdminDashboard: React.FC<AdminDashboardProps> = ({
40
+ isVisible, onClose, language, leads, feedback = [], config, onUpdateConfig, onUpdateLeadStatus, onDeleteLead, showToast, onAdminLoginSuccess
41
+ }) => {
42
+ const [activeTab, setActiveTab] = useState<'leads' | 'settings' | 'feedback'>('leads');
43
+ const [tempConfig, setTempConfig] = useState<AdminConfig>(config);
44
+ const [isLoggedIn, setIsLoggedIn] = useState(false);
45
+ const [password, setPassword] = useState('');
46
+ const t = TRANSLATIONS[language];
47
+
48
+ // Sync temp config when prop changes
49
+ useEffect(() => {
50
+ setTempConfig(config);
51
+ }, [config]);
52
+
53
+ if (!isVisible) return null;
54
+
55
+ const handleLogin = (e: React.FormEvent) => {
56
+ e.preventDefault();
57
+ if (password === 'admin888') {
58
+ setIsLoggedIn(true);
59
+ onAdminLoginSuccess();
60
+ } else { alert("密码错误"); }
61
+ };
62
+
63
+ return (
64
+ <div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-gray-900/80 backdrop-blur-sm">
65
+ <div className="bg-white rounded-2xl shadow-2xl w-full max-w-5xl h-[85vh] flex flex-col overflow-hidden relative">
66
+
67
+ {!isLoggedIn && (
68
+ <div className="absolute inset-0 z-50 bg-gray-900 flex flex-col items-center justify-center text-white">
69
+ <h2 className="text-2xl font-bold font-serif mb-6">{t.adminLoginTitle || '员工登录'}</h2>
70
+ <form onSubmit={handleLogin} className="flex flex-col gap-4 w-64">
71
+ <input type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder="请输入管理密码" className="p-3 rounded bg-gray-800 border border-gray-700 text-center outline-none focus:border-rose-500 placeholder:text-gray-500" />
72
+ <button type="submit" className="p-3 bg-rose-600 rounded font-bold hover:bg-rose-700 transition-colors">登录后台</button>
73
+ </form>
74
+ </div>
75
+ )}
76
+
77
+ <div className="bg-gray-900 text-white p-4 flex justify-between items-center shadow-md z-10">
78
+ <div className="flex items-center gap-2">
79
+ <Settings className="w-5 h-5 text-rose-500" />
80
+ <h2 className="text-lg font-bold">{t.adminTitle}</h2>
81
+ </div>
82
+ <button onClick={onClose} className="p-2 hover:bg-gray-800 rounded-full"><X className="w-5 h-5" /></button>
83
+ </div>
84
+
85
+ <div className="flex flex-1 overflow-hidden">
86
+ <div className="w-48 bg-gray-50 border-r border-gray-200 p-4 space-y-2 shrink-0">
87
+ <button onClick={() => setActiveTab('leads')} className={`w-full text-left p-3 rounded-lg font-medium transition-all ${activeTab === 'leads' ? 'bg-white shadow text-rose-600 border border-rose-100' : 'text-gray-600 hover:bg-gray-200'}`}>客资管理 (Leads)</button>
88
+ <button onClick={() => setActiveTab('feedback')} className={`w-full text-left p-3 rounded-lg font-medium transition-all ${activeTab === 'feedback' ? 'bg-white shadow text-rose-600 border border-rose-100' : 'text-gray-600 hover:bg-gray-200'}`}>用户反馈 (Feedback)</button>
89
+ <button onClick={() => setActiveTab('settings')} className={`w-full text-left p-3 rounded-lg font-medium transition-all ${activeTab === 'settings' ? 'bg-white shadow text-rose-600 border border-rose-100' : 'text-gray-600 hover:bg-gray-200'}`}>系统设置 (Settings)</button>
90
+ </div>
91
+
92
+ <div className="flex-1 p-6 overflow-y-auto custom-scrollbar bg-gray-50/50">
93
+ {activeTab === 'leads' && (
94
+ <div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
95
+ <div className="p-4 border-b border-gray-100 flex justify-between items-center bg-gray-50">
96
+ <h3 className="font-bold text-gray-800">客资列表 ({leads.length})</h3>
97
+ <button className="text-xs flex items-center gap-1 text-gray-500 hover:text-gray-800">
98
+ <Download className="w-3 h-3" /> 导出表格
99
+ </button>
100
+ </div>
101
+ <div className="overflow-x-auto">
102
+ <table className="w-full text-sm">
103
+ <thead>
104
+ <tr className="bg-gray-100 text-left text-gray-600">
105
+ <th className="p-3 font-semibold">姓名</th>
106
+ <th className="p-3 font-semibold">电话</th>
107
+ <th className="p-3 font-semibold">意向服务</th>
108
+ <th className="p-3 font-semibold">CRM同步</th>
109
+ <th className="p-3 font-semibold">状态</th>
110
+ <th className="p-3 font-semibold">操作</th>
111
+ </tr>
112
+ </thead>
113
+ <tbody className="divide-y divide-gray-100">
114
+ {leads.map(lead => (
115
+ <tr key={lead.id} className="hover:bg-gray-50 transition-colors">
116
+ <td className="p-3 font-medium">{lead.name}</td>
117
+ <td className="p-3 text-gray-600">{lead.phone}</td>
118
+ <td className="p-3 text-gray-500">{lead.service || '-'}</td>
119
+ <td className="p-3">
120
+ {lead.syncStatus === 'synced' ? (
121
+ <span className="flex items-center gap-1 text-green-600 text-xs font-bold"><Cloud className="w-3 h-3"/> 已同步</span>
122
+ ) : (
123
+ <span className="text-gray-400 text-xs">-</span>
124
+ )}
125
+ </td>
126
+ <td className="p-3">
127
+ <select
128
+ value={lead.status}
129
+ onChange={e => onUpdateLeadStatus(lead.id, e.target.value as any)}
130
+ className={`border rounded-lg p-1.5 text-xs font-bold outline-none cursor-pointer ${
131
+ lead.status === 'new' ? 'bg-blue-50 text-blue-700 border-blue-200' :
132
+ lead.status === 'contacted' ? 'bg-amber-50 text-amber-700 border-amber-200' :
133
+ 'bg-green-50 text-green-700 border-green-200'
134
+ }`}
135
+ >
136
+ <option value="new">新客</option>
137
+ <option value="contacted">已联系</option>
138
+ <option value="booked">已成交</option>
139
+ </select>
140
+ </td>
141
+ <td className="p-3">
142
+ <button onClick={() => onDeleteLead(lead.id)} className="text-red-400 hover:text-red-600 p-1" title="删除"><X className="w-4 h-4" /></button>
143
+ </td>
144
+ </tr>
145
+ ))}
146
+ {leads.length === 0 && (
147
+ <tr>
148
+ <td colSpan={6} className="p-8 text-center text-gray-400">暂无客资数据</td>
149
+ </tr>
150
+ )}
151
+ </tbody>
152
+ </table>
153
+ </div>
154
+ </div>
155
+ )}
156
+
157
+ {activeTab === 'feedback' && (
158
+ <div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
159
+ <div className="p-4 border-b border-gray-100 flex justify-between items-center bg-gray-50">
160
+ <h3 className="font-bold text-gray-800">用户反馈 ({feedback.length})</h3>
161
+ </div>
162
+ <div className="overflow-x-auto">
163
+ <table className="w-full text-sm">
164
+ <thead>
165
+ <tr className="bg-gray-100 text-left text-gray-600">
166
+ <th className="p-3 font-semibold">类型</th>
167
+ <th className="p-3 font-semibold">评分</th>
168
+ <th className="p-3 font-semibold">内容</th>
169
+ <th className="p-3 font-semibold">联系方式</th>
170
+ <th className="p-3 font-semibold">时间</th>
171
+ </tr>
172
+ </thead>
173
+ <tbody className="divide-y divide-gray-100">
174
+ {feedback.map(item => (
175
+ <tr key={item.id} className="hover:bg-gray-50 transition-colors">
176
+ <td className="p-3">
177
+ <span className={`px-2 py-0.5 rounded text-xs font-bold uppercase ${
178
+ item.type === 'bug' ? 'bg-red-100 text-red-600' :
179
+ item.type === 'suggestion' ? 'bg-amber-100 text-amber-600' : 'bg-blue-100 text-blue-600'
180
+ }`}>
181
+ {item.type}
182
+ </span>
183
+ </td>
184
+ <td className="p-3 font-bold text-yellow-500">{item.rating} ★</td>
185
+ <td className="p-3 text-gray-700 max-w-xs truncate" title={item.content}>{item.content}</td>
186
+ <td className="p-3 text-gray-500 text-xs">{item.contact || '-'}</td>
187
+ <td className="p-3 text-gray-400 text-xs">{new Date(item.timestamp).toLocaleString()}</td>
188
+ </tr>
189
+ ))}
190
+ {feedback.length === 0 && (
191
+ <tr>
192
+ <td colSpan={5} className="p-8 text-center text-gray-400">暂无反馈数据</td>
193
+ </tr>
194
+ )}
195
+ </tbody>
196
+ </table>
197
+ </div>
198
+ </div>
199
+ )}
200
+
201
+ {activeTab === 'settings' && (
202
+ <div className="max-w-3xl mx-auto space-y-2">
203
+ <div className="flex justify-between items-center mb-6">
204
+ <h3 className="text-xl font-bold text-gray-800">小程序/网页配置</h3>
205
+ <button
206
+ onClick={() => { onUpdateConfig(tempConfig); showToast("配置已保存!"); }}
207
+ className="px-6 py-2.5 bg-rose-600 hover:bg-rose-700 text-white rounded-xl font-bold shadow-lg shadow-rose-200 flex items-center gap-2 transition-all active:scale-95"
208
+ >
209
+ <Save className="w-4 h-4" /> 保存设置
210
+ </button>
211
+ </div>
212
+
213
+ <CollapsibleSection title="📍 基础信息 & 品牌设置" defaultOpen>
214
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
215
+ <div className="col-span-2">
216
+ <label className="block text-xs font-bold text-gray-500 mb-1">顶部活动通知文案</label>
217
+ <input type="text" value={tempConfig.promoText} onChange={e => setTempConfig({...tempConfig, promoText: e.target.value})} className="w-full p-2.5 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" />
218
+ </div>
219
+ <div>
220
+ <label className="block text-xs font-bold text-gray-500 mb-1">联系电话</label>
221
+ <input type="text" value={tempConfig.contactPhone} onChange={e => setTempConfig({...tempConfig, contactPhone: e.target.value})} className="w-full p-2.5 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" />
222
+ </div>
223
+ <div>
224
+ <label className="block text-xs font-bold text-gray-500 mb-1">门店地址</label>
225
+ <input type="text" value={tempConfig.footerAddress} onChange={e => setTempConfig({...tempConfig, footerAddress: e.target.value})} className="w-full p-2.5 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" />
226
+ </div>
227
+ <div className="col-span-2">
228
+ <label className="block text-xs font-bold text-gray-500 mb-1">Logo URL (品牌标志图片)</label>
229
+ <div className="flex gap-2 relative">
230
+ <ImageIcon className="absolute left-3 top-3 w-4 h-4 text-gray-400" />
231
+ <input type="text" value={tempConfig.logoUrl || ''} onChange={e => setTempConfig({...tempConfig, logoUrl: e.target.value})} placeholder="https://example.com/logo.png" className="w-full p-2.5 pl-10 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" />
232
+ {tempConfig.logoUrl && <img src={tempConfig.logoUrl} alt="Logo" className="w-10 h-10 object-contain rounded border bg-white" />}
233
+ </div>
234
+ </div>
235
+ <div className="col-span-2">
236
+ <label className="block text-xs font-bold text-gray-500 mb-1">预约/客服二维码图片链接 (QR Code URL)</label>
237
+ <div className="flex gap-2">
238
+ <input type="text" value={tempConfig.qrCodeUrl || ''} onChange={e => setTempConfig({...tempConfig, qrCodeUrl: e.target.value})} placeholder="https://example.com/my-qr.png" className="w-full p-2.5 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" />
239
+ {tempConfig.qrCodeUrl && <img src={tempConfig.qrCodeUrl} alt="Preview" className="w-10 h-10 object-cover rounded border" />}
240
+ </div>
241
+ <p className="text-[10px] text-gray-400 mt-1">
242
+ 提示: 如果您使用家庭宽带部署,请记得在 URL 中加上端口号 (例如 :8080)
243
+ </p>
244
+ </div>
245
+ </div>
246
+ </CollapsibleSection>
247
+
248
+ <CollapsibleSection title="📖 关于我们内容设置">
249
+ <div className="space-y-4">
250
+ <div>
251
+ <label className="block text-xs font-bold text-gray-500 mb-1">品牌故事 (Brand Story)</label>
252
+ <textarea rows={3} value={tempConfig.aboutStory || ''} onChange={e => setTempConfig({...tempConfig, aboutStory: e.target.value})} className="w-full p-2.5 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" placeholder="请输入品牌故事..." />
253
+ </div>
254
+ <div>
255
+ <label className="block text-xs font-bold text-gray-500 mb-1">服务理念 (Philosophy)</label>
256
+ <textarea rows={2} value={tempConfig.aboutPhilosophy || ''} onChange={e => setTempConfig({...tempConfig, aboutPhilosophy: e.target.value})} className="w-full p-2.5 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" placeholder="请输入服务理念..." />
257
+ </div>
258
+ <div>
259
+ <label className="block text-xs font-bold text-gray-500 mb-1">基地环境 (Location)</label>
260
+ <textarea rows={2} value={tempConfig.aboutLocation || ''} onChange={e => setTempConfig({...tempConfig, aboutLocation: e.target.value})} className="w-full p-2.5 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" placeholder="描述拍摄基地环境..." />
261
+ </div>
262
+ </div>
263
+ </CollapsibleSection>
264
+
265
+ <CollapsibleSection title="💎 会员与积分策略">
266
+ <div className="grid grid-cols-2 gap-4">
267
+ <div>
268
+ <label className="block text-xs font-bold text-gray-500 mb-1">奖励: 分享海报 (积分)</label>
269
+ <div className="relative">
270
+ <input type="number" value={tempConfig.pointsShare ?? 10} onChange={e => setTempConfig({...tempConfig, pointsShare: parseInt(e.target.value)})} className="w-full p-2.5 pl-8 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" />
271
+ <span className="absolute left-3 top-2.5 text-gray-400 font-bold">Pt</span>
272
+ </div>
273
+ </div>
274
+ <div>
275
+ <label className="block text-xs font-bold text-gray-500 mb-1">奖励: 邀请好友 (积分)</label>
276
+ <div className="relative">
277
+ <input type="number" value={tempConfig.pointsInvite ?? 50} onChange={e => setTempConfig({...tempConfig, pointsInvite: parseInt(e.target.value)})} className="w-full p-2.5 pl-8 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" />
278
+ <span className="absolute left-3 top-2.5 text-gray-400 font-bold">Pt</span>
279
+ </div>
280
+ </div>
281
+ <div>
282
+ <label className="block text-xs font-bold text-gray-500 mb-1">奖励: 预约留资 (积分)</label>
283
+ <div className="relative">
284
+ <input type="number" value={tempConfig.pointsBook ?? 100} onChange={e => setTempConfig({...tempConfig, pointsBook: parseInt(e.target.value)})} className="w-full p-2.5 pl-8 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" />
285
+ <span className="absolute left-3 top-2.5 text-gray-400 font-bold">Pt</span>
286
+ </div>
287
+ </div>
288
+ <div>
289
+ <label className="block text-xs font-bold text-rose-500 mb-1">消耗: 兑换VIP (积分)</label>
290
+ <div className="relative">
291
+ <input type="number" value={tempConfig.pointsVipCost ?? 100} onChange={e => setTempConfig({...tempConfig, pointsVipCost: parseInt(e.target.value)})} className="w-full p-2.5 pl-8 border border-rose-200 bg-rose-50 text-rose-700 font-bold rounded-lg text-sm focus:border-rose-500 outline-none" />
292
+ <span className="absolute left-3 top-2.5 text-rose-400 font-bold">Pt</span>
293
+ </div>
294
+ </div>
295
+ </div>
296
+ </CollapsibleSection>
297
+
298
+ <CollapsibleSection title="🚀 裂变营销与红包">
299
+ <div className="space-y-4">
300
+ <div>
301
+ <label className="block text-xs font-bold text-gray-500 mb-1">分享标题 (Share Title)</label>
302
+ <input type="text" value={tempConfig.shareTitle || ''} onChange={e => setTempConfig({...tempConfig, shareTitle: e.target.value})} className="w-full p-2.5 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" />
303
+ </div>
304
+ <div>
305
+ <label className="block text-xs font-bold text-gray-500 mb-1">分享描述 (Share Description)</label>
306
+ <input type="text" value={tempConfig.shareDesc || ''} onChange={e => setTempConfig({...tempConfig, shareDesc: e.target.value})} className="w-full p-2.5 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" />
307
+ </div>
308
+ <div className="flex gap-4">
309
+ <div className="flex-1">
310
+ <label className="block text-xs font-bold text-gray-500 mb-1">红包最大金额 (¥)</label>
311
+ <input type="number" value={tempConfig.redPacketMax || 2000} onChange={e => setTempConfig({...tempConfig, redPacketMax: parseInt(e.target.value)})} className="w-full p-2.5 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none" />
312
+ </div>
313
+ </div>
314
+ </div>
315
+ </CollapsibleSection>
316
+
317
+ <CollapsibleSection title="🔌 系统集成 (CRM/AI)">
318
+ <div className="space-y-4">
319
+ <div>
320
+ <label className="block text-xs font-bold text-gray-500 mb-1">CRM 接口地址 (API Endpoint)</label>
321
+ <div className="relative">
322
+ <Cloud className="absolute left-3 top-3 w-4 h-4 text-gray-400" />
323
+ <input type="text" value={tempConfig.crmApiUrl || ''} onChange={e => setTempConfig({...tempConfig, crmApiUrl: e.target.value})} className="w-full p-2.5 pl-10 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none font-mono text-xs" placeholder="https://api.crm.com/leads" />
324
+ </div>
325
+ </div>
326
+ <div>
327
+ <label className="block text-xs font-bold text-gray-500 mb-1">CRM API 密钥</label>
328
+ <input type="password" value={tempConfig.crmApiKey || ''} onChange={e => setTempConfig({...tempConfig, crmApiKey: e.target.value})} className="w-full p-2.5 border border-gray-200 rounded-lg text-sm focus:border-rose-500 outline-none font-mono" />
329
+ </div>
330
+ <div className="p-4 bg-blue-50 rounded-lg border border-blue-100">
331
+ <h4 className="font-bold text-blue-700 mb-2 text-xs uppercase tracking-wider flex items-center gap-2">
332
+ <Cloud className="w-4 h-4" /> Gemini AI Configuration
333
+ </h4>
334
+ <div className="space-y-3">
335
+ <div>
336
+ <label className="block text-xs font-bold text-gray-500 mb-1">API Key</label>
337
+ <input type="password" value={tempConfig.geminiApiKey || ''} onChange={e => setTempConfig({...tempConfig, geminiApiKey: e.target.value})} className="w-full p-2.5 border border-gray-200 rounded-lg text-sm focus:border-blue-500 outline-none font-mono" placeholder="AIzSy..." />
338
+ </div>
339
+ <div>
340
+ <label className="block text-xs font-bold text-gray-500 mb-1">API Base URL (Proxy/Mirror) - Optional</label>
341
+ <div className="relative">
342
+ <LinkIcon className="absolute left-3 top-3 w-4 h-4 text-gray-400" />
343
+ <input type="text" value={tempConfig.geminiApiUrl || ''} onChange={e => setTempConfig({...tempConfig, geminiApiUrl: e.target.value})} className="w-full p-2.5 pl-10 border border-gray-200 rounded-lg text-sm focus:border-blue-500 outline-none font-mono" placeholder="https://my-proxy.com (Leave empty for default)" />
344
+ </div>
345
+ <p className="text-[10px] text-gray-400 mt-1">
346
+ 如果您在中国大陆地区,请配置反向代理地址以解决网络问题。
347
+ </p>
348
+ </div>
349
+ </div>
350
+ </div>
351
+ </div>
352
+ </CollapsibleSection>
353
+ </div>
354
+ )}
355
+ </div>
356
+ </div>
357
+ </div>
358
+ </div>
359
+ );
360
+ };
components/AnalysisModal.tsx ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ import React from 'react';
4
+ import { X, ScanFace, CheckCircle, ArrowRight, Sparkles } from 'lucide-react';
5
+ import { Language } from '../types';
6
+ import { TRANSLATIONS } from '../constants/translations';
7
+
8
+ interface AnalysisModalProps {
9
+ isVisible: boolean;
10
+ onClose: () => void;
11
+ language: Language;
12
+ result: { faceShape: string; skinTone: string; bestVibe: string; };
13
+ onUnlock: () => void;
14
+ }
15
+
16
+ export const AnalysisModal: React.FC<AnalysisModalProps> = ({
17
+ isVisible, onClose, language, result, onUnlock
18
+ }) => {
19
+ const t = TRANSLATIONS[language];
20
+
21
+ if (!isVisible) return null;
22
+
23
+ return (
24
+ <div className="fixed inset-0 z-[80] flex items-center justify-center p-4 bg-gray-900/90 backdrop-blur-md animate-fade-in">
25
+ <div className="w-full max-w-md bg-black text-white rounded-3xl overflow-hidden border border-gray-800 shadow-2xl relative">
26
+ {/* Header */}
27
+ <div className="p-6 text-center border-b border-gray-800 relative">
28
+ <div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-rose-500 to-transparent"></div>
29
+ <div className="w-16 h-16 mx-auto bg-gray-900 rounded-full flex items-center justify-center border border-gray-700 mb-4 shadow-[0_0_30px_rgba(244,63,94,0.3)]">
30
+ <ScanFace className="w-8 h-8 text-rose-500" />
31
+ </div>
32
+ <h2 className="text-2xl font-serif font-bold tracking-wide text-rose-100">{t.analysisTitle}</h2>
33
+ <p className="text-xs text-gray-500 font-mono mt-1 tracking-widest uppercase">{t.analysisSubtitle}</p>
34
+ </div>
35
+
36
+ {/* Report Content */}
37
+ <div className="p-8 space-y-6">
38
+ <div className="flex items-center justify-between group">
39
+ <div>
40
+ <p className="text-xs text-gray-500 uppercase tracking-wider mb-1">{t.lblFaceShape}</p>
41
+ <p className="text-lg font-bold text-white group-hover:text-rose-400 transition-colors">{result.faceShape}</p>
42
+ </div>
43
+ <div className="w-10 h-10 rounded-full border border-gray-700 flex items-center justify-center">
44
+ <div className="w-6 h-8 border-2 border-white rounded-[40%] opacity-80"></div>
45
+ </div>
46
+ </div>
47
+
48
+ <div className="w-full h-px bg-gray-800"></div>
49
+
50
+ <div className="flex items-center justify-between group">
51
+ <div>
52
+ <p className="text-xs text-gray-500 uppercase tracking-wider mb-1">{t.lblSkinTone}</p>
53
+ <p className="text-lg font-bold text-white group-hover:text-rose-400 transition-colors">{result.skinTone}</p>
54
+ </div>
55
+ <div className="w-8 h-8 rounded-full bg-[#fde3e3] border-2 border-white/20"></div>
56
+ </div>
57
+
58
+ <div className="w-full h-px bg-gray-800"></div>
59
+
60
+ <div className="bg-gradient-to-r from-rose-900/50 to-gray-900 p-4 rounded-xl border border-rose-500/30 flex items-center gap-4">
61
+ <div className="w-10 h-10 bg-rose-500 rounded-full flex items-center justify-center text-white shrink-0 shadow-lg shadow-rose-900">
62
+ <Sparkles className="w-5 h-5" />
63
+ </div>
64
+ <div>
65
+ <p className="text-xs text-rose-300 font-bold uppercase mb-0.5">{t.lblRecVibe}</p>
66
+ <p className="text-xl font-serif font-bold text-white leading-none">{result.bestVibe}</p>
67
+ </div>
68
+ </div>
69
+ </div>
70
+
71
+ {/* Footer Action */}
72
+ <div className="p-6 bg-gray-900 border-t border-gray-800">
73
+ <button
74
+ onClick={onUnlock}
75
+ className="w-full py-4 bg-white text-black font-bold text-lg rounded-xl hover:bg-rose-50 transition-all flex items-center justify-center gap-2 group"
76
+ >
77
+ {t.pddSlashBtn} <ArrowRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
78
+ </button>
79
+ <p className="text-center text-[10px] text-gray-600 mt-3 font-mono">
80
+ {t.aiConfidence}: 98.4%
81
+ </p>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ );
86
+ };
components/AuthModal.tsx ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ import React, { useState } from 'react';
4
+ import { X, User, Lock, Phone, CheckCircle, ArrowRight, AlertCircle, RefreshCw, MessageSquare, Eye, EyeOff } from 'lucide-react';
5
+ import { Language } from '../types';
6
+ import { TRANSLATIONS } from '../constants/translations';
7
+
8
+ interface AuthModalProps {
9
+ isVisible: boolean;
10
+ onClose: () => void;
11
+ language: Language;
12
+ onLogin: (phone: string, password?: string) => void;
13
+ onRegister: (phone: string, name: string, pass: string) => void;
14
+ onResetPassword?: (phone: string, newPass: string) => Promise<boolean>;
15
+ }
16
+
17
+ export const AuthModal: React.FC<AuthModalProps> = ({ isVisible, onClose, language, onLogin, onRegister, onResetPassword }) => {
18
+ const [mode, setMode] = useState<'login' | 'register' | 'reset'>('login');
19
+ const [formData, setFormData] = useState({ phone: '', name: '', password: '', confirmPassword: '', code: '' });
20
+ const [error, setError] = useState<string | null>(null);
21
+ const [successMsg, setSuccessMsg] = useState<string | null>(null);
22
+ const [isCodeSent, setIsCodeSent] = useState(false);
23
+ const [showPassword, setShowPassword] = useState(false);
24
+ const t = TRANSLATIONS[language];
25
+
26
+ if (!isVisible) return null;
27
+
28
+ const handleSubmit = async (e: React.FormEvent) => {
29
+ e.preventDefault();
30
+ setError(null);
31
+ setSuccessMsg(null);
32
+
33
+ if (mode === 'login') {
34
+ onLogin(formData.phone, formData.password);
35
+ } else if (mode === 'register') {
36
+ // Registration Validation
37
+ if (!formData.name.trim()) {
38
+ setError("Name is required");
39
+ return;
40
+ }
41
+ if (formData.password.length < 6) {
42
+ setError("Password must be at least 6 characters");
43
+ return;
44
+ }
45
+ if (formData.password !== formData.confirmPassword) {
46
+ setError(t.authPassMismatch);
47
+ return;
48
+ }
49
+ onRegister(formData.phone, formData.name, formData.password);
50
+ setFormData({ phone: '', name: '', password: '', confirmPassword: '', code: '' });
51
+ onClose();
52
+ } else if (mode === 'reset') {
53
+ // Reset Password Flow
54
+ if (!isCodeSent) {
55
+ // Simulate Sending Code
56
+ if (formData.phone.length < 5) {
57
+ setError("Please enter a valid phone number");
58
+ return;
59
+ }
60
+ setIsCodeSent(true);
61
+ setSuccessMsg(`${t.authCodeSent} 8888`); // Simulation
62
+ return;
63
+ }
64
+
65
+ // Verify Step
66
+ if (formData.code !== '8888') {
67
+ setError("Invalid verification code (Demo: 8888)");
68
+ return;
69
+ }
70
+ if (formData.password.length < 6) {
71
+ setError("New password must be at least 6 characters");
72
+ return;
73
+ }
74
+ if (formData.password !== formData.confirmPassword) {
75
+ setError(t.authPassMismatch);
76
+ return;
77
+ }
78
+
79
+ if (onResetPassword) {
80
+ const success = await onResetPassword(formData.phone, formData.password);
81
+ if (success) {
82
+ setSuccessMsg(t.authResetSuccess);
83
+ setTimeout(() => {
84
+ setMode('login');
85
+ setIsCodeSent(false);
86
+ setSuccessMsg(null);
87
+ setFormData({ ...formData, password: '', confirmPassword: '', code: '' });
88
+ }, 2000);
89
+ } else {
90
+ setError(t.authPhoneNotFound);
91
+ }
92
+ }
93
+ }
94
+ };
95
+
96
+ const toggleMode = (newMode: 'login' | 'register' | 'reset') => {
97
+ setMode(newMode);
98
+ setFormData({ phone: '', name: '', password: '', confirmPassword: '', code: '' }); // Clear form
99
+ setError(null);
100
+ setSuccessMsg(null);
101
+ setIsCodeSent(false);
102
+ setShowPassword(false);
103
+ };
104
+
105
+ return (
106
+ <div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm animate-fade-in">
107
+ <div className="bg-white rounded-2xl shadow-2xl w-full max-w-sm overflow-hidden relative">
108
+ <button onClick={onClose} className="absolute top-4 right-4 p-1 rounded-full hover:bg-gray-100 transition-colors z-10">
109
+ <X className="w-5 h-5 text-gray-500" />
110
+ </button>
111
+
112
+ <div className="p-8">
113
+ <div className="text-center mb-6">
114
+ <div className={`w-12 h-12 rounded-full flex items-center justify-center mx-auto mb-3 transition-colors ${mode === 'login' ? 'bg-rose-100 text-rose-500' : mode === 'register' ? 'bg-green-100 text-green-500' : 'bg-blue-100 text-blue-500'}`}>
115
+ {mode === 'login' ? <User className="w-6 h-6" /> : mode === 'register' ? <CheckCircle className="w-6 h-6" /> : <RefreshCw className="w-6 h-6" />}
116
+ </div>
117
+ <h2 className="text-2xl font-serif font-bold text-gray-900">
118
+ {mode === 'login' ? t.authLogin : mode === 'register' ? t.authRegister : t.authReset}
119
+ </h2>
120
+ <p className="text-sm text-gray-500 mt-1">Romantic Life Studio</p>
121
+ </div>
122
+
123
+ {error && (
124
+ <div className="mb-4 p-3 bg-red-50 border border-red-100 rounded-lg flex items-center gap-2 text-xs text-red-600 font-medium animate-pulse">
125
+ <AlertCircle className="w-4 h-4 shrink-0" />
126
+ {error}
127
+ </div>
128
+ )}
129
+
130
+ {successMsg && (
131
+ <div className="mb-4 p-3 bg-green-50 border border-green-100 rounded-lg flex items-center gap-2 text-xs text-green-600 font-medium animate-fade-in">
132
+ <CheckCircle className="w-4 h-4 shrink-0" />
133
+ {successMsg}
134
+ </div>
135
+ )}
136
+
137
+ <form onSubmit={handleSubmit} className="space-y-4">
138
+ {mode === 'register' && (
139
+ <div className="relative group animate-fade-in-down">
140
+ <User className="absolute top-3 left-3 w-5 h-5 text-gray-400 group-focus-within:text-rose-500 transition-colors" />
141
+ <input
142
+ type="text"
143
+ placeholder={t.authNamePlace}
144
+ required
145
+ value={formData.name}
146
+ onChange={e => setFormData({...formData, name: e.target.value})}
147
+ className="w-full pl-10 pr-4 py-2.5 rounded-lg border border-gray-200 focus:border-rose-500 outline-none bg-gray-50 focus:bg-white transition-all"
148
+ />
149
+ </div>
150
+ )}
151
+
152
+ <div className="relative group">
153
+ <Phone className="absolute top-3 left-3 w-5 h-5 text-gray-400 group-focus-within:text-rose-500 transition-colors" />
154
+ <input
155
+ type="tel"
156
+ placeholder={t.authPhonePlace}
157
+ required
158
+ disabled={mode === 'reset' && isCodeSent}
159
+ value={formData.phone}
160
+ onChange={e => setFormData({...formData, phone: e.target.value})}
161
+ className="w-full pl-10 pr-4 py-2.5 rounded-lg border border-gray-200 focus:border-rose-500 outline-none bg-gray-50 focus:bg-white transition-all disabled:bg-gray-100 disabled:text-gray-500"
162
+ />
163
+ </div>
164
+
165
+ {mode === 'reset' && isCodeSent && (
166
+ <div className="relative group animate-fade-in-down">
167
+ <MessageSquare className="absolute top-3 left-3 w-5 h-5 text-gray-400 group-focus-within:text-rose-500 transition-colors" />
168
+ <input
169
+ type="text"
170
+ placeholder={t.authCodePlace}
171
+ required
172
+ value={formData.code}
173
+ onChange={e => setFormData({...formData, code: e.target.value})}
174
+ className="w-full pl-10 pr-4 py-2.5 rounded-lg border border-gray-200 focus:border-rose-500 outline-none bg-gray-50 focus:bg-white transition-all"
175
+ />
176
+ </div>
177
+ )}
178
+
179
+ {/* Password Fields - Hidden for Reset step 1 */}
180
+ {!(mode === 'reset' && !isCodeSent) && (
181
+ <div className="relative group">
182
+ <Lock className="absolute top-3 left-3 w-5 h-5 text-gray-400 group-focus-within:text-rose-500 transition-colors" />
183
+ <input
184
+ type={showPassword ? "text" : "password"}
185
+ placeholder={mode === 'reset' ? t.authNewPassPlace : t.authPassPlace}
186
+ required
187
+ value={formData.password}
188
+ onChange={e => setFormData({...formData, password: e.target.value})}
189
+ className="w-full pl-10 pr-10 py-2.5 rounded-lg border border-gray-200 focus:border-rose-500 outline-none bg-gray-50 focus:bg-white transition-all"
190
+ />
191
+ <button
192
+ type="button"
193
+ onClick={() => setShowPassword(!showPassword)}
194
+ className="absolute top-3 right-3 text-gray-400 hover:text-gray-600 focus:outline-none"
195
+ tabIndex={-1}
196
+ >
197
+ {showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
198
+ </button>
199
+ </div>
200
+ )}
201
+
202
+ {(mode === 'register' || (mode === 'reset' && isCodeSent)) && (
203
+ <div className="relative group animate-fade-in-down">
204
+ <CheckCircle className="absolute top-3 left-3 w-5 h-5 text-gray-400 group-focus-within:text-rose-500 transition-colors" />
205
+ <input
206
+ type="password"
207
+ placeholder={t.authConfirmPassPlace}
208
+ required
209
+ value={formData.confirmPassword}
210
+ onChange={e => setFormData({...formData, confirmPassword: e.target.value})}
211
+ className="w-full pl-10 pr-4 py-2.5 rounded-lg border border-gray-200 focus:border-rose-500 outline-none bg-gray-50 focus:bg-white transition-all"
212
+ />
213
+ </div>
214
+ )}
215
+
216
+ <button type="submit" className={`w-full py-3 text-white rounded-xl font-bold shadow-lg transition-all flex items-center justify-center gap-2 mt-2 ${mode === 'login' ? 'bg-rose-600 hover:bg-rose-700 shadow-rose-200' : mode === 'reset' ? 'bg-blue-600 hover:bg-blue-700 shadow-blue-200' : 'bg-green-600 hover:bg-green-700 shadow-green-200'}`}>
217
+ {mode === 'login' ? t.authLogin : mode === 'register' ? t.authRegister : isCodeSent ? t.authSubmit : t.authSendCode}
218
+ {!(mode === 'reset' && !isCodeSent) && <ArrowRight className="w-4 h-4" />}
219
+ </button>
220
+ </form>
221
+
222
+ {mode === 'login' && (
223
+ <div className="mt-3 text-right">
224
+ <button onClick={() => toggleMode('reset')} className="text-xs text-gray-500 hover:text-rose-500 transition-colors">
225
+ {t.authForgotPass}
226
+ </button>
227
+ </div>
228
+ )}
229
+
230
+ <div className="mt-6 text-center border-t border-gray-100 pt-4">
231
+ <button
232
+ onClick={() => toggleMode(mode === 'login' ? 'register' : 'login')}
233
+ className="text-sm text-rose-500 font-bold hover:text-rose-600 transition-colors hover:underline"
234
+ >
235
+ {mode === 'login' ? t.authNoAccount : t.authHasAccount}
236
+ </button>
237
+ </div>
238
+ </div>
239
+ </div>
240
+ </div>
241
+ );
242
+ };
components/ErrorBoundary.tsx ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { ErrorInfo, ReactNode } from 'react';
2
+ import { AlertTriangle, RefreshCw } from 'lucide-react';
3
+
4
+ interface Props {
5
+ children?: ReactNode;
6
+ }
7
+
8
+ interface State {
9
+ hasError: boolean;
10
+ error?: Error;
11
+ }
12
+
13
+ export class ErrorBoundary extends React.Component<Props, State> {
14
+ public state: State = {
15
+ hasError: false
16
+ };
17
+
18
+ public static getDerivedStateFromError(error: Error): State {
19
+ return { hasError: true, error };
20
+ }
21
+
22
+ public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
23
+ console.error('Uncaught error:', error, errorInfo);
24
+ }
25
+
26
+ public render() {
27
+ if (this.state.hasError) {
28
+ return (
29
+ <div className="min-h-screen flex flex-col items-center justify-center bg-gray-50 p-4 text-center">
30
+ <div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center text-red-500 mb-4">
31
+ <AlertTriangle className="w-8 h-8" />
32
+ </div>
33
+ <h1 className="text-2xl font-bold text-gray-800 mb-2">出错了</h1>
34
+ <p className="text-gray-600 mb-6 max-w-xs">
35
+ 抱歉,系统遇到了一些问题。<br/>
36
+ <span className="text-xs text-gray-400 mt-2 block font-mono bg-gray-100 p-2 rounded">
37
+ {this.state.error?.message || 'Unknown Error'}
38
+ </span>
39
+ </p>
40
+ <button
41
+ className="flex items-center gap-2 px-6 py-3 bg-rose-600 text-white rounded-xl font-bold hover:bg-rose-700 transition-all shadow-lg"
42
+ onClick={() => window.location.reload()}
43
+ >
44
+ <RefreshCw className="w-4 h-4" /> 刷新页面
45
+ </button>
46
+ </div>
47
+ );
48
+ }
49
+
50
+ return this.props.children;
51
+ }
52
+ }
components/ExitIntentModal.tsx ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { X, Gift } from 'lucide-react';
3
+ import { Language } from '../types';
4
+ import { TRANSLATIONS } from '../constants/translations';
5
+
6
+ interface ExitIntentModalProps {
7
+ language: Language;
8
+ onClose: () => void;
9
+ }
10
+
11
+ export const ExitIntentModal: React.FC<ExitIntentModalProps> = ({ language, onClose }) => {
12
+ const [isVisible, setIsVisible] = useState(false);
13
+ const t = TRANSLATIONS[language];
14
+
15
+ useEffect(() => {
16
+ // Only run on desktop where mouse leaves viewport
17
+ if (window.innerWidth < 768) return;
18
+
19
+ const handleMouseLeave = (e: MouseEvent) => {
20
+ if (e.clientY <= 0) {
21
+ // User moved mouse to top of browser (tabs/close button)
22
+ const hasShown = localStorage.getItem('exit_intent_shown');
23
+ if (!hasShown) {
24
+ setIsVisible(true);
25
+ localStorage.setItem('exit_intent_shown', 'true');
26
+ }
27
+ }
28
+ };
29
+
30
+ document.addEventListener('mouseleave', handleMouseLeave);
31
+ return () => document.removeEventListener('mouseleave', handleMouseLeave);
32
+ }, []);
33
+
34
+ if (!isVisible) return null;
35
+
36
+ const handleClose = () => {
37
+ setIsVisible(false);
38
+ onClose();
39
+ };
40
+
41
+ return (
42
+ <div className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm animate-fade-in">
43
+ <div className="bg-white rounded-2xl shadow-2xl max-w-sm w-full overflow-hidden relative text-center p-8">
44
+ <button
45
+ onClick={handleClose}
46
+ className="absolute top-2 right-2 p-1.5 hover:bg-gray-100 rounded-full text-gray-400"
47
+ >
48
+ <X className="w-5 h-5" />
49
+ </button>
50
+
51
+ <div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4 text-red-500 animate-bounce">
52
+ <Gift className="w-8 h-8" />
53
+ </div>
54
+
55
+ <h3 className="text-2xl font-bold text-gray-900 mb-2">等一下!</h3>
56
+ <p className="text-gray-600 mb-6 text-sm">
57
+ 现在离开就错过了 <strong>500元</strong> 现金抵用券!<br/>
58
+ 仅限今日,留下联系方式即可领取。
59
+ </p>
60
+
61
+ <div className="space-y-3">
62
+ <button
63
+ onClick={handleClose} // In real app, open consult modal
64
+ className="w-full py-3 bg-red-600 hover:bg-red-700 text-white font-bold rounded-xl shadow-lg shadow-red-200 transition-all"
65
+ >
66
+ 领取优惠
67
+ </button>
68
+ <button
69
+ onClick={handleClose}
70
+ className="w-full py-2 text-gray-400 text-xs hover:text-gray-600 font-medium"
71
+ >
72
+ 忍痛放弃
73
+ </button>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ );
78
+ };
components/Features.tsx ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { ShieldCheck, HeartHandshake, Crown, Camera, Sparkles } from 'lucide-react';
3
+ import { Language } from '../types';
4
+ import { TRANSLATIONS } from '../constants/translations';
5
+
6
+ interface FeaturesProps {
7
+ language: Language;
8
+ }
9
+
10
+ export const Features: React.FC<FeaturesProps> = ({ language }) => {
11
+ const t = TRANSLATIONS[language];
12
+
13
+ const features = [
14
+ {
15
+ icon: <ShieldCheck className="w-8 h-8 text-rose-500" />,
16
+ title: t.feat1Title,
17
+ desc: t.feat1Desc,
18
+ },
19
+ {
20
+ icon: <HeartHandshake className="w-8 h-8 text-rose-500" />,
21
+ title: t.feat2Title,
22
+ desc: t.feat2Desc,
23
+ },
24
+ {
25
+ icon: <Crown className="w-8 h-8 text-rose-500" />,
26
+ title: t.feat3Title,
27
+ desc: t.feat3Desc,
28
+ },
29
+ {
30
+ icon: <Camera className="w-8 h-8 text-rose-500" />,
31
+ title: t.feat4Title,
32
+ desc: t.feat4Desc,
33
+ },
34
+ ];
35
+
36
+ return (
37
+ <section className="py-12 bg-white">
38
+ <div className="max-w-7xl mx-auto px-4 sm:px-6">
39
+ <div className="text-center mb-10">
40
+ <h2 className="text-2xl sm:text-3xl font-serif font-bold text-gray-900 mb-2">
41
+ {t.whyUsTitle}
42
+ </h2>
43
+ <p className="text-gray-500">{t.whyUsDesc}</p>
44
+ </div>
45
+
46
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
47
+ {features.map((feat, idx) => (
48
+ <div key={idx} className="bg-rose-50/50 rounded-xl p-6 text-center hover:shadow-lg transition-shadow border border-rose-100">
49
+ <div className="w-16 h-16 bg-white rounded-full flex items-center justify-center shadow-sm mx-auto mb-4 text-rose-500">
50
+ {feat.icon}
51
+ </div>
52
+ <h3 className="font-bold text-gray-800 mb-2">{feat.title}</h3>
53
+ <p className="text-sm text-gray-600 leading-relaxed">{feat.desc}</p>
54
+ </div>
55
+ ))}
56
+ </div>
57
+ </div>
58
+ </section>
59
+ );
60
+ };
components/FeedbackModal.tsx ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { X, MessageSquare, AlertCircle, Lightbulb, HelpCircle, Star, Loader2 } from 'lucide-react';
3
+ import { Language, FeedbackItem, UserAccount } from '../types';
4
+ import { TRANSLATIONS } from '../constants/translations';
5
+
6
+ interface FeedbackModalProps {
7
+ isVisible: boolean;
8
+ onClose: () => void;
9
+ language: Language;
10
+ user?: UserAccount;
11
+ onSubmit: (data: Omit<FeedbackItem, 'id' | 'timestamp'>) => Promise<void>;
12
+ }
13
+
14
+ export const FeedbackModal: React.FC<FeedbackModalProps> = ({ isVisible, onClose, language, user, onSubmit }) => {
15
+ const t = TRANSLATIONS[language];
16
+ const [type, setType] = useState<FeedbackItem['type']>('suggestion');
17
+ const [content, setContent] = useState('');
18
+ const [rating, setRating] = useState(5);
19
+ const [isSubmitting, setIsSubmitting] = useState(false);
20
+
21
+ if (!isVisible) return null;
22
+
23
+ const handleSubmit = async (e: React.FormEvent) => {
24
+ e.preventDefault();
25
+ if (!content.trim()) return;
26
+
27
+ setIsSubmitting(true);
28
+ try {
29
+ await onSubmit({
30
+ userId: user?.id,
31
+ type,
32
+ rating,
33
+ content,
34
+ contact: user?.phone
35
+ });
36
+ // Reset form
37
+ setContent('');
38
+ setRating(5);
39
+ setType('suggestion');
40
+ } catch (error) {
41
+ console.error("Feedback error", error);
42
+ } finally {
43
+ setIsSubmitting(false);
44
+ }
45
+ };
46
+
47
+ const getIcon = () => {
48
+ switch (type) {
49
+ case 'bug': return <AlertCircle className="w-6 h-6 text-red-500" />;
50
+ case 'suggestion': return <Lightbulb className="w-6 h-6 text-amber-500" />;
51
+ default: return <HelpCircle className="w-6 h-6 text-blue-500" />;
52
+ }
53
+ };
54
+
55
+ return (
56
+ <div className="fixed inset-0 z-[110] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in">
57
+ <div className="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden relative">
58
+ <button onClick={onClose} className="absolute top-4 right-4 p-1 rounded-full hover:bg-gray-100 transition-colors z-10">
59
+ <X className="w-5 h-5 text-gray-500" />
60
+ </button>
61
+
62
+ <div className="p-6">
63
+ <div className="text-center mb-6">
64
+ <div className="w-12 h-12 bg-rose-50 rounded-full flex items-center justify-center mx-auto mb-3 text-rose-500">
65
+ <MessageSquare className="w-6 h-6" />
66
+ </div>
67
+ <h2 className="text-xl font-bold text-gray-900">{t.feedbackTitle}</h2>
68
+ <p className="text-sm text-gray-500 mt-1">{t.feedbackDesc}</p>
69
+ </div>
70
+
71
+ <form onSubmit={handleSubmit} className="space-y-5">
72
+ {/* Rating */}
73
+ <div className="text-center">
74
+ <label className="block text-xs font-bold text-gray-500 mb-2">{t.feedbackRating}</label>
75
+ <div className="flex justify-center gap-2">
76
+ {[1, 2, 3, 4, 5].map((star) => (
77
+ <button
78
+ key={star}
79
+ type="button"
80
+ onClick={() => setRating(star)}
81
+ className="focus:outline-none transition-transform hover:scale-110"
82
+ >
83
+ <Star className={`w-8 h-8 ${star <= rating ? 'fill-yellow-400 text-yellow-400' : 'text-gray-300'}`} />
84
+ </button>
85
+ ))}
86
+ </div>
87
+ </div>
88
+
89
+ {/* Type Selection */}
90
+ <div className="grid grid-cols-3 gap-2">
91
+ <button
92
+ type="button"
93
+ onClick={() => setType('bug')}
94
+ className={`p-3 rounded-xl border flex flex-col items-center gap-1 transition-all ${type === 'bug' ? 'bg-red-50 border-red-200 text-red-600' : 'bg-white border-gray-200 text-gray-500 hover:bg-gray-50'}`}
95
+ >
96
+ <AlertCircle className="w-5 h-5" />
97
+ <span className="text-xs font-bold">{t.feedbackTypeBug}</span>
98
+ </button>
99
+ <button
100
+ type="button"
101
+ onClick={() => setType('suggestion')}
102
+ className={`p-3 rounded-xl border flex flex-col items-center gap-1 transition-all ${type === 'suggestion' ? 'bg-amber-50 border-amber-200 text-amber-600' : 'bg-white border-gray-200 text-gray-500 hover:bg-gray-50'}`}
103
+ >
104
+ <Lightbulb className="w-5 h-5" />
105
+ <span className="text-xs font-bold">{t.feedbackTypeSugg}</span>
106
+ </button>
107
+ <button
108
+ type="button"
109
+ onClick={() => setType('other')}
110
+ className={`p-3 rounded-xl border flex flex-col items-center gap-1 transition-all ${type === 'other' ? 'bg-blue-50 border-blue-200 text-blue-600' : 'bg-white border-gray-200 text-gray-500 hover:bg-gray-50'}`}
111
+ >
112
+ <HelpCircle className="w-5 h-5" />
113
+ <span className="text-xs font-bold">{t.feedbackTypeOther}</span>
114
+ </button>
115
+ </div>
116
+
117
+ {/* Content */}
118
+ <div>
119
+ <div className="relative">
120
+ <textarea
121
+ value={content}
122
+ onChange={(e) => setContent(e.target.value)}
123
+ placeholder={t.feedbackContentPlace}
124
+ className="w-full p-4 rounded-xl border border-gray-200 focus:border-rose-500 focus:ring-1 focus:ring-rose-200 outline-none h-32 resize-none bg-gray-50 focus:bg-white transition-all text-sm"
125
+ required
126
+ />
127
+ <div className="absolute top-3 right-3 opacity-50 pointer-events-none">
128
+ {getIcon()}
129
+ </div>
130
+ </div>
131
+ </div>
132
+
133
+ <button
134
+ type="submit"
135
+ disabled={isSubmitting || !content.trim()}
136
+ className="w-full py-3 bg-gray-900 text-white rounded-xl font-bold shadow-lg hover:bg-black transition-all flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
137
+ >
138
+ {isSubmitting && <Loader2 className="w-4 h-4 animate-spin" />}
139
+ {t.feedbackSubmit}
140
+ </button>
141
+ </form>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ );
146
+ };
components/FilterSelector.tsx ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Palette, Sun, Moon, Coffee, Droplet, Zap, Maximize, Aperture, Sparkles, Cloud } from 'lucide-react';
3
+ import { Language } from '../types';
4
+ import { TRANSLATIONS } from '../constants/translations';
5
+
6
+ export interface ImageFilter {
7
+ id: string;
8
+ name: string;
9
+ css: string; // CSS filter string
10
+ icon?: React.ReactNode;
11
+ }
12
+
13
+ export const FILTERS: ImageFilter[] = [
14
+ { id: 'none', name: 'Original', css: 'none', icon: <Palette className="w-4 h-4" /> },
15
+ { id: 'bw', name: 'B&W', css: 'grayscale(100%)', icon: <Aperture className="w-4 h-4" /> },
16
+ { id: 'sepia', name: 'Sepia', css: 'sepia(100%)', icon: <Coffee className="w-4 h-4" /> },
17
+ { id: 'vintage', name: 'Vintage', css: 'sepia(0.4) contrast(1.2) brightness(0.9)', icon: <Droplet className="w-4 h-4" /> },
18
+ { id: 'soft', name: 'Soft', css: 'brightness(1.1) contrast(0.9) saturate(0.8)', icon: <Sun className="w-4 h-4" /> },
19
+ { id: 'dramatic', name: 'Dramatic', css: 'contrast(1.4) saturate(0.2)', icon: <Zap className="w-4 h-4" /> },
20
+ { id: 'golden', name: 'Golden', css: 'sepia(0.3) saturate(1.4) contrast(1.1)', icon: <Maximize className="w-4 h-4" /> },
21
+ { id: 'glow', name: 'Glow', css: 'brightness(1.1) saturate(1.2) contrast(0.9)', icon: <Sparkles className="w-4 h-4" /> },
22
+ { id: 'dark', name: 'Dark Mode', css: 'brightness(0.8) contrast(1.2) saturate(1.1) hue-rotate(-15deg)', icon: <Moon className="w-4 h-4" /> },
23
+ ];
24
+
25
+ interface FilterSelectorProps {
26
+ selectedFilter: string;
27
+ onSelect: (filterId: string) => void;
28
+ blurAmount: number;
29
+ onBlurChange: (amount: number) => void;
30
+ disabled: boolean;
31
+ language: Language;
32
+ }
33
+
34
+ export const FilterSelector: React.FC<FilterSelectorProps> = ({
35
+ selectedFilter,
36
+ onSelect,
37
+ blurAmount,
38
+ onBlurChange,
39
+ disabled,
40
+ language
41
+ }) => {
42
+ const t = TRANSLATIONS[language];
43
+
44
+ return (
45
+ <div className="w-full space-y-4">
46
+ {/* Filters Row */}
47
+ <div>
48
+ <div className="flex items-center gap-2 mb-2">
49
+ <Palette className="w-4 h-4 text-gray-500" />
50
+ <h4 className="text-sm font-bold text-gray-700">{t.colorFilters}</h4>
51
+ </div>
52
+ <div className="flex flex-wrap gap-2">
53
+ {FILTERS.map((filter) => (
54
+ <button
55
+ key={filter.id}
56
+ onClick={() => onSelect(filter.id)}
57
+ disabled={disabled}
58
+ className={`
59
+ flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-medium border transition-all
60
+ ${selectedFilter === filter.id
61
+ ? 'bg-rose-50 border-rose-500 text-rose-700 shadow-sm'
62
+ : 'bg-white border-gray-200 text-gray-600 hover:border-rose-200 hover:bg-gray-50'}
63
+ ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
64
+ `}
65
+ >
66
+ {filter.icon}
67
+ {filter.name}
68
+ </button>
69
+ ))}
70
+ </div>
71
+ </div>
72
+
73
+ {/* Blur Slider Row */}
74
+ <div className="pt-2">
75
+ <div className="flex items-center justify-between mb-2">
76
+ <div className="flex items-center gap-2">
77
+ <Cloud className="w-4 h-4 text-gray-500" />
78
+ <h4 className="text-sm font-bold text-gray-700">{t.bgBlur}</h4>
79
+ </div>
80
+ <span className="text-xs font-mono text-gray-500 bg-gray-100 px-2 py-0.5 rounded">
81
+ {blurAmount.toFixed(1)}px
82
+ </span>
83
+ </div>
84
+ <div className="flex items-center gap-4">
85
+ <input
86
+ type="range"
87
+ min="0"
88
+ max="10"
89
+ step="0.5"
90
+ value={blurAmount}
91
+ onChange={(e) => onBlurChange(parseFloat(e.target.value))}
92
+ disabled={disabled}
93
+ className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-rose-500"
94
+ />
95
+ </div>
96
+ <p className="text-[10px] text-gray-400 mt-1">
97
+ {t.bgBlurDesc}
98
+ </p>
99
+ </div>
100
+ </div>
101
+ );
102
+ };
components/FloatingConcierge.tsx ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { Phone, MessageCircle, X, ChevronLeft } from 'lucide-react';
3
+ import { Language, AdminConfig } from '../types';
4
+ import { TRANSLATIONS } from '../constants/translations';
5
+
6
+ interface FloatingConciergeProps {
7
+ language: Language;
8
+ config: AdminConfig;
9
+ }
10
+
11
+ export const FloatingConcierge: React.FC<FloatingConciergeProps> = ({ language, config }) => {
12
+ const [isOpen, setIsOpen] = useState(false);
13
+ const t = TRANSLATIONS[language];
14
+
15
+ const handleContact = (type: 'phone' | 'wechat') => {
16
+ if (type === 'phone' && config.contactPhone) {
17
+ window.location.href = `tel:${config.contactPhone}`;
18
+ }
19
+ // WeChat logic usually involves showing a QR code, handled by the UI expansion
20
+ };
21
+
22
+ if (!isOpen) {
23
+ return (
24
+ <button
25
+ onClick={() => setIsOpen(true)}
26
+ className="fixed right-0 top-1/2 -translate-y-1/2 z-40 bg-rose-600 text-white p-3 rounded-l-xl shadow-lg hover:bg-rose-700 transition-all flex flex-col items-center gap-1 group"
27
+ aria-label="Open Concierge"
28
+ >
29
+ <MessageCircle className="w-5 h-5 animate-pulse" />
30
+ <span className="text-[10px] font-bold writing-vertical-lr hidden sm:block pt-1">咨询</span>
31
+ <ChevronLeft className="w-3 h-3 group-hover:-translate-x-1 transition-transform" />
32
+ </button>
33
+ );
34
+ }
35
+
36
+ return (
37
+ <div className="fixed right-4 top-1/2 -translate-y-1/2 z-50 animate-fade-in">
38
+ <div className="bg-white rounded-2xl shadow-2xl p-5 border border-rose-100 w-64 relative">
39
+ <button
40
+ onClick={() => setIsOpen(false)}
41
+ className="absolute -top-3 -right-3 bg-gray-900 text-white p-1.5 rounded-full hover:bg-black transition-colors shadow-md"
42
+ >
43
+ <X className="w-4 h-4" />
44
+ </button>
45
+
46
+ <div className="text-center mb-4">
47
+ <h3 className="font-bold text-gray-800 text-lg">金牌管家</h3>
48
+ <p className="text-xs text-gray-500">1对1 专属服务</p>
49
+ </div>
50
+
51
+ <div className="space-y-4">
52
+ {/* Phone */}
53
+ <div className="flex items-center gap-3 p-3 bg-gray-50 rounded-xl hover:bg-rose-50 transition-colors cursor-pointer" onClick={() => handleContact('phone')}>
54
+ <div className="w-10 h-10 bg-rose-100 rounded-full flex items-center justify-center text-rose-600">
55
+ <Phone className="w-5 h-5" />
56
+ </div>
57
+ <div className="text-left">
58
+ <p className="text-xs font-bold text-gray-500">电话咨询</p>
59
+ <p className="text-sm font-bold text-gray-800">{config.contactPhone || '0592-8888888'}</p>
60
+ </div>
61
+ </div>
62
+
63
+ {/* WeChat / QR */}
64
+ <div className="flex flex-col items-center gap-2 p-3 bg-gray-50 rounded-xl border-2 border-dashed border-gray-200">
65
+ <div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center text-green-600 mb-1">
66
+ <MessageCircle className="w-5 h-5" />
67
+ </div>
68
+ <p className="text-xs font-bold text-gray-500 mb-1">添加微信客服</p>
69
+ <div className="w-32 h-32 bg-white p-1 rounded-lg shadow-sm">
70
+ <img
71
+ src={config.qrCodeUrl || "https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=https://romantic-life.com"}
72
+ alt="WeChat QR"
73
+ className="w-full h-full object-cover rounded"
74
+ />
75
+ </div>
76
+ <p className="text-[10px] text-gray-400">扫一扫,获取最新报价</p>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ );
82
+ };
components/Header.tsx ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Camera, Globe, PhoneCall, User, LogIn, Settings, Info, MessageSquarePlus } from 'lucide-react';
3
+ import { Language, UserAccount, AdminConfig } from '../types';
4
+ import { TRANSLATIONS } from '../constants/translations';
5
+
6
+ interface HeaderProps {
7
+ language: Language;
8
+ onLanguageChange: (lang: Language) => void;
9
+ onBookClick: () => void;
10
+ user?: UserAccount;
11
+ config?: AdminConfig; // NEW: Receive AdminConfig for logo
12
+ onOpenUserCenter?: () => void;
13
+ onLoginClick?: () => void;
14
+ onOpenAdmin?: () => void;
15
+ onOpenAbout?: () => void;
16
+ onOpenFeedback?: () => void;
17
+ }
18
+
19
+ export const Header: React.FC<HeaderProps> = ({ language, onLanguageChange, onBookClick, user, config, onOpenUserCenter, onLoginClick, onOpenAdmin, onOpenAbout, onOpenFeedback }) => {
20
+ const t = TRANSLATIONS[language];
21
+ const isStaffOrAdmin = user && (user.role === 'admin' || user.role === 'staff');
22
+
23
+ return (
24
+ <header className="w-full py-4 sm:py-6 px-4 sm:px-8 border-b border-rose-100 bg-white/80 backdrop-blur-md sticky top-0 z-50">
25
+ <div className="max-w-7xl mx-auto flex items-center justify-between">
26
+ <div className="flex items-center gap-3 cursor-pointer" onClick={() => window.location.reload()}>
27
+ {config?.logoUrl ? (
28
+ <div className="w-10 h-10 sm:w-12 sm:h-12 rounded-full overflow-hidden shadow-lg ring-2 ring-rose-100">
29
+ <img src={config.logoUrl} alt="Logo" className="w-full h-full object-cover" />
30
+ </div>
31
+ ) : (
32
+ <div className="w-10 h-10 sm:w-12 sm:h-12 bg-gray-900 rounded-full flex items-center justify-center text-rose-300 shadow-lg ring-2 ring-rose-100">
33
+ <Camera className="w-6 h-6" />
34
+ </div>
35
+ )}
36
+
37
+ <div>
38
+ <h1 className="font-serif text-xl sm:text-2xl font-bold text-gray-900 tracking-tight leading-none">
39
+ {t.title}
40
+ </h1>
41
+ <p className="text-[10px] sm:text-xs text-rose-500 font-sans tracking-widest uppercase font-bold mt-1">
42
+ {t.subtitle}
43
+ </p>
44
+ </div>
45
+ </div>
46
+
47
+ <div className="flex items-center gap-3 sm:gap-4">
48
+ {/* About Us Button (Desktop) */}
49
+ <button
50
+ onClick={onOpenAbout}
51
+ className="hidden sm:flex items-center gap-1 text-xs font-bold text-gray-600 hover:text-rose-600 transition-colors mr-2"
52
+ >
53
+ <Info className="w-3.5 h-3.5" />
54
+ {t.aboutUsBtn}
55
+ </button>
56
+
57
+ {/* Feedback Button */}
58
+ <button
59
+ onClick={onOpenFeedback}
60
+ className="flex items-center justify-center w-8 h-8 rounded-full hover:bg-gray-100 text-gray-500 hover:text-rose-500 transition-colors"
61
+ title="Feedback"
62
+ >
63
+ <MessageSquarePlus className="w-5 h-5" />
64
+ </button>
65
+
66
+ {/* Admin Panel Button */}
67
+ {isStaffOrAdmin && (
68
+ <button
69
+ onClick={onOpenAdmin}
70
+ className="hidden sm:flex items-center gap-2 px-3 py-1.5 bg-gray-800 text-white rounded-full text-xs font-bold hover:bg-gray-700 transition-colors"
71
+ >
72
+ <Settings className="w-3.5 h-3.5" />
73
+ Admin Panel
74
+ </button>
75
+ )}
76
+
77
+ {/* Book Now Button (Mobile: Icon only, Desktop: Text) */}
78
+ <button
79
+ onClick={onBookClick}
80
+ className="flex items-center gap-2 bg-rose-600 hover:bg-rose-700 text-white px-4 py-2 rounded-full shadow-lg shadow-rose-200 transition-all hover:scale-105 active:scale-95"
81
+ >
82
+ <PhoneCall className="w-4 h-4" />
83
+ <span className="hidden sm:inline text-xs font-bold uppercase tracking-wide">{t.bookNow}</span>
84
+ </button>
85
+
86
+ {/* User Avatar / Points OR Login Button */}
87
+ {user ? (
88
+ <button
89
+ onClick={onOpenUserCenter}
90
+ className="flex items-center gap-2 bg-gray-50 hover:bg-rose-50 border border-gray-200 pl-2 pr-3 py-1.5 rounded-full transition-colors group"
91
+ >
92
+ <div className="w-6 h-6 rounded-full bg-gray-200 flex items-center justify-center overflow-hidden">
93
+ {user.avatar ? <img src={user.avatar} className="w-full h-full object-cover" /> : <User className="w-4 h-4 text-gray-500" />}
94
+ </div>
95
+ <div className="flex flex-col items-start leading-none">
96
+ <span className="text-[9px] text-gray-400 font-bold uppercase">{t.myPoints}</span>
97
+ <span className="text-xs font-bold text-amber-500 group-hover:text-amber-600">{user.points}</span>
98
+ </div>
99
+ </button>
100
+ ) : (
101
+ <button
102
+ onClick={onLoginClick}
103
+ className="flex items-center gap-2 text-gray-600 hover:text-rose-600 font-bold text-xs"
104
+ >
105
+ <LogIn className="w-4 h-4" />
106
+ <span className="hidden sm:inline">{t.authLogin}</span>
107
+ </button>
108
+ )}
109
+
110
+ {/* Language Selector */}
111
+ <div className="relative group hidden sm:block">
112
+ <button className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-white border border-gray-200 text-xs font-semibold text-gray-700 hover:bg-gray-50 transition-colors">
113
+ <Globe className="w-3.5 h-3.5" />
114
+ <span className="uppercase">{language}</span>
115
+ </button>
116
+
117
+ <div className="absolute right-0 mt-2 w-32 bg-white rounded-xl shadow-xl border border-gray-100 overflow-hidden hidden group-hover:block animate-fade-in z-50">
118
+ <div className="flex flex-col py-1">
119
+ {['en', 'zh', 'ja', 'ko', 'es', 'fr'].map((lang) => (
120
+ <button
121
+ key={lang}
122
+ onClick={() => onLanguageChange(lang as Language)}
123
+ className={`px-4 py-2 text-left text-xs hover:bg-rose-50 ${language === lang ? 'text-rose-600 font-bold bg-rose-50' : 'text-gray-700'}`}
124
+ >
125
+ {lang === 'en' ? 'English' :
126
+ lang === 'zh' ? '中文' :
127
+ lang === 'ja' ? '日本語' :
128
+ lang === 'ko' ? '한국어' :
129
+ lang === 'es' ? 'Español' : 'Français'}
130
+ </button>
131
+ ))}
132
+ </div>
133
+ </div>
134
+ </div>
135
+ </div>
136
+ </div>
137
+ </header>
138
+ );
139
+ };
components/ImageUploader.tsx ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useCallback } from 'react';
2
+ import { Upload, X, Plus } from 'lucide-react';
3
+ import { Language } from '../types';
4
+ import { TRANSLATIONS } from '../constants/translations';
5
+
6
+ interface ImageUploaderProps {
7
+ onImagesSelect: (base64Images: string[]) => void;
8
+ currentImages: string[];
9
+ disabled: boolean;
10
+ language: Language;
11
+ }
12
+
13
+ export const ImageUploader: React.FC<ImageUploaderProps> = ({ onImagesSelect, currentImages, disabled, language }) => {
14
+ const t = TRANSLATIONS[language];
15
+
16
+ const handleFileChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
17
+ const files = event.target.files;
18
+ if (files && files.length > 0) {
19
+ const newImages: string[] = [];
20
+ let processed = 0;
21
+
22
+ // Limit to 5 images for performance
23
+ const maxFiles = Math.min(files.length, 5);
24
+
25
+ for (let i = 0; i < maxFiles; i++) {
26
+ const file = files[i];
27
+ if (file.size > 5 * 1024 * 1024) {
28
+ alert(`File ${file.name} is too large. Skip.`);
29
+ processed++;
30
+ if (processed === maxFiles) onImagesSelect([...currentImages, ...newImages]);
31
+ continue;
32
+ }
33
+
34
+ const reader = new FileReader();
35
+ reader.onloadend = () => {
36
+ newImages.push(reader.result as string);
37
+ processed++;
38
+ if (processed === maxFiles) {
39
+ // Append to existing images
40
+ onImagesSelect([...currentImages, ...newImages]);
41
+ }
42
+ };
43
+ reader.readAsDataURL(file);
44
+ }
45
+ }
46
+ }, [currentImages, onImagesSelect]);
47
+
48
+ const removeImage = (index: number) => {
49
+ const newImages = currentImages.filter((_, i) => i !== index);
50
+ onImagesSelect(newImages);
51
+ };
52
+
53
+ const hasImages = currentImages.length > 0;
54
+
55
+ return (
56
+ <div className="w-full">
57
+ <div className={`
58
+ relative w-full min-h-[300px] rounded-2xl border-2 border-dashed
59
+ transition-all duration-300 flex flex-col items-center justify-center p-4
60
+ ${hasImages ? 'border-rose-300 bg-rose-50' : 'border-gray-300 bg-gray-50 hover:bg-gray-100 hover:border-gray-400'}
61
+ ${disabled ? 'opacity-60 cursor-not-allowed' : ''}
62
+ `}>
63
+
64
+ {!hasImages ? (
65
+ // Empty State
66
+ <div className="flex flex-col items-center justify-center text-center pointer-events-none">
67
+ <div className="w-16 h-16 mb-4 rounded-full bg-white flex items-center justify-center shadow-sm">
68
+ <Upload className="w-8 h-8 text-rose-400" />
69
+ </div>
70
+ <p className="text-lg font-medium text-gray-700">{t.uploadTitle}</p>
71
+ <p className="text-sm text-gray-500 mt-2 max-w-xs">{t.uploadDesc}</p>
72
+ </div>
73
+ ) : (
74
+ // Grid View for Multiple Images
75
+ <div className="w-full grid grid-cols-2 sm:grid-cols-3 gap-4">
76
+ {currentImages.map((img, idx) => (
77
+ <div key={idx} className="relative aspect-[3/4] rounded-xl overflow-hidden shadow-sm group">
78
+ <img src={img} alt={`Uploaded ${idx}`} className="w-full h-full object-cover" />
79
+ {!disabled && (
80
+ <button
81
+ onClick={() => removeImage(idx)}
82
+ className="absolute top-2 right-2 bg-black/50 text-white p-1 rounded-full hover:bg-red-500 transition-colors"
83
+ >
84
+ <X className="w-4 h-4" />
85
+ </button>
86
+ )}
87
+ </div>
88
+ ))}
89
+
90
+ {/* Add More Button (if less than 5) */}
91
+ {currentImages.length < 5 && !disabled && (
92
+ <div className="relative aspect-[3/4] rounded-xl border-2 border-dashed border-rose-300 flex flex-col items-center justify-center bg-white/50 hover:bg-white transition-colors cursor-pointer group">
93
+ <Plus className="w-8 h-8 text-rose-400 mb-2 group-hover:scale-110 transition-transform" />
94
+ <span className="text-xs text-rose-500 font-bold">Add Photo</span>
95
+ <input
96
+ type="file"
97
+ multiple
98
+ accept="image/png, image/jpeg, image/jpg, image/webp"
99
+ onChange={handleFileChange}
100
+ disabled={disabled}
101
+ className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
102
+ />
103
+ </div>
104
+ )}
105
+ </div>
106
+ )}
107
+
108
+ {/* Hidden Input for Initial Drag/Drop area */}
109
+ {!hasImages && (
110
+ <input
111
+ type="file"
112
+ multiple
113
+ accept="image/png, image/jpeg, image/jpg, image/webp"
114
+ onChange={handleFileChange}
115
+ disabled={disabled}
116
+ className="absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed"
117
+ />
118
+ )}
119
+ </div>
120
+
121
+ {hasImages && (
122
+ <p className="text-center text-xs text-gray-400 mt-2">
123
+ {currentImages.length} photo(s) selected. Max 5.
124
+ </p>
125
+ )}
126
+ </div>
127
+ );
128
+ };
components/RedPacketModal.tsx ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { X, Gift } from 'lucide-react';
3
+ import { Language, AdminConfig, UserAccount } from '../types';
4
+ import { TRANSLATIONS } from '../constants/translations';
5
+ // @ts-ignore
6
+ import confetti from 'canvas-confetti';
7
+
8
+ interface RedPacketModalProps {
9
+ isVisible: boolean;
10
+ onClose: () => void;
11
+ language: Language;
12
+ adminConfig: AdminConfig;
13
+ user?: UserAccount;
14
+ onUpdateUser: (balance: number) => void;
15
+ onOpenShare: () => void;
16
+ }
17
+
18
+ export const RedPacketModal: React.FC<RedPacketModalProps> = ({
19
+ isVisible, onClose, language, adminConfig, user, onUpdateUser, onOpenShare
20
+ }) => {
21
+ const [stage, setStage] = useState<'closed' | 'opened'>('closed');
22
+ const [amount, setAmount] = useState(0);
23
+ const t = TRANSLATIONS[language];
24
+
25
+ // Determine target amount
26
+ const max = adminConfig.redPacketMax || 2000;
27
+ // If user exists, show their balance. If no user (guest), show default almost-max (e.g. max - 20)
28
+ const userBalance = user?.redPacketBalance ?? (max - 20);
29
+
30
+ useEffect(() => {
31
+ if (isVisible) {
32
+ setStage('closed');
33
+
34
+ // Animate amount up to userBalance
35
+ let current = 0;
36
+ // Animation speed
37
+ const step = userBalance / 40;
38
+ const interval = setInterval(() => {
39
+ current += step;
40
+ if (current >= userBalance) {
41
+ current = userBalance;
42
+ clearInterval(interval);
43
+ }
44
+ setAmount(Math.floor(current));
45
+ }, 30);
46
+ return () => clearInterval(interval);
47
+ }
48
+ }, [isVisible, userBalance]);
49
+
50
+ if (!isVisible) return null;
51
+
52
+ const handleOpen = () => {
53
+ setStage('opened');
54
+ confetti({
55
+ particleCount: 150,
56
+ spread: 80,
57
+ origin: { y: 0.6 },
58
+ colors: ['#fbbf24', '#ef4444', '#ffffff']
59
+ });
60
+ // Ensure user gets this balance if they haven't already
61
+ if (user && (user.redPacketBalance === undefined || user.redPacketBalance === 0)) {
62
+ onUpdateUser(userBalance);
63
+ }
64
+ };
65
+
66
+ const handleWithdraw = () => {
67
+ onOpenShare();
68
+ onClose();
69
+ };
70
+
71
+ return (
72
+ <div className="fixed inset-0 z-[120] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-fade-in">
73
+ <div className="relative w-full max-w-sm">
74
+ <button onClick={onClose} className="absolute -top-10 right-0 p-2 bg-white/20 rounded-full text-white hover:bg-white/40">
75
+ <X className="w-5 h-5" />
76
+ </button>
77
+
78
+ {stage === 'closed' ? (
79
+ // Stage 1: The Big Red Envelope
80
+ <div className="bg-gradient-to-b from-red-500 to-red-600 rounded-3xl shadow-2xl overflow-hidden text-center p-8 relative animate-bounce-slow">
81
+ <div className="absolute top-0 left-1/2 -translate-x-1/2 w-32 h-32 bg-red-700 rounded-b-full opacity-50"></div>
82
+ <div className="relative z-10 pt-10">
83
+ <p className="text-yellow-200 text-lg font-bold mb-2">{t.pddRedPacketTitle}</p>
84
+ <h3 className="text-3xl font-bold text-white mb-8">{t.pddRedPacketDesc}</h3>
85
+
86
+ <button
87
+ onClick={handleOpen}
88
+ className="w-24 h-24 rounded-full bg-yellow-400 border-4 border-yellow-200 text-red-600 font-bold text-xl shadow-lg hover:scale-110 transition-transform flex items-center justify-center mx-auto"
89
+ >
90
+ <span className="animate-pulse">{t.pddRedPacketCta}</span>
91
+ </button>
92
+ </div>
93
+ </div>
94
+ ) : (
95
+ // Stage 2: The "Almost There" Trap
96
+ <div className="bg-white rounded-3xl shadow-2xl overflow-hidden relative">
97
+ <div className="bg-red-500 p-6 text-center pb-12">
98
+ <p className="text-white/80 text-sm font-bold uppercase tracking-wider">Account Balance</p>
99
+ <h2 className="text-5xl font-bold text-white mt-2">¥{amount}</h2>
100
+ </div>
101
+
102
+ <div className="px-6 -mt-8 relative z-10">
103
+ <div className="bg-white rounded-xl shadow-lg p-6 border border-gray-100 text-center">
104
+ <p className="text-gray-800 font-bold text-lg">{t.pddWithdrawTitle}</p>
105
+ <div className="w-full h-3 bg-gray-100 rounded-full mt-3 overflow-hidden">
106
+ <div className="h-full bg-red-500 animate-pulse" style={{ width: `${(amount / max) * 100}%` }}></div>
107
+ </div>
108
+ <p className="text-xs text-red-500 mt-2 font-bold">{t.pddWithdrawDesc}</p>
109
+
110
+ <button
111
+ onClick={handleWithdraw}
112
+ className="w-full py-3 bg-red-500 text-white rounded-full font-bold mt-4 shadow-lg shadow-red-200 hover:bg-red-600 flex items-center justify-center gap-2"
113
+ >
114
+ <Gift className="w-4 h-4" /> Share to Withdraw
115
+ </button>
116
+ </div>
117
+ </div>
118
+
119
+ <div className="p-6 bg-gray-50">
120
+ <div className="space-y-3">
121
+ {['User 123 withdrew ¥2000', 'Amy unlocked VIP', 'Mike got ¥500'].map((txt, i) => (
122
+ <div key={i} className="flex items-center gap-2 text-xs text-gray-500">
123
+ <div className="w-6 h-6 bg-gray-200 rounded-full"></div>
124
+ <span>{txt}</span>
125
+ <span className="ml-auto text-gray-400">1m ago</span>
126
+ </div>
127
+ ))}
128
+ </div>
129
+ </div>
130
+ </div>
131
+ )}
132
+ </div>
133
+ </div>
134
+ );
135
+ };
components/ResultViewer.tsx ADDED
@@ -0,0 +1,762 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useRef, useCallback } from 'react';
2
+ import { Download, Maximize2, X, Grid, Loader2, Video, Settings, Type, Sparkles, Layers, Stamp, Clock, PlayCircle, Share2, CalendarCheck, ArrowLeftRight, Music, FileText, CheckCircle, Monitor, SplitSquareHorizontal, Zap, Activity, Snowflake, ChevronDown, ChevronUp, CheckSquare, Square, Check } from 'lucide-react';
3
+ import { GeneratedResult, WeddingStyle, Language, VideoTemplate } from '../types';
4
+ import { STYLES } from './StyleSelector';
5
+ import { FILTERS } from './FilterSelector';
6
+ import { TRANSLATIONS } from '../constants/translations';
7
+ import JSZip from 'jszip';
8
+
9
+ interface ResultViewerProps {
10
+ originalImages: string[];
11
+ results: Record<string, GeneratedResult>;
12
+ initialSelectedId?: string;
13
+ onReset: () => void;
14
+ activeFilter?: string;
15
+ blurAmount?: number;
16
+ language: Language;
17
+ onBookClick: () => void;
18
+ onShareClick: () => void;
19
+ }
20
+
21
+ interface VideoConfig {
22
+ transition: 'fade' | 'slide' | 'none';
23
+ customTitle: string;
24
+ enableCustomTitle: boolean;
25
+ showWatermark: boolean;
26
+ transitionDuration: number;
27
+ musicTrack: string;
28
+ format: 'mp4' | 'webm';
29
+ resolution: '720p' | '1080p';
30
+ template: VideoTemplate;
31
+ }
32
+
33
+ // Available Music Options
34
+ const MUSIC_OPTIONS = [
35
+ { id: 'none', labelKey: 'musicNone', src: '' },
36
+ { id: 'romantic', labelKey: 'musicRomantic', src: 'https://cdn.pixabay.com/download/audio/2022/05/27/audio_1808fbf07a.mp3?filename=romantic-wedding-111663.mp3' },
37
+ { id: 'cinematic', labelKey: 'musicCinematic', src: 'https://cdn.pixabay.com/download/audio/2022/03/24/audio_3335555c27.mp3?filename=cinematic-atmosphere-1936.mp3' },
38
+ { id: 'upbeat', labelKey: 'musicUpbeat', src: 'https://cdn.pixabay.com/download/audio/2022/10/25/audio_5e062d1646.mp3?filename=upbeat-pop-1234.mp3' },
39
+ ];
40
+
41
+ const applyEffectsToCanvas = (
42
+ ctx: CanvasRenderingContext2D,
43
+ img: HTMLImageElement,
44
+ width: number,
45
+ height: number,
46
+ filter: string,
47
+ blurAmount: number,
48
+ addWatermark: boolean,
49
+ watermarkText: string
50
+ ) => {
51
+ ctx.filter = filter;
52
+ ctx.drawImage(img, 0, 0, width, height);
53
+ ctx.filter = 'none';
54
+
55
+ if (blurAmount > 0) {
56
+ const offCanvas = document.createElement('canvas');
57
+ offCanvas.width = width;
58
+ offCanvas.height = height;
59
+ const offCtx = offCanvas.getContext('2d');
60
+
61
+ if (offCtx) {
62
+ const scale = Math.max(1, width / 600);
63
+ const scaledBlur = blurAmount * scale;
64
+ const filterStr = filter !== 'none' ? `${filter} blur(${scaledBlur}px)` : `blur(${scaledBlur}px)`;
65
+ offCtx.filter = filterStr;
66
+ offCtx.drawImage(img, 0, 0, width, height);
67
+ offCtx.globalCompositeOperation = 'destination-out';
68
+ const minDim = Math.min(width, height);
69
+ const gradient = offCtx.createRadialGradient(
70
+ width / 2, height / 2, minDim * 0.4,
71
+ width / 2, height / 2, minDim * 0.8
72
+ );
73
+ gradient.addColorStop(0, 'rgba(0,0,0,1)');
74
+ gradient.addColorStop(1, 'rgba(0,0,0,0)');
75
+ offCtx.fillStyle = gradient;
76
+ offCtx.fillRect(0, 0, width, height);
77
+ ctx.globalCompositeOperation = 'source-over';
78
+ ctx.drawImage(offCanvas, 0, 0);
79
+ }
80
+ }
81
+
82
+ if (addWatermark) {
83
+ ctx.save();
84
+ const fontSize = Math.max(14, Math.floor(width * 0.03));
85
+ const padding = Math.floor(fontSize * 1.5);
86
+ ctx.font = `500 ${fontSize}px 'Playfair Display', serif`;
87
+ ctx.textAlign = 'right';
88
+ ctx.textBaseline = 'bottom';
89
+ ctx.shadowColor = 'rgba(0,0,0,0.5)';
90
+ ctx.shadowBlur = 4;
91
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.85)';
92
+ ctx.fillText(watermarkText, width - padding, height - padding);
93
+ ctx.restore();
94
+ }
95
+ };
96
+
97
+ async function generateVideo(
98
+ originalSrcs: string[],
99
+ results: Record<string, GeneratedResult>,
100
+ allStyles: WeddingStyle[],
101
+ filter: string = 'none',
102
+ blurAmount: number = 0,
103
+ config: VideoConfig,
104
+ language: Language,
105
+ targetStyleId?: string
106
+ ): Promise<string | null> {
107
+ return new Promise(async (resolve, reject) => {
108
+ try {
109
+ const canvas = document.createElement('canvas');
110
+ const is1080p = config.resolution === '1080p';
111
+ canvas.width = is1080p ? 1080 : 720;
112
+ canvas.height = is1080p ? 1920 : 1280;
113
+ const ctx = canvas.getContext('2d');
114
+
115
+ if (!ctx) throw new Error("Canvas context failed");
116
+
117
+ // Template Logic
118
+ let durationPerSlide = 2000;
119
+ let transitionDuration = config.transition === 'none' ? 0 : config.transitionDuration;
120
+
121
+ if (config.template === 'flash') {
122
+ durationPerSlide = 500;
123
+ transitionDuration = 0;
124
+ } else if (config.template === 'slow') {
125
+ durationPerSlide = 3000;
126
+ transitionDuration = 1000;
127
+ }
128
+
129
+ // Prepare Audio
130
+ let audioCtx: AudioContext | null = null;
131
+ let dest: MediaStreamAudioDestinationNode | null = null;
132
+ if (config.musicTrack !== 'none') {
133
+ const track = MUSIC_OPTIONS.find(m => m.id === config.musicTrack);
134
+ if (track && track.src) {
135
+ try {
136
+ audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)();
137
+ const response = await fetch(track.src);
138
+ const arrayBuffer = await response.arrayBuffer();
139
+ const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
140
+ dest = audioCtx.createMediaStreamDestination();
141
+ const sourceNode = audioCtx.createBufferSource();
142
+ sourceNode.buffer = audioBuffer;
143
+ sourceNode.loop = true;
144
+ sourceNode.connect(dest);
145
+ sourceNode.start(0);
146
+ } catch (e) { console.warn("Audio load failed", e); }
147
+ }
148
+ }
149
+
150
+ const stream = canvas.captureStream(30);
151
+ if (dest) {
152
+ const audioTrack = dest.stream.getAudioTracks()[0];
153
+ if (audioTrack) stream.addTrack(audioTrack);
154
+ }
155
+
156
+ const mimeType = MediaRecorder.isTypeSupported('video/webm; codecs=vp9') ? 'video/webm; codecs=vp9' : 'video/webm';
157
+ const recorder = new MediaRecorder(stream, { mimeType, videoBitsPerSecond: 4000000 });
158
+ const chunks: Blob[] = [];
159
+ recorder.ondataavailable = (e) => { if (e.data.size > 0) chunks.push(e.data); };
160
+ recorder.onstop = () => {
161
+ const blob = new Blob(chunks, { type: 'video/webm' });
162
+ const url = URL.createObjectURL(blob);
163
+ if (audioCtx) audioCtx.close();
164
+ resolve(url);
165
+ };
166
+ recorder.start();
167
+
168
+ let itemsToRender: GeneratedResult[] = [];
169
+ if (targetStyleId) {
170
+ itemsToRender = [{ styleId: 'ORIGINAL', imageUrl: originalSrcs[0], timestamp: 0 }];
171
+ if(results[targetStyleId]) itemsToRender.push(results[targetStyleId]);
172
+ } else {
173
+ itemsToRender = Object.values(results) as GeneratedResult[];
174
+ }
175
+
176
+ const loadImage = (url: string) => new Promise<HTMLImageElement>(r => {
177
+ const i = new Image(); i.crossOrigin = "anonymous"; i.onload = () => r(i); i.src = url;
178
+ });
179
+
180
+ for (let i = 0; i < itemsToRender.length; i++) {
181
+ const item = itemsToRender[i];
182
+ const img = await loadImage(item.imageUrl);
183
+
184
+ let label = '';
185
+ if (config.enableCustomTitle && config.customTitle) label = config.customTitle;
186
+ else if (item.styleId === 'ORIGINAL') label = 'Before';
187
+ else {
188
+ const s = allStyles.find(st => st.id === item.styleId);
189
+ label = s ? s.name : item.styleId;
190
+ }
191
+
192
+ const startTime = Date.now();
193
+ while (Date.now() - startTime < durationPerSlide) {
194
+ applyEffectsToCanvas(ctx, img, canvas.width, canvas.height, filter, blurAmount, config.showWatermark, "Romantic Life");
195
+
196
+ if (config.template !== 'flash') {
197
+ ctx.fillStyle = 'rgba(0,0,0,0.5)';
198
+ ctx.fillRect(0, canvas.height - 100, canvas.width, 100);
199
+ ctx.fillStyle = 'white';
200
+ ctx.font = 'bold 40px sans-serif';
201
+ ctx.textAlign = 'center';
202
+ ctx.fillText(label, canvas.width / 2, canvas.height - 35);
203
+ }
204
+ await new Promise(r => requestAnimationFrame(r));
205
+ }
206
+
207
+ if (config.template !== 'flash' && i < itemsToRender.length - 1) {
208
+ const nextImg = await loadImage(itemsToRender[i+1].imageUrl);
209
+ const transStart = Date.now();
210
+ while (Date.now() - transStart < transitionDuration) {
211
+ const progress = (Date.now() - transStart) / transitionDuration;
212
+ ctx.globalAlpha = 1;
213
+ applyEffectsToCanvas(ctx, img, canvas.width, canvas.height, filter, blurAmount, config.showWatermark, "Romantic Life");
214
+ ctx.globalAlpha = progress;
215
+ applyEffectsToCanvas(ctx, nextImg, canvas.width, canvas.height, filter, blurAmount, config.showWatermark, "Romantic Life");
216
+ ctx.globalAlpha = 1;
217
+ await new Promise(r => requestAnimationFrame(r));
218
+ }
219
+ }
220
+ }
221
+ recorder.stop();
222
+ } catch (e) { reject(e); }
223
+ });
224
+ }
225
+
226
+ export const ResultViewer: React.FC<ResultViewerProps> = ({
227
+ originalImages, results, initialSelectedId, onReset, activeFilter = 'none', blurAmount = 0, language, onBookClick, onShareClick
228
+ }) => {
229
+ const resultKeys = Object.keys(results);
230
+ const [activeId, setActiveId] = useState<string | null>(initialSelectedId && results[initialSelectedId] ? initialSelectedId : resultKeys[0]);
231
+ const [showVideoSettings, setShowVideoSettings] = useState(false);
232
+ const [videoConfig, setVideoConfig] = useState<VideoConfig>({
233
+ transition: 'fade',
234
+ customTitle: '',
235
+ enableCustomTitle: false,
236
+ showWatermark: true,
237
+ transitionDuration: 800,
238
+ musicTrack: 'none',
239
+ format: 'mp4',
240
+ resolution: '1080p',
241
+ template: 'default'
242
+ });
243
+
244
+ // States
245
+ const [liveEffect, setLiveEffect] = useState<'none'|'breath'|'glitch'|'particles'>('none');
246
+ const [compareMode, setCompareMode] = useState(false);
247
+ const [compareId, setCompareId] = useState<string | null>(null); // null means Original
248
+ const [sliderPosition, setSliderPosition] = useState(50);
249
+ const [isSingleVideoGenerating, setIsSingleVideoGenerating] = useState(false);
250
+ const [includeWatermark, setIncludeWatermark] = useState(true);
251
+ const [openSection, setOpenSection] = useState<'composition' | 'effects' | 'audio' | null>('composition');
252
+ const [isZipping, setIsZipping] = useState(false);
253
+
254
+ // Selection States
255
+ const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
256
+
257
+ const containerRef = useRef<HTMLDivElement>(null);
258
+ const isDragging = useRef(false);
259
+ const t = TRANSLATIONS[language];
260
+
261
+ // SYNC Logic: Listen for parent prop changes
262
+ useEffect(() => {
263
+ if (initialSelectedId && results[initialSelectedId]) {
264
+ setActiveId(initialSelectedId);
265
+ }
266
+ }, [initialSelectedId, results]);
267
+
268
+ const activeResult = activeId ? results[activeId] : null;
269
+ const activeStyle = activeId ? STYLES.find(s => s.id === activeId) : null;
270
+ const activeStyleName = activeStyle ? (t.styles as any)[activeStyle.id] || activeStyle.name : '';
271
+
272
+ // Resolve Comparison Image (Left side)
273
+ const compareResult = compareId ? results[compareId] : null;
274
+ const compareImageUrl = compareResult ? compareResult.imageUrl : originalImages[0];
275
+ const compareLabel = compareId
276
+ ? (STYLES.find(s => s.id === compareId) ? ((t.styles as any)[compareId] || compareId) : compareId)
277
+ : t.original;
278
+
279
+ // Slider Handlers
280
+ const handleMouseDown = useCallback(() => { isDragging.current = true; }, []);
281
+ const handleMouseUp = useCallback(() => { isDragging.current = false; }, []);
282
+
283
+ const updateSlider = useCallback((clientX: number) => {
284
+ if (!containerRef.current) return;
285
+ const rect = containerRef.current.getBoundingClientRect();
286
+ const pos = ((clientX - rect.left) / rect.width) * 100;
287
+ setSliderPosition(Math.max(0, Math.min(100, pos)));
288
+ }, []);
289
+
290
+ const handleMouseMove = useCallback((e: MouseEvent | TouchEvent) => {
291
+ if (!isDragging.current) return;
292
+ const clientX = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX;
293
+ updateSlider(clientX);
294
+ }, [updateSlider]);
295
+
296
+ const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
297
+ if (!compareMode) return;
298
+
299
+ let delta = 0;
300
+ if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') delta = -5;
301
+ if (e.key === 'ArrowRight' || e.key === 'ArrowUp') delta = 5;
302
+
303
+ if (delta !== 0) {
304
+ e.preventDefault();
305
+ setSliderPosition(prev => Math.max(0, Math.min(100, prev + delta)));
306
+ }
307
+ }, [compareMode]);
308
+
309
+ useEffect(() => {
310
+ window.addEventListener('mouseup', handleMouseUp);
311
+ window.addEventListener('touchend', handleMouseUp);
312
+ window.addEventListener('mousemove', handleMouseMove);
313
+ window.addEventListener('touchmove', handleMouseMove);
314
+ return () => {
315
+ window.removeEventListener('mouseup', handleMouseUp);
316
+ window.removeEventListener('touchend', handleMouseUp);
317
+ window.removeEventListener('mousemove', handleMouseMove);
318
+ window.removeEventListener('touchmove', handleMouseMove);
319
+ };
320
+ }, [handleMouseMove, handleMouseUp]);
321
+
322
+ // Selection Logic
323
+ const toggleSelection = (id: string, e: React.MouseEvent) => {
324
+ e.stopPropagation();
325
+ const newSet = new Set(selectedIds);
326
+ if (newSet.has(id)) newSet.delete(id);
327
+ else newSet.add(id);
328
+ setSelectedIds(newSet);
329
+ };
330
+
331
+ const handleSelectAll = () => {
332
+ const resultIds = Object.keys(results);
333
+ // If all currently selected, deselect all. Otherwise select all.
334
+ // Check if current selection covers all available results
335
+ const allSelected = resultIds.every(id => selectedIds.has(id));
336
+
337
+ if (allSelected) {
338
+ setSelectedIds(new Set());
339
+ } else {
340
+ setSelectedIds(new Set(resultIds));
341
+ }
342
+ };
343
+
344
+ const handleGenerateSingleVideo = async () => {
345
+ if (!activeResult || !activeStyle) return;
346
+ setIsSingleVideoGenerating(true);
347
+ try {
348
+ const url = await generateVideo(originalImages, results, STYLES, activeFilter, blurAmount, videoConfig, language, activeResult.styleId);
349
+ if (url) {
350
+ const link = document.createElement('a');
351
+ link.href = url;
352
+ link.download = `RomanticLife_Video_${activeStyleName}.webm`;
353
+ document.body.appendChild(link);
354
+ link.click();
355
+ document.body.removeChild(link);
356
+ alert("Video saved! You can import this into CapCut or TikTok for editing.");
357
+ }
358
+ } catch (e) {
359
+ console.error("Video gen failed", e);
360
+ alert("Video generation failed.");
361
+ } finally {
362
+ setIsSingleVideoGenerating(false);
363
+ }
364
+ };
365
+
366
+ const handleDownloadAll = async () => {
367
+ if (Object.keys(results).length === 0) return;
368
+ setIsZipping(true);
369
+
370
+ try {
371
+ const zip = new JSZip();
372
+ const imgFolder = zip.folder("RomanticLife_Photos");
373
+
374
+ // Filter: If selectedIds has items, use only those. Else use all results.
375
+ // Explicitly typing res to GeneratedResult to fix type error
376
+ const values = Object.values(results) as GeneratedResult[];
377
+ const targetResults: GeneratedResult[] = selectedIds.size > 0
378
+ ? values.filter((res) => selectedIds.has(res.styleId))
379
+ : values;
380
+
381
+ let count = 0;
382
+ targetResults.forEach((res: GeneratedResult) => {
383
+ if (res.imageUrl.startsWith('data:image')) {
384
+ const base64Data = res.imageUrl.split(',')[1];
385
+ const style = STYLES.find(s => s.id === res.styleId);
386
+ const styleName = style ? ((t.styles as any)[style.id] || style.name) : res.styleId;
387
+ const safeName = styleName.replace(/[^a-z0-9\u4e00-\u9fa5]/gi, '_');
388
+ imgFolder?.file(`${safeName}_${res.timestamp}.png`, base64Data, { base64: true });
389
+ count++;
390
+ }
391
+ });
392
+ if (count === 0) {
393
+ alert("No valid images to zip.");
394
+ setIsZipping(false);
395
+ return;
396
+ }
397
+ const content = await zip.generateAsync({ type: "blob" });
398
+ const url = URL.createObjectURL(content);
399
+ const link = document.createElement('a');
400
+ const dateStr = new Date().toISOString().slice(0,10);
401
+ link.href = url;
402
+ link.download = `RomanticLife_Album_${dateStr}.zip`;
403
+ document.body.appendChild(link);
404
+ link.click();
405
+ document.body.removeChild(link);
406
+ URL.revokeObjectURL(url);
407
+ } catch (error) {
408
+ console.error("Zip failed", error);
409
+ alert("Failed to package images.");
410
+ } finally {
411
+ setIsZipping(false);
412
+ }
413
+ };
414
+
415
+ if (!activeResult || !activeStyle) return null;
416
+
417
+ return (
418
+ <div className="flex flex-col h-full gap-6">
419
+ {/* Video Settings Modal */}
420
+ {showVideoSettings && (
421
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
422
+ <div className="bg-white rounded-2xl p-0 max-w-sm w-full shadow-2xl overflow-hidden flex flex-col max-h-[85vh]">
423
+ {/* Header */}
424
+ <div className="p-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
425
+ <h3 className="font-bold text-lg font-serif text-gray-800">{t.vidSettings}</h3>
426
+ <button
427
+ onClick={() => setShowVideoSettings(false)}
428
+ className="p-1 hover:bg-gray-200 rounded-full transition-colors"
429
+ aria-label="Close"
430
+ >
431
+ <X className="w-5 h-5 text-gray-500" />
432
+ </button>
433
+ </div>
434
+
435
+ {/* Scrollable Content */}
436
+ <div className="overflow-y-auto p-4 space-y-3 custom-scrollbar">
437
+ {/* Composition Section */}
438
+ <div className="border border-gray-200 rounded-xl overflow-hidden">
439
+ <button
440
+ onClick={() => setOpenSection(openSection === 'composition' ? null : 'composition')}
441
+ className="w-full flex justify-between items-center p-3 bg-gray-50 hover:bg-gray-100 transition-colors text-sm font-bold text-gray-700"
442
+ aria-expanded={openSection === 'composition'}
443
+ >
444
+ <span className="flex items-center gap-2"><Grid className="w-4 h-4 text-rose-500" /> {t.sectComposition || 'Composition'}</span>
445
+ {openSection === 'composition' ? <ChevronUp className="w-4 h-4 text-gray-400" /> : <ChevronDown className="w-4 h-4 text-gray-400" />}
446
+ </button>
447
+ {openSection === 'composition' && (
448
+ <div className="p-4 bg-white space-y-4 animate-fade-in">
449
+ <div>
450
+ <label className="text-xs font-bold text-gray-500 mb-1.5 block">Template</label>
451
+ <div className="relative">
452
+ <select
453
+ value={videoConfig.template}
454
+ onChange={e => setVideoConfig({...videoConfig, template: e.target.value as any})}
455
+ className="w-full p-2.5 pl-3 pr-8 bg-gray-50 border border-gray-200 rounded-lg text-sm appearance-none outline-none focus:border-rose-500 focus:ring-1 focus:ring-rose-200"
456
+ >
457
+ <option value="default">Default</option>
458
+ <option value="flash">Flash Cut (Fast)</option>
459
+ <option value="slow">Slow Motion</option>
460
+ </select>
461
+ <ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none" />
462
+ </div>
463
+ </div>
464
+ <div className="grid grid-cols-2 gap-3">
465
+ <div>
466
+ <label className="text-xs font-bold text-gray-500 mb-1.5 block">{t.vidFormat}</label>
467
+ <div className="relative">
468
+ <select
469
+ value={videoConfig.format}
470
+ onChange={e => setVideoConfig({...videoConfig, format: e.target.value as any})}
471
+ className="w-full p-2.5 pl-3 pr-8 bg-gray-50 border border-gray-200 rounded-lg text-sm appearance-none outline-none focus:border-rose-500 focus:ring-1 focus:ring-rose-200"
472
+ >
473
+ <option value="mp4">MP4</option>
474
+ <option value="webm">WebM</option>
475
+ </select>
476
+ <ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none" />
477
+ </div>
478
+ </div>
479
+ <div>
480
+ <label className="text-xs font-bold text-gray-500 mb-1.5 block">{t.vidResolution}</label>
481
+ <div className="relative">
482
+ <select
483
+ value={videoConfig.resolution}
484
+ onChange={e => setVideoConfig({...videoConfig, resolution: e.target.value as any})}
485
+ className="w-full p-2.5 pl-3 pr-8 bg-gray-50 border border-gray-200 rounded-lg text-sm appearance-none outline-none focus:border-rose-500 focus:ring-1 focus:ring-rose-200"
486
+ >
487
+ <option value="1080p">1080p</option>
488
+ <option value="720p">720p</option>
489
+ </select>
490
+ <ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none" />
491
+ </div>
492
+ </div>
493
+ </div>
494
+ </div>
495
+ )}
496
+ </div>
497
+
498
+ {/* Effects Section */}
499
+ <div className="border border-gray-200 rounded-xl overflow-hidden">
500
+ <button
501
+ onClick={() => setOpenSection(openSection === 'effects' ? null : 'effects')}
502
+ className="w-full flex justify-between items-center p-3 bg-gray-50 hover:bg-gray-100 transition-colors text-sm font-bold text-gray-700"
503
+ aria-expanded={openSection === 'effects'}
504
+ >
505
+ <span className="flex items-center gap-2"><Sparkles className="w-4 h-4 text-purple-500" /> {t.sectEffects || 'Effects'}</span>
506
+ {openSection === 'effects' ? <ChevronUp className="w-4 h-4 text-gray-400" /> : <ChevronDown className="w-4 h-4 text-gray-400" />}
507
+ </button>
508
+ {openSection === 'effects' && (
509
+ <div className="p-4 bg-white space-y-4 animate-fade-in">
510
+ <div>
511
+ <label className="text-xs font-bold text-gray-500 mb-1.5 block">{t.transEffect}</label>
512
+ <div className="relative">
513
+ <select
514
+ value={videoConfig.transition}
515
+ onChange={e => setVideoConfig({...videoConfig, transition: e.target.value as any})}
516
+ className="w-full p-2.5 pl-3 pr-8 bg-gray-50 border border-gray-200 rounded-lg text-sm appearance-none outline-none focus:border-rose-500 focus:ring-1 focus:ring-rose-200"
517
+ >
518
+ <option value="fade">Fade</option>
519
+ <option value="slide">Slide</option>
520
+ <option value="none">None</option>
521
+ </select>
522
+ <ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none" />
523
+ </div>
524
+ </div>
525
+ </div>
526
+ )}
527
+ </div>
528
+
529
+ {/* Audio Section */}
530
+ <div className="border border-gray-200 rounded-xl overflow-hidden">
531
+ <button
532
+ onClick={() => setOpenSection(openSection === 'audio' ? null : 'audio')}
533
+ className="w-full flex justify-between items-center p-3 bg-gray-50 hover:bg-gray-100 transition-colors text-sm font-bold text-gray-700"
534
+ aria-expanded={openSection === 'audio'}
535
+ >
536
+ <span className="flex items-center gap-2"><Music className="w-4 h-4 text-blue-500" /> {t.sectAudio || 'Audio'}</span>
537
+ {openSection === 'audio' ? <ChevronUp className="w-4 h-4 text-gray-400" /> : <ChevronDown className="w-4 h-4 text-gray-400" />}
538
+ </button>
539
+ {openSection === 'audio' && (
540
+ <div className="p-4 bg-white space-y-4 animate-fade-in">
541
+ <div>
542
+ <label className="text-xs font-bold text-gray-500 mb-1.5 block">{t.videoMusic}</label>
543
+ <div className="relative">
544
+ <select
545
+ value={videoConfig.musicTrack}
546
+ onChange={e => setVideoConfig({...videoConfig, musicTrack: e.target.value})}
547
+ className="w-full p-2.5 pl-3 pr-8 bg-gray-50 border border-gray-200 rounded-lg text-sm appearance-none outline-none focus:border-rose-500 focus:ring-1 focus:ring-rose-200"
548
+ >
549
+ {MUSIC_OPTIONS.map(m => (
550
+ <option key={m.id} value={m.id}>{(t as any)[m.labelKey] || m.id}</option>
551
+ ))}
552
+ </select>
553
+ <ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none" />
554
+ </div>
555
+ </div>
556
+ </div>
557
+ )}
558
+ </div>
559
+ </div>
560
+
561
+ {/* Footer */}
562
+ <div className="p-4 border-t border-gray-100 bg-gray-50/50">
563
+ <button
564
+ onClick={() => setShowVideoSettings(false)}
565
+ className="w-full py-3 bg-gray-900 text-white rounded-xl font-bold hover:bg-black transition-all shadow-lg shadow-gray-200 flex items-center justify-center gap-2"
566
+ >
567
+ <CheckCircle className="w-4 h-4" />
568
+ Done
569
+ </button>
570
+ </div>
571
+ </div>
572
+ </div>
573
+ )}
574
+
575
+ {/* Main Preview Area */}
576
+ <div className="flex-1 bg-gray-50 rounded-3xl border border-gray-200 overflow-hidden relative shadow-inner min-h-[500px] group">
577
+ {/* Live Effects Layer */}
578
+ <div className={`absolute inset-0 pointer-events-none z-10 ${
579
+ liveEffect === 'breath' ? 'effect-breath' :
580
+ liveEffect === 'glitch' ? 'effect-glitch' :
581
+ liveEffect === 'particles' ? 'effect-particles' : ''
582
+ }`} aria-hidden="true"></div>
583
+
584
+ {/* Control Bar */}
585
+ <div className="absolute top-0 left-0 right-0 p-4 flex justify-between items-start z-40 pointer-events-none">
586
+ <div className="bg-white/90 backdrop-blur-sm px-4 py-2 rounded-full shadow-sm border border-gray-100 pointer-events-auto">
587
+ <span className="font-serif font-bold text-gray-800">{activeStyleName}</span>
588
+ </div>
589
+
590
+ <div className="flex gap-2 pointer-events-auto">
591
+ <button onClick={() => setLiveEffect(liveEffect === 'none' ? 'breath' : 'none')} className="p-2 bg-white rounded-full shadow-sm" aria-label="Toggle Live Effect"><Zap className="w-4 h-4" aria-hidden="true" /></button>
592
+ <button
593
+ onClick={() => setCompareMode(!compareMode)}
594
+ className={`p-2 rounded-full shadow-sm transition-colors ${compareMode ? 'bg-blue-600 text-white' : 'bg-white hover:bg-gray-100'}`}
595
+ aria-label={compareMode ? "Exit Compare Mode" : "Enter Compare Mode"}
596
+ >
597
+ <SplitSquareHorizontal className="w-4 h-4" aria-hidden="true" />
598
+ </button>
599
+ <button onClick={onReset} className="p-2 bg-gray-900 text-white rounded-full shadow-lg hover:bg-black" aria-label="Reset and Close"><X className="w-4 h-4" aria-hidden="true" /></button>
600
+ </div>
601
+ </div>
602
+
603
+ {/* Canvas Area with Keyboard Support */}
604
+ <div
605
+ className="relative w-full h-full cursor-col-resize select-none flex items-center justify-center bg-gray-200 outline-none focus:ring-2 focus:ring-rose-500 focus:ring-inset rounded-lg overflow-hidden"
606
+ ref={containerRef}
607
+ onMouseDown={handleMouseDown}
608
+ onTouchStart={handleMouseDown}
609
+ role={compareMode ? "slider" : "img"}
610
+ aria-label={compareMode ? "Comparison Slider. Use Left/Right arrows to adjust." : `Generated image for ${activeStyleName}`}
611
+ aria-valuenow={compareMode ? sliderPosition : undefined}
612
+ aria-valuemin={compareMode ? 0 : undefined}
613
+ aria-valuemax={compareMode ? 100 : undefined}
614
+ tabIndex={0}
615
+ onKeyDown={handleKeyDown}
616
+ >
617
+ {/* Background Image (Active Style / Right Side) */}
618
+ <img src={activeResult.imageUrl} className="absolute inset-0 w-full h-full object-contain p-2 sm:p-8 transition-opacity duration-300" alt="After" />
619
+
620
+ {compareMode && (
621
+ <>
622
+ {/* Overlay Image (Original or Compare Style / Left Side) - Clipped */}
623
+ <img
624
+ src={compareImageUrl}
625
+ className="absolute inset-0 w-full h-full object-contain p-2 sm:p-8 transition-[clip-path] duration-75 ease-linear bg-gray-200"
626
+ alt="Before"
627
+ style={{ clipPath: `inset(0 ${100 - sliderPosition}% 0 0)` }}
628
+ />
629
+
630
+ {/* Slider Handle */}
631
+ <div
632
+ className="absolute top-0 bottom-0 w-0.5 bg-white cursor-ew-resize z-30 filter drop-shadow-[0_0_5px_rgba(0,0,0,0.5)]"
633
+ style={{ left: `${sliderPosition}%` }}
634
+ >
635
+ <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-8 h-8 bg-white rounded-full shadow-lg flex items-center justify-center text-rose-500 hover:scale-110 transition-transform group-active:scale-110">
636
+ <ArrowLeftRight className="w-4 h-4" />
637
+ </div>
638
+ </div>
639
+
640
+ {/* Floating Labels */}
641
+ <div className="absolute top-16 left-4 z-20 bg-black/60 text-white px-3 py-1 rounded-full text-xs font-bold backdrop-blur-md border border-white/10 shadow-lg pointer-events-none animate-fade-in">
642
+ {compareLabel}
643
+ </div>
644
+ <div className="absolute top-16 right-4 z-20 bg-rose-500/90 text-white px-3 py-1 rounded-full text-xs font-bold backdrop-blur-md border border-white/10 shadow-lg pointer-events-none animate-fade-in">
645
+ {activeStyleName}
646
+ </div>
647
+ </>
648
+ )}
649
+ </div>
650
+ </div>
651
+
652
+ {/* Action Bar */}
653
+ <div className="flex justify-between items-center bg-white p-4 rounded-2xl shadow-sm border border-gray-100">
654
+ <button onClick={() => setIncludeWatermark(!includeWatermark)} className={`text-xs font-bold ${includeWatermark ? 'text-rose-600' : 'text-gray-400'}`}>
655
+ Watermark: {includeWatermark ? 'ON' : 'OFF'}
656
+ </button>
657
+ <div className="flex gap-2">
658
+ <button onClick={onShareClick} className="p-2 bg-gray-100 rounded-full hover:bg-gray-200 text-gray-700" aria-label="Share">
659
+ <Share2 className="w-4 h-4" aria-hidden="true" />
660
+ </button>
661
+ <button onClick={() => setShowVideoSettings(true)} className="p-2 bg-gray-100 rounded-full hover:bg-gray-200" aria-label="Video Settings"><Settings className="w-4 h-4" aria-hidden="true" /></button>
662
+
663
+ <button
664
+ onClick={handleDownloadAll}
665
+ disabled={isZipping}
666
+ className="px-4 py-2 bg-gray-800 text-white rounded-xl font-bold text-xs flex items-center gap-2 hover:bg-black transition-colors"
667
+ >
668
+ {isZipping ? <Loader2 className="w-4 h-4 animate-spin" /> : <Download className="w-4 h-4" />}
669
+ {isZipping ? t.zipLoading : (selectedIds.size > 0 ? `Download (${selectedIds.size})` : t.zipBtn)}
670
+ </button>
671
+
672
+ <button onClick={handleGenerateSingleVideo} disabled={isSingleVideoGenerating} className="px-4 py-2 bg-rose-600 text-white rounded-xl font-bold text-xs flex items-center gap-2">
673
+ {isSingleVideoGenerating ? <Loader2 className="w-4 h-4 animate-spin" aria-hidden="true" /> : <PlayCircle className="w-4 h-4" aria-hidden="true" />}
674
+ {t.videoBtn}
675
+ </button>
676
+ </div>
677
+ </div>
678
+
679
+ {/* Thumbnail Grid */}
680
+ <div className="flex flex-col gap-2">
681
+ {/* Grid Toolbar */}
682
+ <div className="flex items-center justify-between px-2">
683
+ <button
684
+ onClick={handleSelectAll}
685
+ className="flex items-center gap-1.5 text-xs font-bold text-gray-500 hover:text-rose-500 transition-colors"
686
+ >
687
+ {Object.keys(results).length > 0 && selectedIds.size === Object.keys(results).length ? (
688
+ <CheckSquare className="w-4 h-4 text-rose-500" />
689
+ ) : (
690
+ <Square className="w-4 h-4" />
691
+ )}
692
+ Select All
693
+ </button>
694
+ <span className="text-xs text-gray-400 font-mono">
695
+ {selectedIds.size} selected
696
+ </span>
697
+ </div>
698
+
699
+ <div className="grid grid-cols-5 gap-2 overflow-x-auto pb-2" role="list" aria-label="Generated Results">
700
+ {/* Original Image Button (Visible only in Compare Mode) */}
701
+ {compareMode && (
702
+ <button
703
+ onClick={() => setCompareId(null)}
704
+ aria-pressed={compareId === null}
705
+ className={`aspect-square rounded-lg overflow-hidden cursor-pointer border-2 relative ${compareId === null ? 'border-blue-500 ring-2 ring-blue-200' : 'border-gray-200'}`}
706
+ >
707
+ <img src={originalImages[0]} className="w-full h-full object-cover" alt="Original" />
708
+ <span className="absolute bottom-0 left-0 w-full bg-black/60 text-white text-[9px] font-bold text-center py-0.5 backdrop-blur-sm">{t.original}</span>
709
+ {compareId === null && <div className="absolute top-1 left-1 w-2 h-2 rounded-full bg-blue-500 ring-1 ring-white"></div>}
710
+ </button>
711
+ )}
712
+
713
+ {STYLES.filter(s => results[s.id]).map(style => {
714
+ const isSelectedView = activeId === style.id;
715
+ const isCompare = compareId === style.id;
716
+ const isChecked = selectedIds.has(style.id);
717
+
718
+ let borderClass = 'border-transparent';
719
+ if (compareMode) {
720
+ if (isSelectedView) borderClass = 'border-rose-500'; // Right Side
721
+ if (isCompare) borderClass = 'border-blue-500 ring-2 ring-blue-200'; // Left Side
722
+ } else {
723
+ if (isSelectedView) borderClass = 'border-rose-500';
724
+ }
725
+
726
+ return (
727
+ <button
728
+ key={style.id}
729
+ onClick={() => {
730
+ if(compareMode) {
731
+ if (activeId !== style.id) setCompareId(style.id);
732
+ } else {
733
+ setActiveId(style.id);
734
+ }
735
+ }}
736
+ aria-pressed={isSelectedView || isCompare}
737
+ aria-label={`View ${style.name} result`}
738
+ className={`aspect-square rounded-lg overflow-hidden cursor-pointer border-2 relative transition-all group ${borderClass}`}
739
+ >
740
+ <img src={results[style.id].imageUrl} className="w-full h-full object-cover" alt="" />
741
+
742
+ {/* Selection Checkbox Overlay */}
743
+ <div
744
+ className="absolute top-1 left-1 z-20 cursor-pointer"
745
+ onClick={(e) => toggleSelection(style.id, e)}
746
+ >
747
+ <div className={`w-5 h-5 rounded border shadow-sm flex items-center justify-center transition-colors ${isChecked ? 'bg-rose-500 border-rose-600' : 'bg-white/80 border-gray-300 hover:bg-white'}`}>
748
+ {isChecked && <Check className="w-3.5 h-3.5 text-white" />}
749
+ </div>
750
+ </div>
751
+
752
+ {/* Visual Indicators for Compare Mode */}
753
+ {compareMode && isCompare && <div className="absolute top-1 right-1 w-2 h-2 rounded-full bg-blue-500 ring-1 ring-white"></div>}
754
+ {compareMode && isSelectedView && <div className="absolute top-1 right-1 w-2 h-2 rounded-full bg-rose-500 ring-1 ring-white"></div>}
755
+ </button>
756
+ );
757
+ })}
758
+ </div>
759
+ </div>
760
+ </div>
761
+ );
762
+ };
components/ShareModal.tsx ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React from 'react';
3
+ import { X, Copy, Share2, MessageCircle, Facebook, Twitter, Check } from 'lucide-react';
4
+ import { Language } from '../types';
5
+ import { TRANSLATIONS } from '../constants/translations';
6
+
7
+ interface ShareModalProps {
8
+ isVisible: boolean;
9
+ onClose: () => void;
10
+ language: Language;
11
+ config?: any; // Share config from admin
12
+ showToast: (msg: string) => void;
13
+ onShareSuccess: () => void;
14
+ }
15
+
16
+ export const ShareModal: React.FC<ShareModalProps> = ({
17
+ isVisible, onClose, language, config, showToast, onShareSuccess
18
+ }) => {
19
+ const t = TRANSLATIONS[language];
20
+ const [copied, setCopied] = React.useState(false);
21
+
22
+ if (!isVisible) return null;
23
+
24
+ const shareTitle = config?.shareTitle || "Check out Romantic Life AI Studio!";
25
+ const shareUrl = window.location.href;
26
+ const fullShareText = `${shareTitle} ${shareUrl}`;
27
+
28
+ const handleCopyLink = async () => {
29
+ try {
30
+ if (navigator.clipboard && navigator.clipboard.writeText) {
31
+ await navigator.clipboard.writeText(fullShareText);
32
+ setCopied(true);
33
+ showToast(t.toastCopy);
34
+ onShareSuccess();
35
+ setTimeout(() => {
36
+ setCopied(false);
37
+ onClose();
38
+ }, 1500);
39
+ } else {
40
+ // Fallback for older browsers
41
+ const textArea = document.createElement("textarea");
42
+ textArea.value = fullShareText;
43
+ document.body.appendChild(textArea);
44
+ textArea.select();
45
+ document.execCommand('copy');
46
+ document.body.removeChild(textArea);
47
+ setCopied(true);
48
+ showToast(t.toastCopy);
49
+ onShareSuccess();
50
+ setTimeout(() => onClose(), 1500);
51
+ }
52
+ } catch (err) {
53
+ console.error('Failed to copy: ', err);
54
+ alert("Failed to copy link. Please copy manually from address bar.");
55
+ }
56
+ };
57
+
58
+ const handleWeChat = () => {
59
+ // In a real PWA, this might trigger a native share if available,
60
+ // or show a QR code modal. For now, we simulate the "Shared" callback.
61
+ if (navigator.share) {
62
+ navigator.share({
63
+ title: shareTitle,
64
+ text: shareTitle,
65
+ url: shareUrl,
66
+ }).then(() => {
67
+ onShareSuccess();
68
+ onClose();
69
+ }).catch((error) => console.log('Error sharing', error));
70
+ } else {
71
+ alert("Please screenshot this page and share to WeChat Moments.");
72
+ onShareSuccess(); // Assume they did it for UX flow
73
+ setTimeout(onClose, 500);
74
+ }
75
+ };
76
+
77
+ return (
78
+ <div className="fixed inset-0 z-[130] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in">
79
+ <div className="bg-white rounded-2xl shadow-2xl w-full max-w-sm overflow-hidden relative">
80
+ <div className="bg-gray-50 p-4 border-b border-gray-100 flex justify-between items-center">
81
+ <h3 className="font-bold text-gray-800 flex items-center gap-2">
82
+ <Share2 className="w-4 h-4 text-rose-500" /> Share to Earn
83
+ </h3>
84
+ <button onClick={onClose} className="p-1 hover:bg-gray-200 rounded-full">
85
+ <X className="w-5 h-5 text-gray-500" />
86
+ </button>
87
+ </div>
88
+
89
+ <div className="p-6 grid grid-cols-2 gap-4">
90
+ <button
91
+ onClick={handleWeChat}
92
+ className="flex flex-col items-center justify-center gap-2 p-4 rounded-xl bg-green-50 text-green-700 hover:bg-green-100 transition-colors group"
93
+ >
94
+ <div className="w-10 h-10 bg-green-500 text-white rounded-full flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform">
95
+ <MessageCircle className="w-6 h-6" />
96
+ </div>
97
+ <span className="text-xs font-bold">WeChat</span>
98
+ </button>
99
+
100
+ <button
101
+ onClick={handleCopyLink}
102
+ className={`flex flex-col items-center justify-center gap-2 p-4 rounded-xl transition-colors group ${copied ? 'bg-gray-800 text-white' : 'bg-gray-50 text-gray-700 hover:bg-gray-100'}`}
103
+ >
104
+ <div className={`w-10 h-10 rounded-full flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform ${copied ? 'bg-green-500 text-white' : 'bg-gray-700 text-white'}`}>
105
+ {copied ? <Check className="w-6 h-6" /> : <Copy className="w-5 h-5" />}
106
+ </div>
107
+ <span className="text-xs font-bold">{copied ? "Copied!" : "Copy Link"}</span>
108
+ </button>
109
+
110
+ <button
111
+ className="flex flex-col items-center justify-center gap-2 p-4 rounded-xl bg-blue-50 text-blue-700 hover:bg-blue-100 transition-colors opacity-60 hover:opacity-100"
112
+ onClick={() => {
113
+ window.open(`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}`, '_blank');
114
+ onShareSuccess();
115
+ }}
116
+ >
117
+ <div className="w-10 h-10 bg-blue-600 text-white rounded-full flex items-center justify-center shadow-lg">
118
+ <Facebook className="w-5 h-5" />
119
+ </div>
120
+ <span className="text-xs font-bold">Facebook</span>
121
+ </button>
122
+
123
+ <button
124
+ className="flex flex-col items-center justify-center gap-2 p-4 rounded-xl bg-sky-50 text-sky-700 hover:bg-sky-100 transition-colors opacity-60 hover:opacity-100"
125
+ onClick={() => {
126
+ window.open(`https://twitter.com/intent/tweet?text=${encodeURIComponent(shareTitle)}&url=${encodeURIComponent(shareUrl)}`, '_blank');
127
+ onShareSuccess();
128
+ }}
129
+ >
130
+ <div className="w-10 h-10 bg-sky-500 text-white rounded-full flex items-center justify-center shadow-lg">
131
+ <Twitter className="w-5 h-5" />
132
+ </div>
133
+ <span className="text-xs font-bold">Twitter</span>
134
+ </button>
135
+ </div>
136
+
137
+ <div className="p-4 bg-gray-50 text-center">
138
+ <p className="text-xs text-gray-500">Share now to earn +10 Points instantly!</p>
139
+ </div>
140
+ </div>
141
+ </div>
142
+ );
143
+ };
components/SlashModal.tsx ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { X, Zap, Timer } from 'lucide-react';
3
+ import { Language, WeddingStyle, AdminConfig, UserAccount } from '../types';
4
+ import { TRANSLATIONS } from '../constants/translations';
5
+
6
+ interface SlashModalProps {
7
+ isVisible: boolean;
8
+ onClose: () => void;
9
+ language: Language;
10
+ style: WeddingStyle | null;
11
+ onUnlock: () => void;
12
+ adminConfig: AdminConfig;
13
+ user?: UserAccount;
14
+ onUpdateUser: (progress: Record<string, number>) => void;
15
+ onOpenShare: () => void;
16
+ }
17
+
18
+ export const SlashModal: React.FC<SlashModalProps> = ({
19
+ isVisible, onClose, language, style, onUnlock, adminConfig, user, onUpdateUser, onOpenShare
20
+ }) => {
21
+ const t = TRANSLATIONS[language];
22
+ const [progress, setProgress] = useState(98);
23
+
24
+ useEffect(() => {
25
+ if (isVisible && style) {
26
+ if (user?.slashProgress && user.slashProgress[style.id]) {
27
+ setProgress(user.slashProgress[style.id]);
28
+ } else {
29
+ setProgress(98);
30
+ }
31
+ }
32
+ }, [isVisible, style, user]);
33
+
34
+ if (!isVisible || !style) return null;
35
+
36
+ const styleName = (t.styles as any)[style.id] || style.name;
37
+
38
+ const handleInvite = () => {
39
+ onOpenShare();
40
+
41
+ // Update progress logic
42
+ const remaining = 100 - progress;
43
+ // Slash logic: Cut 50-80% of remaining
44
+ const cutPercent = 0.5 + Math.random() * 0.3;
45
+ let newProgress = progress + (remaining * cutPercent);
46
+
47
+ // If very close, complete it (e.g. > 99.5)
48
+ if (newProgress > 99.5) {
49
+ newProgress = 100;
50
+ }
51
+
52
+ setProgress(newProgress);
53
+
54
+ if (user) {
55
+ const newMap = { ...(user.slashProgress || {}), [style.id]: newProgress };
56
+ onUpdateUser(newMap);
57
+ }
58
+
59
+ if (newProgress >= 100) {
60
+ onUnlock();
61
+ }
62
+
63
+ onClose();
64
+ };
65
+
66
+ return (
67
+ <div className="fixed inset-0 z-[110] flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm animate-fade-in">
68
+ <div className="bg-gradient-to-b from-orange-500 to-red-600 rounded-2xl shadow-2xl w-full max-w-sm overflow-hidden relative text-white">
69
+ <button onClick={onClose} className="absolute top-2 right-2 p-1.5 bg-black/20 rounded-full hover:bg-black/40 z-20">
70
+ <X className="w-4 h-4" />
71
+ </button>
72
+
73
+ <div className="p-6 text-center">
74
+ <div className="inline-block px-3 py-1 bg-black/30 rounded-full text-xs font-bold mb-4 flex items-center gap-1 mx-auto border border-white/20">
75
+ <Timer className="w-3 h-3" /> Ends in 23:59:10
76
+ </div>
77
+
78
+ <h2 className="text-2xl font-bold italic drop-shadow-md">{t.pddSlashTitle}</h2>
79
+ <p className="text-orange-100 text-sm mt-1">{t.pddSlashDesc}</p>
80
+
81
+ {/* Product Card */}
82
+ <div className="bg-white text-gray-900 rounded-xl p-3 mt-6 flex items-center gap-3 shadow-lg">
83
+ <div className="w-16 h-16 bg-gray-200 rounded-lg shrink-0 overflow-hidden relative">
84
+ <div className={`w-full h-full ${style.coverColor} opacity-50`}></div>
85
+ <div className="absolute inset-0 flex items-center justify-center">
86
+ <span className="text-xs font-bold text-gray-500">IMG</span>
87
+ </div>
88
+ </div>
89
+ <div className="text-left flex-1">
90
+ <p className="font-bold text-sm truncate">{styleName}</p>
91
+ <p className="text-xs text-gray-500">VIP Premium Collection</p>
92
+ <p className="text-red-500 font-bold text-sm mt-1">¥0.00 <span className="text-gray-400 line-through text-xs">¥199</span></p>
93
+ </div>
94
+ </div>
95
+
96
+ {/* Progress Bar */}
97
+ <div className="mt-6 relative">
98
+ <div className="flex justify-between text-xs font-bold mb-1">
99
+ <span className="text-yellow-200">{t.pddSlashProgress}</span>
100
+ <span>{progress.toFixed(2)}%</span>
101
+ </div>
102
+ <div className="h-4 bg-black/30 rounded-full overflow-hidden border border-white/20">
103
+ <div className="h-full bg-gradient-to-r from-yellow-300 to-yellow-500 relative" style={{ width: `${progress}%` }}>
104
+ <div className="absolute top-0 right-0 h-full w-2 bg-white/50 animate-pulse"></div>
105
+ </div>
106
+ </div>
107
+ <div className="absolute -right-2 -top-2 bg-red-100 text-red-600 text-[10px] font-bold px-1.5 rounded-full border border-red-500 shadow-sm animate-bounce">
108
+ Only {(100 - progress).toFixed(2)}% left!
109
+ </div>
110
+ </div>
111
+
112
+ <button
113
+ onClick={handleInvite}
114
+ className="w-full py-3 bg-yellow-400 hover:bg-yellow-300 text-red-700 font-extrabold text-lg rounded-full mt-6 shadow-xl shadow-orange-700/50 flex items-center justify-center gap-2 transform active:scale-95 transition-all"
115
+ >
116
+ <Zap className="w-5 h-5 fill-current" /> {t.pddSlashCta}
117
+ </button>
118
+ </div>
119
+
120
+ {/* Social Proof List */}
121
+ <div className="bg-white/10 p-4 border-t border-white/10">
122
+ <p className="text-xs text-white/60 mb-2 text-center">Friends who helped</p>
123
+ <div className="flex justify-center -space-x-2">
124
+ {[1,2,3].map(i => (
125
+ <div key={i} className="w-8 h-8 rounded-full bg-gray-200 border-2 border-orange-500"></div>
126
+ ))}
127
+ <div className="w-8 h-8 rounded-full bg-gray-800 border-2 border-orange-500 flex items-center justify-center text-[10px] text-white">
128
+ +99
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ </div>
134
+ );
135
+ };
components/StyleSelector.tsx ADDED
@@ -0,0 +1,1318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useRef, useState, useMemo, useEffect } from 'react';
2
+ import { WeddingStyle, Language, Resolution } from '../types';
3
+ import { TRANSLATIONS } from '../constants/translations';
4
+ import { useUserStore } from '../store';
5
+ import {
6
+ Heart, Crown, Leaf, Zap, Clock, Sparkles,
7
+ Camera, Feather, Film, Palette, Coffee,
8
+ Sun, Music, BookOpen, Star, Aperture,
9
+ Smile, PenTool, Layout, Image as ImageIcon, Upload,
10
+ Flower, Gem, Eye, Cog, Rocket, Cpu,
11
+ Castle, Anchor, Snowflake, Droplets, Warehouse,
12
+ Trees, Tv, Brush, Cloud, Layers, Unlink, ScanFace,
13
+ Lock, Dice5, Users, ArrowRight, Search, Briefcase, User, Watch, GraduationCap, Baby, Grid,
14
+ Fish, Flame, Moon, Hourglass, Landmark, Sword, Monitor, Shirt, Box, Home, Book, Circle, Globe, Activity, Check, Maximize, Loader2
15
+ } from 'lucide-react';
16
+
17
+ interface StyleSelectorProps {
18
+ selectedStyle: WeddingStyle | null;
19
+ onSelect: (style: WeddingStyle) => void;
20
+ onCustomSelect: (image: string) => void;
21
+ onLuckySelect: () => void;
22
+ onSlashClick: (style: WeddingStyle) => void;
23
+ disabled: boolean;
24
+ language: Language;
25
+ recommendedStyleIds?: string[];
26
+ isVipUnlocked?: boolean;
27
+ isLoading?: boolean;
28
+ resolution: Resolution;
29
+ onResolutionChange: (res: Resolution) => void;
30
+ }
31
+
32
+ // Simple icon wrapper
33
+ const BoxIcon = ({ className }: { className?: string }) => (
34
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className={className} aria-hidden="true">
35
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
36
+ </svg>
37
+ );
38
+
39
+ // Skeleton for loading state
40
+ const StyleCardSkeleton = () => (
41
+ <li className="list-none aspect-[3/4] rounded-xl overflow-hidden bg-gray-100 relative animate-pulse border border-gray-100">
42
+ <div className="absolute inset-0 bg-gray-200/50"></div>
43
+ <div className="absolute bottom-0 left-0 right-0 p-3 space-y-2 bg-gradient-to-t from-gray-200 to-transparent pt-10">
44
+ <div className="flex justify-between items-end">
45
+ <div className="space-y-1.5 w-full">
46
+ <div className="h-4 bg-gray-300 rounded w-2/3"></div>
47
+ <div className="h-3 bg-gray-300 rounded w-1/2"></div>
48
+ </div>
49
+ <div className="w-8 h-8 rounded-full bg-gray-300 shrink-0"></div>
50
+ </div>
51
+ </div>
52
+ </li>
53
+ );
54
+
55
+ // Lazy Background Component with Pulse Effect
56
+ const LazyStyleBackground = ({ src, colorClass }: { src?: string, colorClass: string }) => {
57
+ const [loaded, setLoaded] = useState(false);
58
+ const [error, setError] = useState(false);
59
+
60
+ // If no image or error, return just the color background (original behavior)
61
+ if (!src || error) {
62
+ return <div className={`absolute inset-0 opacity-40 ${colorClass} transition-opacity group-hover:opacity-60`} aria-hidden="true" />;
63
+ }
64
+
65
+ return (
66
+ <>
67
+ {/* Skeleton / Loading State for individual image */}
68
+ {!loaded && (
69
+ <div className={`absolute inset-0 ${colorClass} flex items-center justify-center z-10`} aria-hidden="true">
70
+ <div className="absolute inset-0 bg-white/20 animate-pulse" />
71
+ <Loader2 className="w-6 h-6 text-gray-400/50 animate-spin" />
72
+ </div>
73
+ )}
74
+
75
+ {/* Lazy Loaded Image */}
76
+ <img
77
+ src={src}
78
+ alt=""
79
+ loading="lazy"
80
+ onLoad={() => setLoaded(true)}
81
+ onError={() => setError(true)}
82
+ className={`absolute inset-0 w-full h-full object-cover transition-all duration-500 ${loaded ? 'opacity-30 group-hover:opacity-50 scale-100' : 'opacity-0 scale-105'}`}
83
+ aria-hidden="true"
84
+ />
85
+ </>
86
+ );
87
+ };
88
+
89
+ // We keep the logic constants here, but names will be dynamic based on language
90
+ export const STYLES: WeddingStyle[] = [
91
+ // 1. Classic & Western
92
+ {
93
+ id: 'korean',
94
+ name: '韩式 (Korean)', // Fallback
95
+ prompt: 'Korean wedding photography style, minimalist, clean solid background, soft studio lighting, elegant white mermaid dress, simple veil, romantic and sweet atmosphere, flawless makeup, K-drama aesthetic.',
96
+ description: 'Minimalist, sweet, and elegant.',
97
+ coverColor: 'bg-rose-50',
98
+ previewImage: 'https://images.unsplash.com/photo-1520854221256-17451cc330e7?auto=format&fit=crop&w=400&q=60',
99
+ icon: <Heart className="w-5 h-5 text-rose-400" aria-hidden="true" />,
100
+ tags: ['hot'],
101
+ isLocked: true, // Aggressive VIP Strategy: Lock the most popular style
102
+ groupCount: 523 // PDD: Fake group count
103
+ },
104
+ {
105
+ id: 'british',
106
+ name: '英伦 (British)',
107
+ prompt: 'British royal style wedding, vintage manor background, groom in morning suit, bride in lace vintage gown, elegant hat, overcast soft light, noble and aristocratic atmosphere.',
108
+ description: 'Aristocratic, vintage manor vibes.',
109
+ coverColor: 'bg-blue-50',
110
+ previewImage: 'https://images.unsplash.com/photo-1505944270255-72b8c68c6a70?auto=format&fit=crop&w=400&q=60',
111
+ icon: <Crown className="w-5 h-5 text-blue-600" aria-hidden="true" />
112
+ },
113
+ {
114
+ id: 'euro_american',
115
+ name: '欧美 (Western)',
116
+ prompt: 'Western fashion wedding style, Vogue magazine aesthetic, bold emotions, grand cathedral or outdoor lawn background, classic tuxedo and ballgown, dramatic lighting.',
117
+ description: 'Classic western grandeur and emotion.',
118
+ coverColor: 'bg-slate-50',
119
+ previewImage: 'https://images.unsplash.com/photo-1519741497674-611481863552?auto=format&fit=crop&w=400&q=60',
120
+ icon: <Star className="w-5 h-5 text-slate-600" aria-hidden="true" />
121
+ },
122
+ {
123
+ id: 'hollywood',
124
+ name: '好莱坞 (Hollywood)',
125
+ prompt: 'Classic Hollywood movie glamour, black and white, dramatic lighting, elegant 1950s gowns, tuxedo, red carpet aesthetic, high contrast film noir style.',
126
+ description: 'Glamorous 1950s film noir.',
127
+ coverColor: 'bg-gray-200',
128
+ previewImage: 'https://images.unsplash.com/photo-1537633552985-df8429e8048b?auto=format&fit=crop&w=400&q=60',
129
+ icon: <Film className="w-5 h-5 text-gray-800" aria-hidden="true" />
130
+ },
131
+
132
+ // 2. Cultural & Traditional
133
+ {
134
+ id: 'chinese',
135
+ name: '中国风 (Chinese)',
136
+ prompt: 'Modern Chinese wedding style, luxurious red embroidery, traditional elements mixed with modern aesthetics, fan, porcelain, red background, festive and grand.',
137
+ description: 'Modern red aesthetics and embroidery.',
138
+ coverColor: 'bg-red-50',
139
+ previewImage: 'https://images.unsplash.com/photo-1588667822949-a034d612df08?auto=format&fit=crop&w=400&q=60',
140
+ icon: <Leaf className="w-5 h-5 text-red-600" aria-hidden="true" />,
141
+ tags: ['hot'],
142
+ isLocked: true, // Aggressive VIP Strategy: Lock the most popular style
143
+ groupCount: 890 // PDD
144
+ },
145
+ {
146
+ id: 'gufeng',
147
+ name: '古风 (Ancient)',
148
+ prompt: 'Ancient Chinese Hanfu style, ethereal fairy tale vibe, flowing silk robes, ink painting background, bamboo forest, flute, poetic and historical atmosphere.',
149
+ description: 'Ethereal Hanfu and poetic vibes.',
150
+ coverColor: 'bg-emerald-50',
151
+ previewImage: 'https://images.unsplash.com/photo-1535189043414-47a3c49a0bed?auto=format&fit=crop&w=400&q=60',
152
+ icon: <Feather className="w-5 h-5 text-emerald-600" aria-hidden="true" />
153
+ },
154
+ {
155
+ id: 'japanese',
156
+ name: '日系 (Japanese)',
157
+ prompt: 'Japanese aesthetic, bright and airy, overexposed soft light, film grain, emotional close-up, clean streets or cherry blossoms background, natural makeup, pure and fresh.',
158
+ description: 'Bright, airy, and emotional.',
159
+ coverColor: 'bg-sky-50',
160
+ previewImage: 'https://images.unsplash.com/photo-1492596985094-52771095ad79?auto=format&fit=crop&w=400&q=60',
161
+ icon: <Sun className="w-5 h-5 text-sky-400" aria-hidden="true" />
162
+ },
163
+ {
164
+ id: 'ethnic',
165
+ name: '民族风 (Ethnic)',
166
+ prompt: 'Exotic ethnic wedding style, colorful traditional patterns, heavy silver jewelry, vibrant fabrics, wild nature background, bohemian spirit, cultural richness.',
167
+ description: 'Vibrant colors and cultural patterns.',
168
+ coverColor: 'bg-orange-50',
169
+ previewImage: 'https://images.unsplash.com/photo-1606216794074-735e91aa2c92?auto=format&fit=crop&w=400&q=60',
170
+ icon: <Music className="w-5 h-5 text-orange-500" aria-hidden="true" />
171
+ },
172
+ {
173
+ id: 'exotic',
174
+ name: '异域风 (Exotic)',
175
+ prompt: 'Exotic desert or island wedding, Persian or Moroccan influence, warm sunset lighting, golden accessories, mysterious and alluring atmosphere.',
176
+ description: 'Mysterious, warm, and unique.',
177
+ coverColor: 'bg-amber-50',
178
+ previewImage: 'https://images.unsplash.com/photo-1516815231560-8f41ec531527?auto=format&fit=crop&w=400&q=60',
179
+ icon: <Sun className="w-5 h-5 text-amber-600" aria-hidden="true" />
180
+ },
181
+
182
+ // 3. Artistic & Mood
183
+ {
184
+ id: 'cinematic',
185
+ name: '电影感 (Cinematic)',
186
+ prompt: 'Cinematic movie still, Wong Kar-wai style, moody lighting, strong shadows, emotional storytelling, color graded, wide aspect ratio composition feeling.',
187
+ description: 'Moody, storytelling movie stills.',
188
+ coverColor: 'bg-gray-100',
189
+ previewImage: 'https://images.unsplash.com/photo-1470163395405-d2b80e7450ed?auto=format&fit=crop&w=400&q=60',
190
+ icon: <Film className="w-5 h-5 text-gray-700" aria-hidden="true" />,
191
+ tags: ['recommend']
192
+ },
193
+ {
194
+ id: 'film',
195
+ name: '胶片 (Film)',
196
+ prompt: 'Vintage film photography, Kodak Portra 400 look, grain, light leaks, nostalgic color palette, candid moment, timeless texture.',
197
+ description: 'Nostalgic grain and analog texture.',
198
+ coverColor: 'bg-yellow-50',
199
+ previewImage: 'https://images.unsplash.com/photo-1522673607200-1645062cd958?auto=format&fit=crop&w=400&q=60',
200
+ icon: <Camera className="w-5 h-5 text-yellow-600" aria-hidden="true" />
201
+ },
202
+ {
203
+ id: 'blackwhite',
204
+ name: '黑白 (B&W)',
205
+ prompt: 'Classic black and white photography, high contrast, Ansel Adams style, timeless, emotional focus, artistic composition, removing color distractions.',
206
+ description: 'Timeless high-contrast monochrome.',
207
+ coverColor: 'bg-gray-50',
208
+ previewImage: 'https://images.unsplash.com/photo-1604017011826-d3b4c23f8914?auto=format&fit=crop&w=400&q=60',
209
+ icon: <Aperture className="w-5 h-5 text-black" aria-hidden="true" />
210
+ },
211
+ {
212
+ id: 'artistic',
213
+ name: '艺术 (Artistic)',
214
+ prompt: 'Fine art wedding photography, abstract elements, creative lighting, blurry motion, painterly texture, museum quality, avant-garde outfit styling.',
215
+ description: 'Abstract, creative, fine art.',
216
+ coverColor: 'bg-purple-50',
217
+ previewImage: 'https://images.unsplash.com/photo-1523438885200-e635ba2c371e?auto=format&fit=crop&w=400&q=60',
218
+ icon: <Palette className="w-5 h-5 text-purple-500" aria-hidden="true" />
219
+ },
220
+ {
221
+ id: 'illustration',
222
+ name: '手绘 (Illustration)',
223
+ prompt: 'Wedding illustration style, watercolor or sketch effect, soft pastel colors, artistic strokes, dreamy and romantic drawing style, 2D art.',
224
+ description: 'Watercolor and sketch effects.',
225
+ coverColor: 'bg-pink-50',
226
+ previewImage: 'https://images.unsplash.com/photo-1544890225-2f3faec4cd60?auto=format&fit=crop&w=400&q=60',
227
+ icon: <PenTool className="w-5 h-5 text-pink-500" aria-hidden="true" />
228
+ },
229
+ {
230
+ id: 'oil_painting',
231
+ name: '油画 (Oil Painting)',
232
+ prompt: 'Classic oil painting style wedding, rich textured brushstrokes, Renaissance lighting, deep colors, artistic masterpiece, framed museum quality.',
233
+ description: 'Classic textured masterpiece.',
234
+ coverColor: 'bg-amber-100',
235
+ previewImage: 'https://images.unsplash.com/photo-1579783902614-a3fb39279c0f?auto=format&fit=crop&w=400&q=60',
236
+ icon: <Brush className="w-5 h-5 text-amber-700" aria-hidden="true" />
237
+ },
238
+ {
239
+ id: 'anime',
240
+ name: '动漫 (Anime)',
241
+ prompt: 'Japanese anime art style, Makoto Shinkai inspired, vibrant blue sky with cumulus clouds, sparkling light effects, high contrast, emotional and beautiful 2D animation aesthetic.',
242
+ description: 'Vibrant, emotional 2D animation.',
243
+ coverColor: 'bg-indigo-50',
244
+ previewImage: 'https://images.unsplash.com/photo-1563784462041-5f97ac9523dd?auto=format&fit=crop&w=400&q=60',
245
+ icon: <Tv className="w-5 h-5 text-indigo-500" aria-hidden="true" />
246
+ },
247
+ {
248
+ id: 'glitch',
249
+ name: 'Glitch',
250
+ prompt: 'Cyberpunk glitch art aesthetic, distorted neon lights, digital artifacts, fragmented reality, futuristic wedding attire, vibrant cyan and magenta hues, high-tech urban backdrop.',
251
+ description: 'Digital distortion and neon artifacts.',
252
+ coverColor: 'bg-teal-50',
253
+ previewImage: 'https://images.unsplash.com/photo-1550745165-9bc0b252726f?auto=format&fit=crop&w=400&q=60',
254
+ icon: <Unlink className="w-5 h-5 text-teal-600" aria-hidden="true" />,
255
+ isLocked: true // VIP
256
+ },
257
+
258
+ // 4. Modern & Trendy
259
+ {
260
+ id: 'ins',
261
+ name: 'Ins风 (Insta)',
262
+ prompt: 'Instagram aesthetic, trendy filters, low contrast, lifestyle vibe, stylish decor, succulents, macrame, influencer style, highly shareable.',
263
+ description: 'Trendy, lifestyle, social media ready.',
264
+ coverColor: 'bg-fuchsia-50',
265
+ previewImage: 'https://images.unsplash.com/photo-1515934751635-c81c6bc9a2d8?auto=format&fit=crop&w=400&q=60',
266
+ icon: <Camera className="w-5 h-5 text-fuchsia-500" aria-hidden="true" />
267
+ },
268
+ {
269
+ id: 'magazine',
270
+ name: '杂志 (Magazine)',
271
+ prompt: 'High fashion magazine cover, Vogue or Harper\'s Bazaar style, bold typography feeling, confident poses, studio lighting, sharp focus, luxury fashion.',
272
+ description: 'High fashion editorial look.',
273
+ coverColor: 'bg-zinc-50',
274
+ previewImage: 'https://images.unsplash.com/photo-1469334031218-e382a71b716b?auto=format&fit=crop&w=400&q=60',
275
+ icon: <Layout className="w-5 h-5 text-zinc-600" aria-hidden="true" />
276
+ },
277
+ {
278
+ id: 'minimalist',
279
+ name: '简约 (Minimalist)',
280
+ prompt: 'Minimalist wedding, clean lines, plenty of negative space, monochromatic color palette, simple but high-quality silk dress, sophisticated simplicity.',
281
+ description: 'Less is more, sophisticated.',
282
+ coverColor: 'bg-stone-50',
283
+ previewImage: 'https://images.unsplash.com/photo-1445633475854-60c07d57cf84?auto=format&fit=crop&w=400&q=60',
284
+ icon: <BoxIcon className="w-5 h-5 text-stone-500" />
285
+ },
286
+ {
287
+ id: 'fashion',
288
+ name: '时尚 (Fashion)',
289
+ prompt: 'Street fashion wedding, urban setting, sunglasses, blazer over dress, running shoes with gown, chic, modern, rebellious, city lights.',
290
+ description: 'Chic, urban, and runway ready.',
291
+ coverColor: 'bg-indigo-50',
292
+ previewImage: 'https://images.unsplash.com/photo-1532453288672-3a27e9be9efd?auto=format&fit=crop&w=400&q=60',
293
+ icon: <Zap className="w-5 h-5 text-indigo-500" aria-hidden="true" />
294
+ },
295
+
296
+ // 5. Atmosphere & Theme
297
+ {
298
+ id: 'retro',
299
+ name: '复古 (Retro)',
300
+ prompt: '1980s or 1990s retro hong kong style, warm yellowish tint, vintage disco vibe, sequins, puffy sleeves, nostalgic romance.',
301
+ description: '80s/90s nostalgic vibes.',
302
+ coverColor: 'bg-orange-100',
303
+ previewImage: 'https://images.unsplash.com/photo-1551024601-56296352630a?auto=format&fit=crop&w=400&q=60',
304
+ icon: <Clock className="w-5 h-5 text-orange-700" aria-hidden="true" />,
305
+ tags: ['hot']
306
+ },
307
+ {
308
+ id: 'literary',
309
+ name: '文艺 (Literary)',
310
+ prompt: 'Literary youth style, library or cafe background, soft knitted textures, glasses, muted colors, intellectual and quiet atmosphere.',
311
+ description: 'Soft, intellectual, quiet beauty.',
312
+ coverColor: 'bg-teal-50',
313
+ previewImage: 'https://images.unsplash.com/photo-1456327102063-fb5054efe647?auto=format&fit=crop&w=400&q=60',
314
+ icon: <BookOpen className="w-5 h-5 text-teal-600" aria-hidden="true" />
315
+ },
316
+ {
317
+ id: 'campus',
318
+ name: '校园风 (Campus)',
319
+ prompt: 'School days romance, school uniforms or casual wedding wear, classroom or playground background, youthful energy, sunshine, innocent love.',
320
+ description: 'Youthful, innocent school memories.',
321
+ coverColor: 'bg-blue-100',
322
+ previewImage: 'https://images.unsplash.com/photo-1523050854058-8df90110c9f1?auto=format&fit=crop&w=400&q=60',
323
+ icon: <BookOpen className="w-5 h-5 text-blue-500" aria-hidden="true" />
324
+ },
325
+ {
326
+ id: 'fairytale',
327
+ name: '童话 (Fairy Tale)',
328
+ prompt: 'Disney fairy tale style, magical castle, glowing lights, cinderella dress, sparkles, pumpkin carriage, dreamy blue and purple tones.',
329
+ description: 'Magical castles and dreams.',
330
+ coverColor: 'bg-purple-100',
331
+ previewImage: 'https://images.unsplash.com/photo-1520024146169-3240400354ae?auto=format&fit=crop&w=400&q=60',
332
+ icon: <Sparkles className="w-5 h-5 text-purple-600" aria-hidden="true" />
333
+ },
334
+ {
335
+ id: 'sweet',
336
+ name: '甜美 (Sweet)',
337
+ prompt: 'Sweet and cute style, candy colors, balloons, playful poses, pink background, joyful expressions, ribbons and bows.',
338
+ description: 'Cute, playful, candy colors.',
339
+ coverColor: 'bg-pink-100',
340
+ previewImage: 'https://images.unsplash.com/photo-1522083165195-3424ed129620?auto=format&fit=crop&w=400&q=60',
341
+ icon: <Smile className="w-5 h-5 text-pink-500" aria-hidden="true" />
342
+ },
343
+ {
344
+ id: 'story',
345
+ name: '故事 (Story)',
346
+ prompt: 'Storytelling photography, sequence of emotions, documentary approach, meaningful props, looking at each other, narrative composition.',
347
+ description: 'Narrative and emotional moments.',
348
+ coverColor: 'bg-slate-100',
349
+ previewImage: 'https://images.unsplash.com/photo-1511285560982-1351c4f809b9?auto=format&fit=crop&w=400&q=60',
350
+ icon: <Film className="w-5 h-5 text-slate-600" aria-hidden="true" />
351
+ },
352
+ {
353
+ id: 'documentary',
354
+ name: '纪实 (Documentary)',
355
+ prompt: 'Documentary wedding photojournalism, raw emotions, unposed, natural lighting, messy hair, genuine laughter, tears of joy, authentic.',
356
+ description: 'Raw, unposed, authentic moments.',
357
+ coverColor: 'bg-neutral-50',
358
+ previewImage: 'https://images.unsplash.com/photo-1519225421980-715cb0202128?auto=format&fit=crop&w=400&q=60',
359
+ icon: <Camera className="w-5 h-5 text-neutral-600" aria-hidden="true" />
360
+ },
361
+ {
362
+ id: 'fresh',
363
+ name: '小清新 (Fresh)',
364
+ prompt: 'Small fresh style (Xiao Qing Xin), nature, green grass, blue sky, white dress, oxygen girl vibe, high key lighting, natural and clean.',
365
+ description: 'Nature, clean air, greenery.',
366
+ coverColor: 'bg-lime-50',
367
+ previewImage: 'https://images.unsplash.com/photo-1470252649378-9c29740c9fa8?auto=format&fit=crop&w=400&q=60',
368
+ icon: <Leaf className="w-5 h-5 text-lime-500" aria-hidden="true" />
369
+ },
370
+ {
371
+ id: 'garden',
372
+ name: '森系 (Garden)',
373
+ prompt: 'Dreamy garden wedding, surrounded by blooming flowers, lush greenery, soft dappled sunlight filtering through trees, romantic fairy tale atmosphere, floral arch, ethereal beauty.',
374
+ description: 'Lush greenery and soft sunlight.',
375
+ coverColor: 'bg-green-50',
376
+ previewImage: 'https://images.unsplash.com/photo-1465495976277-4387d4b0b4c6?auto=format&fit=crop&w=400&q=60',
377
+ icon: <Trees className="w-5 h-5 text-green-600" aria-hidden="true" />
378
+ },
379
+ {
380
+ id: 'sexy',
381
+ name: '性感 (Sexy)',
382
+ prompt: 'Boudoir or glamorous sexy style, silk slip dress, moody lighting, red lipstick, confident gaze, allure, intimate atmosphere.',
383
+ description: 'Confident, glamorous, allure.',
384
+ coverColor: 'bg-red-100',
385
+ previewImage: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=400&q=60',
386
+ icon: <Heart className="w-5 h-5 text-red-800" aria-hidden="true" />
387
+ },
388
+ {
389
+ id: 'personality',
390
+ name: '个性 (Unique)',
391
+ prompt: 'Avant-garde personality style, quirky props, sunglasses, unconventional wedding outfit, color blocking, pop art background, fun and weird.',
392
+ description: 'Quirky, fun, unconventional.',
393
+ coverColor: 'bg-yellow-100',
394
+ previewImage: 'https://images.unsplash.com/photo-1502685104226-ee32379fefbe?auto=format&fit=crop&w=400&q=60',
395
+ icon: <Zap className="w-5 h-5 text-yellow-500" aria-hidden="true" />
396
+ },
397
+
398
+ // 6. New Diverse Styles
399
+ {
400
+ id: 'bohemian',
401
+ name: '波西米亚 (Bohemian)',
402
+ prompt: 'Bohemian wedding style, outdoor nature setting, macrame decorations, pampas grass, wildflower bouquet, lace dress, relaxed groom attire, warm earth tones, sunset light.',
403
+ description: 'Free-spirited, earthy, and natural.',
404
+ coverColor: 'bg-orange-50',
405
+ previewImage: 'https://images.unsplash.com/photo-1515488764276-beab7607c1e6?auto=format&fit=crop&w=400&q=60',
406
+ icon: <Flower className="w-5 h-5 text-orange-600" aria-hidden="true" />
407
+ },
408
+ {
409
+ id: 'art_deco',
410
+ name: '装饰艺术 (Art Deco)',
411
+ prompt: 'Great Gatsby 1920s Art Deco style, gold and black geometric patterns, luxurious ballroom, pearls, feathers, sharp tuxedo, vintage glamour, jazz age atmosphere.',
412
+ description: '1920s Gatsby luxury and geometry.',
413
+ coverColor: 'bg-yellow-50',
414
+ previewImage: 'https://images.unsplash.com/photo-1560963689-02e0397d66b7?auto=format&fit=crop&w=400&q=60',
415
+ icon: <Gem className="w-5 h-5 text-yellow-600" aria-hidden="true" />
416
+ },
417
+ {
418
+ id: 'masquerade',
419
+ name: 'Masquerade Ball',
420
+ prompt: 'Venetian Masquerade Ball wedding, elegant ornate masks, candlelit ballroom, velvet textures, mystery, dramatic shadows, opulent gowns and suits.',
421
+ description: 'Mysterious, opulent, masked elegance.',
422
+ coverColor: 'bg-purple-50',
423
+ previewImage: 'https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?auto=format&fit=crop&w=400&q=60',
424
+ icon: <Eye className="w-5 h-5 text-purple-700" aria-hidden="true" />
425
+ },
426
+ {
427
+ id: 'steampunk',
428
+ name: '蒸汽朋克 (Steampunk)',
429
+ prompt: 'Steampunk wedding style, Victorian industrial era, brass gears, clockwork elements, corsets, goggles, leather accessories, steam fog, copper tones, sepia lighting.',
430
+ description: 'Victorian sci-fi, gears, and brass.',
431
+ coverColor: 'bg-amber-100',
432
+ previewImage: 'https://images.unsplash.com/photo-1535581652167-3d6b98c39327?auto=format&fit=crop&w=400&q=60',
433
+ icon: <Cog className="w-5 h-5 text-amber-700" aria-hidden="true" />
434
+ },
435
+ {
436
+ id: 'galactic',
437
+ name: '银河 (Galactic)',
438
+ prompt: 'Sci-fi galactic wedding, outer space background, nebulae, stars, futuristic glowing fabrics, metallic silver outfits, neon lights, ethereal cosmic atmosphere.',
439
+ description: 'Futuristic, cosmic, among the stars.',
440
+ coverColor: 'bg-indigo-100',
441
+ previewImage: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?auto=format&fit=crop&w=400&q=60',
442
+ icon: <Rocket className="w-5 h-5 text-indigo-600" aria-hidden="true" />,
443
+ isLocked: true // VIP
444
+ },
445
+ {
446
+ id: 'cyberpunk',
447
+ name: '赛博朋克 (Cyberpunk)',
448
+ prompt: 'Cyberpunk wedding style, neon lights, night city, rain, futuristic techwear mixed with wedding attire, glowing accessories, blue and pink lighting, Blade Runner aesthetic.',
449
+ description: 'Neon, high-tech, futuristic city.',
450
+ coverColor: 'bg-cyan-100',
451
+ previewImage: 'https://images.unsplash.com/photo-1535295972055-1c762f4483e5?auto=format&fit=crop&w=400&q=60',
452
+ icon: <Cpu className="w-5 h-5 text-cyan-600" aria-hidden="true" />,
453
+ tags: ['new'],
454
+ isLocked: true // VIP
455
+ },
456
+
457
+ // 7. Expanded Thematic Styles
458
+ {
459
+ id: 'gothic',
460
+ name: '哥特 (Gothic)',
461
+ prompt: 'High-fashion Gothic wedding, dramatic cathedral architecture, stained glass light, black lace gown, mysterious fog, candlelight, deep burgundy and charcoal tones, moody cinematic atmosphere.',
462
+ description: 'Dark, mysterious, dramatic elegance.',
463
+ coverColor: 'bg-slate-700',
464
+ previewImage: 'https://images.unsplash.com/photo-1539103377911-4909a1bac382?auto=format&fit=crop&w=400&q=60',
465
+ icon: <Castle className="w-5 h-5 text-slate-800" aria-hidden="true" />
466
+ },
467
+ {
468
+ id: 'beach',
469
+ name: '海岛 (Beach)',
470
+ prompt: 'Luxury destination beach wedding, golden hour sunlight, turquoise ocean background, white sands, flowing boho dress, tropical florals, relaxed but elegant, warm and airy aesthetic.',
471
+ description: 'Sunset, ocean, tropical breeze.',
472
+ coverColor: 'bg-cyan-50',
473
+ previewImage: 'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?auto=format&fit=crop&w=400&q=60',
474
+ icon: <Anchor className="w-5 h-5 text-cyan-500" aria-hidden="true" />,
475
+ tags: ['hot']
476
+ },
477
+ {
478
+ id: 'winter',
479
+ name: '冰雪 (Winter)',
480
+ prompt: 'Ethereal winter wedding, snowy forest backdrop, falling snow, ice blue and silver tones, fur stole, crystal accessories, magical frozen lighting, crisp and pure atmosphere.',
481
+ description: 'Snowy, magical, frozen beauty.',
482
+ coverColor: 'bg-blue-50',
483
+ previewImage: 'https://images.unsplash.com/photo-1483664852095-d6cc6870705d?auto=format&fit=crop&w=400&q=60',
484
+ icon: <Snowflake className="w-5 h-5 text-blue-400" aria-hidden="true" />
485
+ },
486
+ {
487
+ id: 'underwater',
488
+ name: '水下 (Underwater)',
489
+ prompt: 'Fine art underwater wedding photography, weightless flowing fabric, deep blue water, shafts of sunlight breaking through surface, bubbles, serene and dreamlike composition.',
490
+ description: 'Ethereal, weightless, dreamlike.',
491
+ coverColor: 'bg-indigo-50',
492
+ previewImage: 'https://images.unsplash.com/photo-1504198266287-1659872e6590?auto=format&fit=crop&w=400&q=60',
493
+ icon: <Droplets className="w-5 h-5 text-indigo-500" aria-hidden="true" />
494
+ },
495
+ {
496
+ id: 'rustic',
497
+ name: '乡村 (Rustic)',
498
+ prompt: 'Cozy rustic barn wedding, warm string lights, wooden beams, dried flowers, vintage lace, warm brown and amber tones, intimate countryside celebration vibe.',
499
+ description: 'Barns, fairy lights, cozy warmth.',
500
+ coverColor: 'bg-amber-50',
501
+ previewImage: 'https://images.unsplash.com/photo-1470790376778-a9fcd484f839?auto=format&fit=crop&w=400&q=60',
502
+ icon: <Warehouse className="w-5 h-5 text-amber-600" aria-hidden="true" />
503
+ },
504
+
505
+ // 8. Cultural & Artistic Extras
506
+ {
507
+ id: 'elf',
508
+ name: '精灵 (Elf)',
509
+ prompt: 'Ethereal Elven wedding style, Lord of the Rings Rivendell vibe, magical forest, soft glowing light, bride in flowing gown with cape, pointed ears hint, mystical and dreamy atmosphere.',
510
+ description: 'Magical forest and ethereal vibes.',
511
+ coverColor: 'bg-emerald-100',
512
+ previewImage: 'https://images.unsplash.com/photo-1500917293891-ef795e70e1f6?auto=format&fit=crop&w=400&q=60',
513
+ icon: <Cloud className="w-5 h-5 text-emerald-500" aria-hidden="true" />,
514
+ tags: ['new'],
515
+ isLocked: true // VIP
516
+ },
517
+ {
518
+ id: 'indian',
519
+ name: '印度 (Indian)',
520
+ prompt: 'Traditional Indian wedding, luxurious red and gold Lehenga and Sherwani, intricate jewelry, Henna, marigold flowers, festive and opulent atmosphere, Bollywood grandeur.',
521
+ description: 'Opulent red and gold festivity.',
522
+ coverColor: 'bg-orange-200',
523
+ previewImage: 'https://images.unsplash.com/photo-1583391724094-067980337059?auto=format&fit=crop&w=400&q=60',
524
+ icon: <Sun className="w-5 h-5 text-orange-700" aria-hidden="true" />
525
+ },
526
+ {
527
+ id: 'thai',
528
+ name: '泰式 (Thai)',
529
+ prompt: 'Traditional Thai wedding style, Chut Thai silk costumes, golden accessories, elegant temple architectural background, warm lighting, noble and serene.',
530
+ description: 'Elegant silk and golden nobility.',
531
+ coverColor: 'bg-yellow-200',
532
+ previewImage: 'https://images.unsplash.com/photo-1596711912530-9b391787d55c?auto=format&fit=crop&w=400&q=60',
533
+ icon: <Flower className="w-5 h-5 text-yellow-700" aria-hidden="true" />
534
+ },
535
+ {
536
+ id: 'double_exposure',
537
+ name: '双重曝光 (Double Exp)',
538
+ prompt: 'Artistic double exposure photography, silhouette of the couple blended with a forest or cityscape, dreamy overlay, high contrast, surreal and creative art style.',
539
+ description: 'Surreal artistic silhouette blend.',
540
+ coverColor: 'bg-gray-200',
541
+ previewImage: 'https://images.unsplash.com/photo-1550684848-fac1c5b4e853?auto=format&fit=crop&w=400&q=60',
542
+ icon: <Layers className="w-5 h-5 text-gray-700" aria-hidden="true" />
543
+ },
544
+
545
+ // 9. Douyin Viral (TikTok China)
546
+ {
547
+ id: 'old_money',
548
+ name: '老钱风 (Old Money)',
549
+ prompt: 'Old Money aesthetic wedding, Ralph Lauren style, luxurious country club or yacht background, understated elegance, beige and navy tones, pearl necklace, tennis sweater or classy suit, rich lifestyle vibe.',
550
+ description: 'Understated wealth, luxury vibe.',
551
+ coverColor: 'bg-stone-100',
552
+ previewImage: 'https://images.unsplash.com/photo-1566737236500-c8ac43014a67?auto=format&fit=crop&w=400&q=60',
553
+ icon: <Briefcase className="w-5 h-5 text-stone-700" aria-hidden="true" />,
554
+ tags: ['new', 'hot'],
555
+ isLocked: true
556
+ },
557
+ {
558
+ id: 'phoenix_crown',
559
+ name: '凤冠霞帔 (Phoenix)',
560
+ prompt: 'Traditional Chinese Phoenix Crown style, extremely luxurious gold headdress, heavy makeup with red lips, dark background with spot lighting, majestic and dramatic queen vibe, intricate embroidery.',
561
+ description: 'Majestic gold crown, dramatic.',
562
+ coverColor: 'bg-red-200',
563
+ previewImage: 'https://images.unsplash.com/photo-1540932296774-34d6d45e933a?auto=format&fit=crop&w=400&q=60',
564
+ icon: <Crown className="w-5 h-5 text-red-800" aria-hidden="true" />,
565
+ tags: ['hot']
566
+ },
567
+ {
568
+ id: 'balletcore',
569
+ name: '芭蕾风 (Balletcore)',
570
+ prompt: 'Balletcore wedding style, ballerina aesthetics, tulle skirt, corset, ribbon lace-up, ballet flats, soft pink studio background, elegant poses, dreamy and graceful.',
571
+ description: 'Graceful ballerina ribbons.',
572
+ coverColor: 'bg-pink-50',
573
+ previewImage: 'https://images.unsplash.com/photo-1516575334481-f85287c2c81d?auto=format&fit=crop&w=400&q=60',
574
+ icon: <Music className="w-5 h-5 text-pink-400" aria-hidden="true" />,
575
+ tags: ['new']
576
+ },
577
+ {
578
+ id: 'dopamine',
579
+ name: '多巴胺 (Dopamine)',
580
+ prompt: 'Dopamine dressing wedding style, high saturation bright colors, colorful sunglasses, playful props, y2k vibes, happy energetic atmosphere, rainbow background elements.',
581
+ description: 'Bright colors, happy Y2K vibe.',
582
+ coverColor: 'bg-yellow-100',
583
+ previewImage: 'https://images.unsplash.com/photo-1496747611176-843222e1e57c?auto=format&fit=crop&w=400&q=60',
584
+ icon: <Palette className="w-5 h-5 text-yellow-600" aria-hidden="true" />
585
+ },
586
+ {
587
+ id: 'clean_fit',
588
+ name: '极简智性 (Clean Fit)',
589
+ prompt: 'Clean fit aesthetic wedding, oversized blazer, city walk street background, effortless chic, neutral tones, smart casual wedding attire, intellectual and high-end vibe.',
590
+ description: 'Effortless chic, intellectual.',
591
+ coverColor: 'bg-gray-50',
592
+ previewImage: 'https://images.unsplash.com/photo-1496440738361-1e38b979f6d4?auto=format&fit=crop&w=400&q=60',
593
+ icon: <Search className="w-5 h-5 text-gray-600" aria-hidden="true" />
594
+ },
595
+
596
+ // 10. Groom / Male Styles
597
+ {
598
+ id: 'groom_classic',
599
+ name: 'Classic Tuxedo',
600
+ prompt: 'Classic Groom portrait, sharp black tuxedo, bow tie, crisp white shirt, studio grey background, confident pose, high-end men\'s fashion photography.',
601
+ description: 'Sharp, timeless black tuxedo.',
602
+ coverColor: 'bg-gray-100',
603
+ previewImage: 'https://images.unsplash.com/photo-1507679799987-c73779587ccf?auto=format&fit=crop&w=400&q=60',
604
+ icon: <Briefcase className="w-5 h-5 text-gray-800" aria-hidden="true" />
605
+ },
606
+ {
607
+ id: 'groom_modern',
608
+ name: 'Modern Suit',
609
+ prompt: 'Modern Groom style, fitted navy or beige suit, no tie, relaxed but elegant, outdoor urban background, natural lighting, GQ magazine style.',
610
+ description: 'Relaxed modern fitted suit.',
611
+ coverColor: 'bg-blue-50',
612
+ previewImage: 'https://images.unsplash.com/photo-1480455624313-e29b44bbfde1?auto=format&fit=crop&w=400&q=60',
613
+ icon: <User className="w-5 h-5 text-blue-600" aria-hidden="true" />
614
+ },
615
+ {
616
+ id: 'groom_hanfu',
617
+ name: 'Scholar Hanfu',
618
+ prompt: 'Traditional Chinese Scholar Groom, elegant flowing Hanfu robes, ink painting background, holding a fan or scroll, poetic and noble atmosphere.',
619
+ description: 'Noble scholar in flowing robes.',
620
+ coverColor: 'bg-emerald-50',
621
+ previewImage: 'https://images.unsplash.com/photo-1531303435785-3853fb435460?auto=format&fit=crop&w=400&q=60',
622
+ icon: <Feather className="w-5 h-5 text-emerald-600" aria-hidden="true" />
623
+ },
624
+ {
625
+ id: 'groom_retro',
626
+ name: 'Retro Gent',
627
+ prompt: 'Retro Gentleman Groom, Peaky Blinders style, tweed suit, flat cap, vintage pocket watch, moody lighting, 1920s vintage atmosphere.',
628
+ description: '1920s vintage gentleman.',
629
+ coverColor: 'bg-amber-100',
630
+ previewImage: 'https://images.unsplash.com/photo-1506839683516-433b283c7438?auto=format&fit=crop&w=400&q=60',
631
+ icon: <Watch className="w-5 h-5 text-amber-800" aria-hidden="true" />
632
+ },
633
+ {
634
+ id: 'groom_cyber',
635
+ name: 'Cyber Warrior',
636
+ prompt: 'Futuristic Groom, high-tech tactical suit mixed with formal wear, neon lighting, cyberpunk city background, cool and edgy sci-fi vibe.',
637
+ description: 'Edgy, high-tech futuristic suit.',
638
+ coverColor: 'bg-cyan-100',
639
+ previewImage: 'https://images.unsplash.com/photo-1550751827-4bd374c3f58b?auto=format&fit=crop&w=400&q=60',
640
+ icon: <Cpu className="w-5 h-5 text-cyan-600" aria-hidden="true" />
641
+ },
642
+
643
+ // 11. Children's Styles
644
+ {
645
+ id: 'child_prince',
646
+ name: 'Little Prince',
647
+ prompt: 'Cute little boy dressed as a prince, royal outfit with sash and medals, small crown, castle interior background, magical lighting, adorable and noble.',
648
+ description: 'Royal outfit for a little prince.',
649
+ coverColor: 'bg-blue-100',
650
+ previewImage: 'https://images.unsplash.com/photo-1519340570198-63a234f9a56e?auto=format&fit=crop&w=400&q=60',
651
+ icon: <Crown className="w-5 h-5 text-blue-600" aria-hidden="true" />
652
+ },
653
+ {
654
+ id: 'child_princess',
655
+ name: 'Little Princess',
656
+ prompt: 'Adorable little girl in a puffy princess ballgown, sparkling tiara, magical fairy tale forest background, soft pink and glittery atmosphere.',
657
+ description: 'Magical puffy dress and tiara.',
658
+ coverColor: 'bg-pink-100',
659
+ previewImage: 'https://images.unsplash.com/photo-1526466384813-f54668b82402?auto=format&fit=crop&w=400&q=60',
660
+ icon: <Heart className="w-5 h-5 text-pink-500" aria-hidden="true" />
661
+ },
662
+ {
663
+ id: 'child_suit',
664
+ name: 'Mini Gentleman',
665
+ prompt: 'Little boy in a miniature modern suit and bow tie, hands in pockets, clean studio background, looking cool and fashion-forward, child model vibe.',
666
+ description: 'Cool mini suit and bow tie.',
667
+ coverColor: 'bg-gray-100',
668
+ previewImage: 'https://images.unsplash.com/photo-1489710437720-ebb67ec84dd2?auto=format&fit=crop&w=400&q=60',
669
+ icon: <Briefcase className="w-5 h-5 text-gray-800" aria-hidden="true" />
670
+ },
671
+
672
+ // 12. NEW 38 STYLES (Total 100)
673
+ {
674
+ id: 'mermaid',
675
+ name: 'Mermaid',
676
+ prompt: 'Magical Mermaid fantasy, underwater vibe, iridescent scales, flowing hair, pearls, shells, ethereal lighting, blue and aquamarine tones.',
677
+ description: 'Magical underwater mermaid.',
678
+ coverColor: 'bg-cyan-200',
679
+ previewImage: 'https://images.unsplash.com/photo-1534954714659-3a362f79029a?auto=format&fit=crop&w=400&q=60',
680
+ icon: <Fish className="w-5 h-5 text-cyan-600" />
681
+ },
682
+ {
683
+ id: 'angel',
684
+ name: 'Angel',
685
+ prompt: 'Heavenly Angel wedding style, massive white feathered wings, glowing halo, clouds background, divine light, pure white gown, holy and serene.',
686
+ description: 'Divine wings and halo.',
687
+ coverColor: 'bg-blue-50',
688
+ previewImage: 'https://images.unsplash.com/photo-1560130958-eff2f8546fd7?auto=format&fit=crop&w=400&q=60',
689
+ icon: <Feather className="w-5 h-5 text-blue-400" />
690
+ },
691
+ {
692
+ id: 'demon',
693
+ name: 'Demon',
694
+ prompt: 'Dark Fantasy Demon queen style, black horns, gothic makeup, red eyes, dramatic shadows, fire and smoke background, fierce and powerful.',
695
+ description: 'Dark fantasy demon queen.',
696
+ coverColor: 'bg-red-900',
697
+ previewImage: 'https://images.unsplash.com/photo-1620583486376-79c855422892?auto=format&fit=crop&w=400&q=60',
698
+ icon: <Flame className="w-5 h-5 text-red-600" />,
699
+ isLocked: true
700
+ },
701
+ {
702
+ id: 'vampire',
703
+ name: 'Vampire',
704
+ prompt: 'Victorian Vampire aesthetic, pale skin, red lips, vintage lace choker, gothic castle background, moonlight, romantic horror vibe.',
705
+ description: 'Romantic gothic horror.',
706
+ coverColor: 'bg-slate-800',
707
+ previewImage: 'https://images.unsplash.com/photo-1628260412297-a3377e45006f?auto=format&fit=crop&w=400&q=60',
708
+ icon: <Moon className="w-5 h-5 text-purple-400" />
709
+ },
710
+ {
711
+ id: 'victorian',
712
+ name: 'Victorian',
713
+ prompt: 'Victorian era portrait, high collar lace dress, corset, cameo brooch, vintage furniture background, sepia tone, historical accuracy.',
714
+ description: '19th century historical elegance.',
715
+ coverColor: 'bg-amber-100',
716
+ previewImage: 'https://images.unsplash.com/photo-1534567215160-b9df4b4f8d29?auto=format&fit=crop&w=400&q=60',
717
+ icon: <Hourglass className="w-5 h-5 text-amber-700" />
718
+ },
719
+ {
720
+ id: 'renaissance',
721
+ name: 'Renaissance',
722
+ prompt: 'Renaissance art style portrait, velvet robes, rich jewel tones, chiaroscuro lighting, Da Vinci painting aesthetic, museum quality.',
723
+ description: 'Classic art masterpiece.',
724
+ coverColor: 'bg-yellow-200',
725
+ previewImage: 'https://images.unsplash.com/photo-1578301978693-85fa9c0320b9?auto=format&fit=crop&w=400&q=60',
726
+ icon: <Brush className="w-5 h-5 text-yellow-800" />
727
+ },
728
+ {
729
+ id: 'egyptian',
730
+ name: 'Egyptian',
731
+ prompt: 'Ancient Egyptian royalty, Cleopatra style, gold headpiece, heavy eyeliner, desert background, pyramids, turquoise and gold jewelry.',
732
+ description: 'Pharaoh queen luxury.',
733
+ coverColor: 'bg-yellow-300',
734
+ previewImage: 'https://images.unsplash.com/photo-1563200989-123498877665?auto=format&fit=crop&w=400&q=60',
735
+ icon: <Landmark className="w-5 h-5 text-yellow-600" />
736
+ },
737
+ {
738
+ id: 'greek',
739
+ name: 'Greek Goddess',
740
+ prompt: 'Greek Goddess style, flowing white toga, laurel wreath, marble columns background, Mediterranean sunlight, ethereal and statuesque.',
741
+ description: 'Olympian goddess ethereal.',
742
+ coverColor: 'bg-blue-50',
743
+ previewImage: 'https://images.unsplash.com/photo-1601004890684-d8cbf643f5f2?auto=format&fit=crop&w=400&q=60',
744
+ icon: <Landmark className="w-5 h-5 text-blue-500" />
745
+ },
746
+ {
747
+ id: 'roman',
748
+ name: 'Roman Holiday',
749
+ prompt: 'Roman Holiday movie style, 1950s Vespa, Audrey Hepburn look, Rome street background, black and white or technicolor, chic and joyful.',
750
+ description: '50s chic Rome holiday.',
751
+ coverColor: 'bg-orange-50',
752
+ previewImage: 'https://images.unsplash.com/photo-1516483638261-f4dbaf036963?auto=format&fit=crop&w=400&q=60',
753
+ icon: <Film className="w-5 h-5 text-orange-500" />
754
+ },
755
+ {
756
+ id: 'viking',
757
+ name: 'Viking',
758
+ prompt: 'Nordic Viking wedding, fur cloaks, braided hair, snowy mountain background, leather armor elements, fierce and rustic atmosphere.',
759
+ description: 'Fierce Nordic warrior.',
760
+ coverColor: 'bg-stone-200',
761
+ previewImage: 'https://images.unsplash.com/photo-1616786524317-1c19b673676c?auto=format&fit=crop&w=400&q=60',
762
+ icon: <Sword className="w-5 h-5 text-stone-700" />
763
+ },
764
+ {
765
+ id: 'y2k',
766
+ name: 'Y2K',
767
+ prompt: 'Y2K aesthetic wedding, millennium cyber fashion, metallic fabrics, butterfly clips, tinted sunglasses, flip phone prop, holographic background.',
768
+ description: 'Millennium cyber fashion.',
769
+ coverColor: 'bg-pink-200',
770
+ previewImage: 'https://images.unsplash.com/photo-1617196034096-16408229e710?auto=format&fit=crop&w=400&q=60',
771
+ icon: <Monitor className="w-5 h-5 text-pink-600" />
772
+ },
773
+ {
774
+ id: 'neon',
775
+ name: 'Neon',
776
+ prompt: 'Neon noir style, dark background with bright neon sign lights (blue/pink), wet pavement, cinematic night vibe, edgy and cool.',
777
+ description: 'Dark city neon lights.',
778
+ coverColor: 'bg-purple-900',
779
+ previewImage: 'https://images.unsplash.com/photo-1563297129-9dc476717462?auto=format&fit=crop&w=400&q=60',
780
+ icon: <Zap className="w-5 h-5 text-purple-400" />
781
+ },
782
+ {
783
+ id: 'denim',
784
+ name: 'Denim',
785
+ prompt: 'Casual Denim wedding theme, matching denim jackets over wedding outfits, cool sunglasses, American vibes, relaxed and stylish.',
786
+ description: 'Casual cool denim.',
787
+ coverColor: 'bg-blue-200',
788
+ previewImage: 'https://images.unsplash.com/photo-1520336208070-13d80630b4ac?auto=format&fit=crop&w=400&q=60',
789
+ icon: <Shirt className="w-5 h-5 text-blue-700" />
790
+ },
791
+ {
792
+ id: 'suit_bride',
793
+ name: 'Bridal Suit',
794
+ prompt: 'Androgynous chic, bride in a tailored white tuxedo suit, slicked back hair, minimalist studio background, confident power pose, high fashion.',
795
+ description: 'Power suit chic.',
796
+ coverColor: 'bg-gray-50',
797
+ previewImage: 'https://images.unsplash.com/photo-1594912959648-2c262a6d71b3?auto=format&fit=crop&w=400&q=60',
798
+ icon: <Briefcase className="w-5 h-5 text-gray-700" />
799
+ },
800
+ {
801
+ id: 'sketch',
802
+ name: 'Sketch',
803
+ prompt: 'Pencil sketch art style, graphite texture, white paper background, artistic shading, hand-drawn look, creative and unique.',
804
+ description: 'Hand-drawn pencil art.',
805
+ coverColor: 'bg-gray-100',
806
+ previewImage: 'https://images.unsplash.com/photo-1516962215378-7fa2e137ae93?auto=format&fit=crop&w=400&q=60',
807
+ icon: <PenTool className="w-5 h-5 text-gray-500" />
808
+ },
809
+ {
810
+ id: 'pixel',
811
+ name: 'Pixel Art',
812
+ prompt: '8-bit Pixel Art style, retro game aesthetic, blocky graphics, vibrant colors, digital nostalgia, fun and playful.',
813
+ description: 'Retro 8-bit game style.',
814
+ coverColor: 'bg-green-100',
815
+ previewImage: 'https://images.unsplash.com/photo-1550684848-86a5d8727436?auto=format&fit=crop&w=400&q=60',
816
+ icon: <Box className="w-5 h-5 text-green-600" />
817
+ },
818
+ {
819
+ id: 'clay',
820
+ name: 'Claymation',
821
+ prompt: 'Stop-motion claymation style, Aardman animation look, plasticine texture, soft lighting, whimsical and cute.',
822
+ description: 'Whimsical stop-motion clay.',
823
+ coverColor: 'bg-orange-200',
824
+ previewImage: 'https://images.unsplash.com/photo-1620023642398-a006c7104a37?auto=format&fit=crop&w=400&q=60',
825
+ icon: <Smile className="w-5 h-5 text-orange-600" />
826
+ },
827
+ {
828
+ id: 'pop_art',
829
+ name: 'Pop Art',
830
+ prompt: 'Andy Warhol Pop Art style, comic book dots, bold solid colors, speech bubbles, high contrast, retro 60s artistic vibe.',
831
+ description: 'Bold colorful comic art.',
832
+ coverColor: 'bg-yellow-200',
833
+ previewImage: 'https://images.unsplash.com/photo-1563725656-78c9b2f67644?auto=format&fit=crop&w=400&q=60',
834
+ icon: <Zap className="w-5 h-5 text-yellow-600" />
835
+ },
836
+ {
837
+ id: 'autumn',
838
+ name: 'Autumn',
839
+ prompt: 'Golden Autumn wedding, falling yellow leaves, park setting, warm sweater textures, soft golden light, cozy and romantic.',
840
+ description: 'Golden leaves and cozy.',
841
+ coverColor: 'bg-orange-100',
842
+ previewImage: 'https://images.unsplash.com/photo-1507783548239-5198030bb402?auto=format&fit=crop&w=400&q=60',
843
+ icon: <Leaf className="w-5 h-5 text-orange-500" />
844
+ },
845
+ {
846
+ id: 'sakura',
847
+ name: 'Sakura',
848
+ prompt: 'Cherry Blossom season, pink petals falling, soft spring light, Japanese street or park background, pastel pink tones, romantic anime vibe.',
849
+ description: 'Pink cherry blossom romance.',
850
+ coverColor: 'bg-pink-100',
851
+ previewImage: 'https://images.unsplash.com/photo-1522383225653-ed111181a951?auto=format&fit=crop&w=400&q=60',
852
+ icon: <Flower className="w-5 h-5 text-pink-400" />
853
+ },
854
+ {
855
+ id: 'rainforest',
856
+ name: 'Rainforest',
857
+ prompt: 'Tropical Rainforest wedding, giant monstera leaves, waterfall background, mist, vibrant green tones, exotic flowers, wild nature.',
858
+ description: 'Lush tropical jungle.',
859
+ coverColor: 'bg-green-100',
860
+ previewImage: 'https://images.unsplash.com/photo-1542614945-316279f064f2?auto=format&fit=crop&w=400&q=60',
861
+ icon: <Trees className="w-5 h-5 text-green-700" />
862
+ },
863
+ {
864
+ id: 'barbie',
865
+ name: 'Barbie',
866
+ prompt: 'Barbiecore aesthetic, everything hot pink, plastic perfect textures, doll-like poses, fun and glamorous, fashion doll box prop.',
867
+ description: 'Hot pink doll glamour.',
868
+ coverColor: 'bg-pink-300',
869
+ previewImage: 'https://images.unsplash.com/photo-1596489369903-a128695d73ba?auto=format&fit=crop&w=400&q=60',
870
+ icon: <Heart className="w-5 h-5 text-pink-600" />
871
+ },
872
+ {
873
+ id: 'wes',
874
+ name: 'Wes Anderson',
875
+ prompt: 'Wes Anderson film style, perfect symmetry, pastel color palette, quirky props, deadpan expressions, distinctive centered composition.',
876
+ description: 'Symmetrical pastel quirky.',
877
+ coverColor: 'bg-yellow-100',
878
+ previewImage: 'https://images.unsplash.com/photo-1520635467475-6eb86a22b406?auto=format&fit=crop&w=400&q=60',
879
+ icon: <Film className="w-5 h-5 text-yellow-600" />
880
+ },
881
+ {
882
+ id: 'cottagecore',
883
+ name: 'Cottagecore',
884
+ prompt: 'Cottagecore aesthetic, picnic in a meadow, gingham dress, straw hat, wildflowers, soft sunlight, simple rural life vibe.',
885
+ description: 'Rural meadow picnic.',
886
+ coverColor: 'bg-green-50',
887
+ previewImage: 'https://images.unsplash.com/photo-1523168892408-c8167f0022d1?auto=format&fit=crop&w=400&q=60',
888
+ icon: <Home className="w-5 h-5 text-green-500" />
889
+ },
890
+ {
891
+ id: 'dark_academia',
892
+ name: 'Dark Academia',
893
+ prompt: 'Dark Academia aesthetic, old library background, tweed blazers, books, mood lighting, coffee tones, intellectual and mysterious.',
894
+ description: 'Moody library intellectual.',
895
+ coverColor: 'bg-stone-200',
896
+ previewImage: 'https://images.unsplash.com/photo-1519340241574-2cec6aef0c01?auto=format&fit=crop&w=400&q=60',
897
+ icon: <Book className="w-5 h-5 text-stone-700" />
898
+ },
899
+ {
900
+ id: 'light_academia',
901
+ name: 'Light Academia',
902
+ prompt: 'Light Academia aesthetic, beige and cream tones, classical statues, sunlight, poetry books, soft and scholarly vibe.',
903
+ description: 'Beige scholarly softness.',
904
+ coverColor: 'bg-orange-50',
905
+ previewImage: 'https://images.unsplash.com/photo-1457369804613-52c61a468e7d?auto=format&fit=crop&w=400&q=60',
906
+ icon: <BookOpen className="w-5 h-5 text-orange-400" />
907
+ },
908
+ {
909
+ id: 'mob_wife',
910
+ name: 'Mob Wife',
911
+ prompt: 'Mob Wife aesthetic, fur coat, heavy gold jewelry, animal print, big sunglasses, confident attitude, luxury SUV background, maximalist glamour.',
912
+ description: 'Maximalist fur glamour.',
913
+ coverColor: 'bg-stone-300',
914
+ previewImage: 'https://images.unsplash.com/photo-1616166417772-520f34086db0?auto=format&fit=crop&w=400&q=60',
915
+ icon: <Gem className="w-5 h-5 text-stone-800" />
916
+ },
917
+ {
918
+ id: 'all_red',
919
+ name: 'Monochrome Red',
920
+ prompt: 'Artistic monochrome red photography, everything in shades of red, high fashion, bold and dramatic, intense emotion.',
921
+ description: 'Intense all-red artistic.',
922
+ coverColor: 'bg-red-200',
923
+ previewImage: 'https://images.unsplash.com/photo-1529139574466-a302d2d3f524?auto=format&fit=crop&w=400&q=60',
924
+ icon: <Circle className="w-5 h-5 text-red-600" />
925
+ },
926
+ {
927
+ id: 'all_black',
928
+ name: 'Monochrome Black',
929
+ prompt: 'Artistic monochrome black photography, playing with shadows and textures, mysterious, elegant, high contrast.',
930
+ description: 'Elegant all-black mystery.',
931
+ coverColor: 'bg-gray-800',
932
+ previewImage: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=400&q=60',
933
+ icon: <Circle className="w-5 h-5 text-black" />
934
+ },
935
+ {
936
+ id: 'pastel',
937
+ name: 'Pastel Dream',
938
+ prompt: 'Pastel dreamscape, cotton candy clouds, soft mint, lilac and baby pink tones, dreamy and surreal atmosphere.',
939
+ description: 'Soft pastel dreamscape.',
940
+ coverColor: 'bg-purple-50',
941
+ previewImage: 'https://images.unsplash.com/photo-1492596985094-52771095ad79?auto=format&fit=crop&w=400&q=60',
942
+ icon: <Cloud className="w-5 h-5 text-purple-300" />
943
+ },
944
+ {
945
+ id: 'cyber_angel',
946
+ name: 'Cyber Angel',
947
+ prompt: 'Cybernetic Angel, mechanical wings, glowing halo, futuristic armor mixed with robes, high-tech divine aesthetic.',
948
+ description: 'High-tech divine wings.',
949
+ coverColor: 'bg-cyan-100',
950
+ previewImage: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?auto=format&fit=crop&w=400&q=60',
951
+ icon: <Cpu className="w-5 h-5 text-cyan-500" />
952
+ },
953
+ {
954
+ id: 'hanbok',
955
+ name: 'Hanbok',
956
+ prompt: 'Traditional Korean Hanbok wedding, vibrant colors, simple elegance, palace background, graceful and cultural.',
957
+ description: 'Traditional Korean dress.',
958
+ coverColor: 'bg-rose-50',
959
+ previewImage: 'https://images.unsplash.com/photo-1588667822949-a034d612df08?auto=format&fit=crop&w=400&q=60',
960
+ icon: <Globe className="w-5 h-5 text-rose-500" />
961
+ },
962
+ {
963
+ id: 'kimono',
964
+ name: 'Kimono',
965
+ prompt: 'Traditional Japanese Kimono wedding (Shiromuku), shrine background, white silk, serene and ceremonial atmosphere.',
966
+ description: 'Traditional Japanese silk.',
967
+ coverColor: 'bg-white',
968
+ previewImage: 'https://images.unsplash.com/photo-1492596985094-52771095ad79?auto=format&fit=crop&w=400&q=60',
969
+ icon: <Globe className="w-5 h-5 text-red-500" />
970
+ },
971
+ {
972
+ id: 'cheongsam',
973
+ name: 'Qipao',
974
+ prompt: 'Shanghai 1930s style Qipao (Cheongsam), vintage interior, finger wave hair, elegant and seductive oriental beauty.',
975
+ description: 'Vintage Shanghai elegance.',
976
+ coverColor: 'bg-red-50',
977
+ previewImage: 'https://images.unsplash.com/photo-1540932296774-34d6d45e933a?auto=format&fit=crop&w=400&q=60',
978
+ icon: <Flower className="w-5 h-5 text-red-700" />
979
+ },
980
+ {
981
+ id: 'cowboy',
982
+ name: 'Cowboy',
983
+ prompt: 'Western Cowboy wedding, ranch setting, cowboy hats, boots, denim and leather, horses in background, rustic and adventurous.',
984
+ description: 'Western ranch adventure.',
985
+ coverColor: 'bg-amber-100',
986
+ previewImage: 'https://images.unsplash.com/photo-1533514114760-43846b07bd26?auto=format&fit=crop&w=400&q=60',
987
+ icon: <Briefcase className="w-5 h-5 text-amber-800" />
988
+ },
989
+ {
990
+ id: 'disco',
991
+ name: 'Disco',
992
+ prompt: '70s Disco wedding, mirror ball, funky patterns, platform shoes, dance floor lighting, groovy and fun.',
993
+ description: '70s funky groove.',
994
+ coverColor: 'bg-purple-100',
995
+ previewImage: 'https://images.unsplash.com/photo-1551024601-56296352630a?auto=format&fit=crop&w=400&q=60',
996
+ icon: <Music className="w-5 h-5 text-purple-600" />
997
+ },
998
+ {
999
+ id: 'jazz',
1000
+ name: 'Jazz Bar',
1001
+ prompt: 'Jazz Club wedding vibe, smoky atmosphere, saxophone, spotlight, velvet curtains, classy and soulful.',
1002
+ description: 'Smoky classy club vibe.',
1003
+ coverColor: 'bg-stone-900',
1004
+ previewImage: 'https://images.unsplash.com/photo-1514525253440-b39345208668?auto=format&fit=crop&w=400&q=60',
1005
+ icon: <Music className="w-5 h-5 text-yellow-500" />
1006
+ },
1007
+ {
1008
+ id: 'sporty',
1009
+ name: 'Sporty',
1010
+ prompt: 'Sporty chic wedding, tennis court or stadium background, sneakers with wedding outfit, energetic and healthy vibe.',
1011
+ description: 'Active energy and sneakers.',
1012
+ coverColor: 'bg-green-50',
1013
+ previewImage: 'https://images.unsplash.com/photo-1530549387789-4c1017266635?auto=format&fit=crop&w=400&q=60',
1014
+ icon: <Activity className="w-5 h-5 text-green-600" />
1015
+ }
1016
+ ];
1017
+
1018
+ export const StyleSelector: React.FC<StyleSelectorProps> = ({
1019
+ selectedStyle,
1020
+ onSelect,
1021
+ onCustomSelect,
1022
+ onLuckySelect,
1023
+ onSlashClick,
1024
+ disabled,
1025
+ language,
1026
+ recommendedStyleIds,
1027
+ isVipUnlocked,
1028
+ isLoading,
1029
+ resolution,
1030
+ onResolutionChange
1031
+ }) => {
1032
+ const t = TRANSLATIONS[language];
1033
+ const fileInputRef = useRef<HTMLInputElement>(null);
1034
+ const [searchTerm, setSearchTerm] = useState('');
1035
+
1036
+ // Connect to User Store for Favorites Management
1037
+ const { currentUser, guestFavorites, toggleFavorite } = useUserStore();
1038
+
1039
+ // Derived favorites state: Use user's if logged in, otherwise guest favorites
1040
+ const favorites = currentUser ? (currentUser.favorites || []) : guestFavorites;
1041
+
1042
+ const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
1043
+ const file = event.target.files?.[0];
1044
+ if (file) {
1045
+ const reader = new FileReader();
1046
+ reader.onloadend = () => {
1047
+ onCustomSelect(reader.result as string);
1048
+ };
1049
+ reader.readAsDataURL(file);
1050
+ }
1051
+ };
1052
+
1053
+ const handleCustomClick = () => {
1054
+ fileInputRef.current?.click();
1055
+ };
1056
+
1057
+ const handleToggleFavorite = (id: string, e: React.MouseEvent) => {
1058
+ e.stopPropagation();
1059
+ toggleFavorite(id);
1060
+ };
1061
+
1062
+ const filteredStyles = useMemo(() => {
1063
+ return STYLES.filter(style => {
1064
+ const name = (t.styles as any)[style.id] || style.name;
1065
+ const term = searchTerm.toLowerCase();
1066
+ return name.toLowerCase().includes(term) || style.description.toLowerCase().includes(term);
1067
+ });
1068
+ }, [searchTerm, language, t.styles]);
1069
+
1070
+ // Card Renderer Helper
1071
+ const renderStyleCard = (style: WeddingStyle) => {
1072
+ const isSelected = selectedStyle?.id === style.id;
1073
+ const isRecommended = recommendedStyleIds?.includes(style.id);
1074
+ const isLocked = style.isLocked && !isVipUnlocked;
1075
+ const name = (t.styles as any)[style.id] || style.name;
1076
+ const isFav = favorites.includes(style.id);
1077
+
1078
+ return (
1079
+ <li key={style.id} className="list-none">
1080
+ <button
1081
+ onClick={() => !isLocked && onSelect(style)}
1082
+ disabled={disabled}
1083
+ className={`
1084
+ w-full group relative aspect-[3/4] rounded-xl overflow-hidden text-left transition-all duration-300
1085
+ border-2 ${isSelected ? 'border-rose-500 ring-4 ring-rose-100 scale-[1.02] shadow-xl z-10' : 'border-transparent hover:border-gray-200 hover:shadow-lg hover:scale-[1.01]'}
1086
+ ${isLocked ? 'cursor-not-allowed opacity-80' : 'cursor-pointer'}
1087
+ `}
1088
+ >
1089
+ {/* Background Image / Color */}
1090
+ <LazyStyleBackground src={style.previewImage} colorClass={style.coverColor} />
1091
+
1092
+ {/* Content Overlay */}
1093
+ <div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent p-3 flex flex-col justify-end">
1094
+ <div className="flex items-start justify-between mb-auto">
1095
+ {/* Left: Tags */}
1096
+ <div className="flex flex-col gap-1">
1097
+ {style.tags?.includes('hot') && (
1098
+ <span className="px-1.5 py-0.5 bg-red-500 text-white text-[9px] font-bold rounded flex items-center gap-0.5 w-fit shadow-sm">
1099
+ <Flame className="w-2.5 h-2.5" aria-hidden="true" /> HOT
1100
+ </span>
1101
+ )}
1102
+ {style.tags?.includes('new') && (
1103
+ <span className="px-1.5 py-0.5 bg-blue-500 text-white text-[9px] font-bold rounded flex items-center gap-0.5 w-fit shadow-sm">
1104
+ <Sparkles className="w-2.5 h-2.5" aria-hidden="true" /> NEW
1105
+ </span>
1106
+ )}
1107
+ {isRecommended && (
1108
+ <span className="px-1.5 py-0.5 bg-green-500 text-white text-[9px] font-bold rounded flex items-center gap-0.5 w-fit animate-pulse shadow-sm">
1109
+ <ScanFace className="w-2.5 h-2.5" aria-hidden="true" /> MATCH
1110
+ </span>
1111
+ )}
1112
+ </div>
1113
+
1114
+ {/* Right: Actions (Fav + Status) */}
1115
+ <div className="flex gap-1 items-start">
1116
+ {/* Favorite Button */}
1117
+ <button
1118
+ onClick={(e) => handleToggleFavorite(style.id, e)}
1119
+ className={`
1120
+ p-1.5 rounded-full backdrop-blur-sm transition-all duration-300 z-20 shadow-sm
1121
+ hover:scale-110 active:scale-90
1122
+ ${isFav
1123
+ ? 'bg-white text-yellow-400 shadow-yellow-200/50'
1124
+ : 'bg-black/40 text-white hover:bg-black/60'}
1125
+ `}
1126
+ aria-label={isFav ? "Remove from favorites" : "Add to favorites"}
1127
+ aria-pressed={isFav}
1128
+ >
1129
+ <Star
1130
+ className={`w-3.5 h-3.5 transition-all duration-300 ${isFav ? 'fill-yellow-400 scale-110' : 'fill-transparent'}`}
1131
+ />
1132
+ </button>
1133
+
1134
+ {/* Status Icon */}
1135
+ {isSelected ? (
1136
+ <div className="bg-rose-500 text-white p-1 rounded-full shadow-lg animate-fade-in" aria-label="Selected">
1137
+ <CheckIcon className="w-3.5 h-3.5" />
1138
+ </div>
1139
+ ) : isLocked ? (
1140
+ <button
1141
+ onClick={(e) => { e.stopPropagation(); onSlashClick(style); }}
1142
+ className="bg-black/60 backdrop-blur text-white p-1.5 rounded-full hover:bg-rose-500 transition-colors z-20"
1143
+ aria-label="Locked, click to unlock"
1144
+ >
1145
+ <Lock className="w-3.5 h-3.5" />
1146
+ </button>
1147
+ ) : (
1148
+ <div className="bg-white/20 backdrop-blur text-white p-1.5 rounded-full opacity-0 group-hover:opacity-100 transition-opacity" aria-hidden="true">
1149
+ {style.icon}
1150
+ </div>
1151
+ )}
1152
+ </div>
1153
+ </div>
1154
+
1155
+ <h3 className="font-bold text-white text-sm leading-tight drop-shadow-md" aria-hidden={isLocked && !isSelected}>
1156
+ {name}
1157
+ </h3>
1158
+ <p className="text-[10px] text-gray-300 line-clamp-2 mt-0.5" aria-hidden="true">
1159
+ {style.description}
1160
+ </p>
1161
+ </div>
1162
+
1163
+ {/* Selection Indicator Border Overlay */}
1164
+ {isSelected && <div className="absolute inset-0 border-2 border-rose-500 rounded-xl pointer-events-none"></div>}
1165
+ </button>
1166
+ </li>
1167
+ );
1168
+ };
1169
+
1170
+ if (isLoading) {
1171
+ return (
1172
+ <div className="space-y-6">
1173
+ <div className="flex flex-col sm:flex-row gap-3">
1174
+ <div className="flex-1 h-12 bg-gray-100 rounded-xl animate-pulse"></div>
1175
+ <div className="w-full sm:w-48 h-12 bg-gray-100 rounded-xl animate-pulse"></div>
1176
+ </div>
1177
+ <ul className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3 list-none p-0">
1178
+ {[...Array(8)].map((_, i) => <StyleCardSkeleton key={i} />)}
1179
+ </ul>
1180
+ </div>
1181
+ );
1182
+ }
1183
+
1184
+ return (
1185
+ <div className="space-y-6">
1186
+ {/* Search and Resolution Toolbar */}
1187
+ <div className="flex flex-col sm:flex-row gap-3">
1188
+ {/* Search Bar */}
1189
+ <div className="relative flex-1">
1190
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" aria-hidden="true" />
1191
+ <input
1192
+ type="text"
1193
+ placeholder={t.styleSearchPlace}
1194
+ value={searchTerm}
1195
+ onChange={e => setSearchTerm(e.target.value)}
1196
+ className="w-full pl-9 pr-4 py-2.5 rounded-xl border border-gray-200 bg-gray-50 focus:bg-white focus:border-rose-500 focus:ring-1 focus:ring-rose-200 outline-none text-sm transition-all"
1197
+ disabled={disabled}
1198
+ aria-label="Search styles"
1199
+ />
1200
+ </div>
1201
+
1202
+ {/* Resolution Selector */}
1203
+ <div className="flex bg-gray-100 p-1 rounded-xl shrink-0 overflow-x-auto">
1204
+ <button
1205
+ onClick={() => onResolutionChange('standard')}
1206
+ className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-all whitespace-nowrap ${resolution === 'standard' ? 'bg-white shadow text-gray-800' : 'text-gray-500 hover:text-gray-700'}`}
1207
+ >
1208
+ HD
1209
+ </button>
1210
+ <button
1211
+ onClick={() => onResolutionChange('high')}
1212
+ className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-all whitespace-nowrap ${resolution === 'high' ? 'bg-white shadow text-rose-600' : 'text-gray-500 hover:text-gray-700'}`}
1213
+ >
1214
+ 4K
1215
+ </button>
1216
+ <button
1217
+ onClick={() => onResolutionChange('ultra')}
1218
+ className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-all flex items-center gap-1 whitespace-nowrap ${resolution === 'ultra' ? 'bg-gradient-to-r from-rose-500 to-orange-500 text-white shadow' : 'text-gray-500 hover:text-gray-700'}`}
1219
+ >
1220
+ {resolution === 'ultra' && <Maximize className="w-3 h-3" />}
1221
+ Ultra 8K
1222
+ </button>
1223
+ </div>
1224
+ </div>
1225
+
1226
+ {/* Quick Actions Grid */}
1227
+ <ul className="grid grid-cols-2 sm:grid-cols-4 gap-3 list-none p-0">
1228
+ {/* Custom Upload */}
1229
+ <li>
1230
+ <button
1231
+ onClick={handleCustomClick}
1232
+ disabled={disabled}
1233
+ className="w-full h-full relative overflow-hidden group flex flex-col items-center justify-center p-3 rounded-xl border border-gray-200 hover:border-rose-300 hover:bg-rose-50 transition-all bg-white"
1234
+ aria-label="Upload custom style image"
1235
+ >
1236
+ <div className="w-8 h-8 rounded-full bg-rose-100 flex items-center justify-center mb-1 group-hover:scale-110 transition-transform">
1237
+ <Upload className="w-4 h-4 text-rose-500" aria-hidden="true" />
1238
+ </div>
1239
+ <span className="text-xs font-bold text-gray-700">{t.customStyle}</span>
1240
+ <input type="file" accept="image/*" className="hidden" ref={fileInputRef} onChange={handleFileChange} tabIndex={-1} aria-hidden="true" />
1241
+ </button>
1242
+ </li>
1243
+
1244
+ {/* Lucky Pick */}
1245
+ <li>
1246
+ <button
1247
+ onClick={onLuckySelect}
1248
+ disabled={disabled}
1249
+ className="w-full h-full relative overflow-hidden group flex flex-col items-center justify-center p-3 rounded-xl border border-gray-200 hover:border-amber-300 hover:bg-amber-50 transition-all bg-white"
1250
+ aria-label="Lucky Pick: Random style"
1251
+ >
1252
+ <div className="w-8 h-8 rounded-full bg-amber-100 flex items-center justify-center mb-1 group-hover:scale-110 transition-transform">
1253
+ <Sparkles className="w-4 h-4 text-amber-500" aria-hidden="true" />
1254
+ </div>
1255
+ <span className="text-xs font-bold text-gray-700">{t.luckyStyle}</span>
1256
+ </button>
1257
+ </li>
1258
+
1259
+ {/* VIP Unlock / Slash - Only show if not VIP */}
1260
+ {!isVipUnlocked && (
1261
+ <li className="col-span-2 sm:col-span-2">
1262
+ <button
1263
+ onClick={() => onSlashClick(STYLES[0])} // Just trigger modal
1264
+ disabled={disabled}
1265
+ className="w-full h-full relative overflow-hidden group flex flex-col items-center justify-center p-3 rounded-xl border border-gray-200 hover:border-purple-300 hover:bg-purple-50 transition-all bg-white"
1266
+ aria-label="Unlock styles via viral share"
1267
+ >
1268
+ <div className="flex items-center gap-2 mb-1">
1269
+ <div className="w-8 h-8 rounded-full bg-purple-100 flex items-center justify-center group-hover:scale-110 transition-transform">
1270
+ <Zap className="w-4 h-4 text-purple-500" aria-hidden="true" />
1271
+ </div>
1272
+ </div>
1273
+ <span className="text-xs font-bold text-gray-700">{t.pddSlashTitle}</span>
1274
+ <span className="text-[10px] text-gray-400" aria-hidden="true">Limited Time Offer</span>
1275
+ </button>
1276
+ </li>
1277
+ )}
1278
+ </ul>
1279
+
1280
+ {/* Favorites Section */}
1281
+ {!searchTerm && favorites.length > 0 && (
1282
+ <section className="space-y-3" aria-label="Favorites">
1283
+ <h3 className="text-sm font-bold text-gray-800 flex items-center gap-2 px-1">
1284
+ <Star className="w-4 h-4 fill-rose-500 text-rose-500" aria-hidden="true" />
1285
+ {t.sectFavorites || "My Favorites"}
1286
+ </h3>
1287
+ <ul className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3 list-none p-0">
1288
+ {favorites.map(id => {
1289
+ const s = STYLES.find(style => style.id === id);
1290
+ return s ? renderStyleCard(s) : null;
1291
+ })}
1292
+ </ul>
1293
+ </section>
1294
+ )}
1295
+
1296
+ {/* Styles Grid */}
1297
+ <section className="space-y-3" aria-label="All Styles">
1298
+ {(!searchTerm && favorites.length > 0) && (
1299
+ <h3 className="text-sm font-bold text-gray-700 px-1">{t.browseAll || "All Styles"}</h3>
1300
+ )}
1301
+ <ul className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3 max-h-[500px] overflow-y-auto pr-2 custom-scrollbar pb-10 list-none p-0">
1302
+ {filteredStyles.map(renderStyleCard)}
1303
+ {filteredStyles.length === 0 && (
1304
+ <li className="col-span-full py-8 text-center text-gray-400 text-sm">
1305
+ No styles found matching "{searchTerm}"
1306
+ </li>
1307
+ )}
1308
+ </ul>
1309
+ </section>
1310
+ </div>
1311
+ );
1312
+ };
1313
+
1314
+ const CheckIcon = ({ className }: { className?: string }) => (
1315
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" className={className} aria-hidden="true">
1316
+ <polyline points="20 6 9 17 4 12" />
1317
+ </svg>
1318
+ );
components/Testimonials.tsx ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Star, Quote } from 'lucide-react';
3
+ import { Language } from '../types';
4
+ import { TRANSLATIONS } from '../constants/translations';
5
+
6
+ interface TestimonialsProps {
7
+ language: Language;
8
+ }
9
+
10
+ export const Testimonials: React.FC<TestimonialsProps> = ({ language }) => {
11
+ const t = TRANSLATIONS[language];
12
+
13
+ const reviews = [
14
+ {
15
+ text: t.review1,
16
+ user: t.review1User,
17
+ initials: "AT",
18
+ color: "bg-blue-100 text-blue-600"
19
+ },
20
+ {
21
+ text: t.review2,
22
+ user: t.review2User,
23
+ initials: "Z",
24
+ color: "bg-rose-100 text-rose-600"
25
+ },
26
+ {
27
+ text: t.review3,
28
+ user: t.review3User,
29
+ initials: "EW",
30
+ color: "bg-amber-100 text-amber-600"
31
+ }
32
+ ];
33
+
34
+ return (
35
+ <section className="py-12 bg-gray-50 border-t border-gray-100">
36
+ <div className="max-w-7xl mx-auto px-4 sm:px-6">
37
+ <div className="text-center mb-10">
38
+ <h2 className="text-2xl sm:text-3xl font-serif font-bold text-gray-900 mb-2">
39
+ {t.reviewsTitle}
40
+ </h2>
41
+ <div className="flex items-center justify-center gap-1 text-yellow-400 mb-2">
42
+ {[1,2,3,4,5].map(i => <Star key={i} className="w-5 h-5 fill-current" />)}
43
+ </div>
44
+ <p className="text-gray-500">{t.reviewsDesc}</p>
45
+ </div>
46
+
47
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
48
+ {reviews.map((review, idx) => (
49
+ <div key={idx} className="bg-white p-6 rounded-2xl shadow-sm border border-gray-100 relative hover:-translate-y-1 transition-transform duration-300">
50
+ <Quote className="absolute top-6 right-6 w-8 h-8 text-gray-100" />
51
+ <div className="flex items-center gap-1 text-yellow-400 mb-4">
52
+ {[1,2,3,4,5].map(i => <Star key={i} className="w-4 h-4 fill-current" />)}
53
+ </div>
54
+ <p className="text-gray-600 text-sm italic mb-6 leading-relaxed relative z-10">
55
+ "{review.text}"
56
+ </p>
57
+ <div className="flex items-center gap-3">
58
+ <div className={`w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm ${review.color}`}>
59
+ {review.initials}
60
+ </div>
61
+ <div>
62
+ <h4 className="font-bold text-gray-900 text-sm">{review.user}</h4>
63
+ <p className="text-xs text-gray-400">Verified Customer</p>
64
+ </div>
65
+ </div>
66
+ </div>
67
+ ))}
68
+ </div>
69
+ </div>
70
+ </section>
71
+ );
72
+ };
components/UserCenter.tsx ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React, { useState } from 'react';
3
+ import { X, Crown, Star, Share2, UserPlus, Gift, Calendar, Check, User, Clock, FileText } from 'lucide-react';
4
+ import { Language, UserAccount, LeadData, AdminConfig } from '../types';
5
+ import { TRANSLATIONS } from '../constants/translations';
6
+
7
+ interface UserCenterProps {
8
+ isVisible: boolean;
9
+ onClose: () => void;
10
+ language: Language;
11
+ user: UserAccount;
12
+ leads?: LeadData[];
13
+ config?: AdminConfig; // NEW
14
+ onRedeem: () => void;
15
+ showToast: (msg: string) => void;
16
+ onAddPoints: (amount: number, reason: string) => void;
17
+ }
18
+
19
+ export const UserCenter: React.FC<UserCenterProps> = ({
20
+ isVisible, onClose, language, user, leads = [], config, onRedeem, showToast, onAddPoints
21
+ }) => {
22
+ const [activeTab, setActiveTab] = useState<'profile' | 'bookings'>('profile');
23
+ const t = TRANSLATIONS[language];
24
+
25
+ if (!isVisible) return null;
26
+
27
+ // Filter leads for this user
28
+ const myBookings = leads.filter(l => l.userId === user.id);
29
+
30
+ // Dynamic Point Values
31
+ const ptShare = config?.pointsShare || 10;
32
+ const ptInvite = config?.pointsInvite || 50;
33
+ const ptBook = config?.pointsBook || 100;
34
+ const costVip = config?.pointsVipCost || 100;
35
+
36
+ const handleCopyLink = () => {
37
+ navigator.clipboard.writeText("https://romantic-life.com/invite/" + user.id);
38
+ showToast(t.toastCopy);
39
+ setTimeout(() => {
40
+ const recent = user.history.find(h => h.action === 'invite' && (Date.now() - h.timestamp) < 60000);
41
+ if(!recent) onAddPoints(ptInvite, 'Invite Link Shared');
42
+ }, 1000);
43
+ };
44
+
45
+ const handleShare = () => {
46
+ showToast("Sharing to WeChat...");
47
+ setTimeout(() => {
48
+ const recent = user.history.find(h => h.action === 'share' && (Date.now() - h.timestamp) < 60000);
49
+ if(!recent) onAddPoints(ptShare, 'Poster Shared');
50
+ }, 1000);
51
+ }
52
+
53
+ const getStatusBadge = (status: LeadData['status']) => {
54
+ switch(status) {
55
+ case 'new': return <span className="text-[10px] font-bold px-2 py-0.5 rounded bg-blue-100 text-blue-700 uppercase">{t.statusNew}</span>;
56
+ case 'contacted': return <span className="text-[10px] font-bold px-2 py-0.5 rounded bg-amber-100 text-amber-700 uppercase">{t.statusContacted}</span>;
57
+ case 'booked': return <span className="text-[10px] font-bold px-2 py-0.5 rounded bg-green-100 text-green-700 uppercase">{t.statusBooked}</span>;
58
+ }
59
+ }
60
+
61
+ return (
62
+ <div className="fixed inset-0 z-[90] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in">
63
+ <div className="bg-white rounded-3xl shadow-2xl w-full max-w-md overflow-hidden relative flex flex-col max-h-[80vh]">
64
+ {/* Header Card */}
65
+ <div className="bg-gray-900 text-white p-6 relative overflow-hidden shrink-0">
66
+ <button onClick={onClose} className="absolute top-4 right-4 p-1 bg-white/10 rounded-full hover:bg-white/20 transition-colors z-20">
67
+ <X className="w-5 h-5 text-white" />
68
+ </button>
69
+
70
+ {/* Decorative Circles */}
71
+ <div className="absolute -top-10 -right-10 w-32 h-32 bg-rose-500/20 rounded-full blur-2xl"></div>
72
+ <div className="absolute bottom-0 left-0 w-24 h-24 bg-blue-500/20 rounded-full blur-2xl"></div>
73
+
74
+ <div className="relative z-10 flex items-center gap-4">
75
+ <div className="w-16 h-16 rounded-full bg-gradient-to-br from-rose-400 to-orange-500 p-0.5">
76
+ <div className="w-full h-full rounded-full bg-gray-800 flex items-center justify-center overflow-hidden">
77
+ {user.avatar ? (
78
+ <img src={user.avatar} className="w-full h-full object-cover" />
79
+ ) : (
80
+ <span className="text-2xl font-bold text-white">{user.name.charAt(0)}</span>
81
+ )}
82
+ </div>
83
+ </div>
84
+ <div>
85
+ <h2 className="text-xl font-bold">{user.name}</h2>
86
+ <div className="flex items-center gap-2 mt-1">
87
+ <div className={`text-[10px] font-bold px-2 py-0.5 rounded-full flex items-center gap-1 ${user.isVip ? 'bg-amber-400 text-amber-900' : 'bg-gray-700 text-gray-300'}`}>
88
+ <Crown className="w-3 h-3" />
89
+ {user.isVip ? t.vipActive : t.vipInactive}
90
+ </div>
91
+ <span className="text-xs text-gray-400">ID: {user.id.slice(0,6)}</span>
92
+ </div>
93
+ </div>
94
+ </div>
95
+
96
+ <div className="mt-6 bg-white/10 rounded-xl p-4 flex items-center justify-between backdrop-blur-sm">
97
+ <div>
98
+ <p className="text-xs text-gray-400 uppercase tracking-wider">{t.myPoints}</p>
99
+ <p className="text-3xl font-bold text-amber-400 mt-1">{user.points}</p>
100
+ </div>
101
+ {user.isVip ? (
102
+ <div className="px-3 py-1.5 bg-amber-500/20 text-amber-400 rounded-lg text-xs font-bold border border-amber-500/50 flex items-center gap-1">
103
+ <Check className="w-3 h-3" /> VIP Unlocked
104
+ </div>
105
+ ) : (
106
+ <button
107
+ onClick={onRedeem}
108
+ disabled={user.points < costVip}
109
+ className={`px-4 py-2 rounded-lg text-xs font-bold transition-colors ${user.points >= costVip ? 'bg-amber-400 text-amber-900 hover:bg-amber-300' : 'bg-gray-700 text-gray-500 cursor-not-allowed'}`}
110
+ >
111
+ {t.rewardVip} ({costVip} pts)
112
+ </button>
113
+ )}
114
+ </div>
115
+ </div>
116
+
117
+ {/* Navigation Tabs */}
118
+ <div className="flex border-b border-gray-100 bg-white shrink-0">
119
+ <button
120
+ onClick={() => setActiveTab('profile')}
121
+ className={`flex-1 py-3 text-sm font-bold flex items-center justify-center gap-2 ${activeTab === 'profile' ? 'text-rose-600 border-b-2 border-rose-600' : 'text-gray-500'}`}
122
+ >
123
+ <User className="w-4 h-4" /> {t.tabProfile}
124
+ </button>
125
+ <button
126
+ onClick={() => setActiveTab('bookings')}
127
+ className={`flex-1 py-3 text-sm font-bold flex items-center justify-center gap-2 ${activeTab === 'bookings' ? 'text-rose-600 border-b-2 border-rose-600' : 'text-gray-500'}`}
128
+ >
129
+ <Clock className="w-4 h-4" /> {t.tabBookings}
130
+ </button>
131
+ </div>
132
+
133
+ {/* Content Area */}
134
+ <div className="p-6 space-y-6 overflow-y-auto flex-1 bg-gray-50/50">
135
+
136
+ {activeTab === 'profile' && (
137
+ <>
138
+ {/* Tasks Section */}
139
+ <div>
140
+ <h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2">
141
+ <Star className="w-4 h-4 text-rose-500" /> {t.earnPoints}
142
+ </h3>
143
+ <div className="space-y-3">
144
+ <div className="flex items-center justify-between p-3 bg-white rounded-xl border border-rose-100 shadow-sm">
145
+ <div className="flex items-center gap-3">
146
+ <div className="w-8 h-8 rounded-full bg-rose-50 flex items-center justify-center text-rose-500"><Share2 className="w-4 h-4" /></div>
147
+ <div>
148
+ <p className="text-sm font-bold text-gray-800">{t.taskShare}</p>
149
+ <p className="text-xs text-rose-500">+{ptShare} pts</p>
150
+ </div>
151
+ </div>
152
+ <button onClick={handleShare} className="text-xs font-bold bg-gray-50 px-3 py-1.5 rounded-full border border-gray-200 text-gray-600 hover:bg-rose-50 transition-colors">Go</button>
153
+ </div>
154
+
155
+ <div className="flex items-center justify-between p-3 bg-white rounded-xl border border-blue-100 shadow-sm">
156
+ <div className="flex items-center gap-3">
157
+ <div className="w-8 h-8 rounded-full bg-blue-50 flex items-center justify-center text-blue-500"><UserPlus className="w-4 h-4" /></div>
158
+ <div>
159
+ <p className="text-sm font-bold text-gray-800">{t.taskInvite}</p>
160
+ <p className="text-xs text-blue-500">+{ptInvite} pts</p>
161
+ </div>
162
+ </div>
163
+ <button onClick={handleCopyLink} className="text-xs font-bold bg-gray-50 px-3 py-1.5 rounded-full border border-gray-200 text-gray-600 hover:bg-blue-50 transition-colors">Copy</button>
164
+ </div>
165
+
166
+ <div className="flex items-center justify-between p-3 bg-white rounded-xl border border-amber-100 shadow-sm">
167
+ <div className="flex items-center gap-3">
168
+ <div className="w-8 h-8 rounded-full bg-amber-50 flex items-center justify-center text-amber-500"><Calendar className="w-4 h-4" /></div>
169
+ <div>
170
+ <p className="text-sm font-bold text-gray-800">{t.taskBook}</p>
171
+ <p className="text-xs text-amber-500">+{ptBook} pts</p>
172
+ </div>
173
+ </div>
174
+ <button className="text-xs font-bold bg-gray-50 px-3 py-1.5 rounded-full border border-gray-200 text-gray-600 cursor-default opacity-60">Book</button>
175
+ </div>
176
+ </div>
177
+ </div>
178
+
179
+ {/* Rewards Section */}
180
+ <div>
181
+ <h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2">
182
+ <Gift className="w-4 h-4 text-purple-500" /> {t.redeemRewards}
183
+ </h3>
184
+ <div className="grid grid-cols-2 gap-3">
185
+ <div className={`p-4 bg-white rounded-xl border border-gray-200 shadow-sm flex flex-col items-center text-center transition-opacity ${user.isVip ? 'opacity-50' : 'opacity-100'}`}>
186
+ <div className="w-10 h-10 bg-gray-50 rounded-full flex items-center justify-center text-gray-400 mb-2"><Crown className="w-5 h-5" /></div>
187
+ <p className="text-xs font-bold text-gray-800">{t.rewardVip}</p>
188
+ <p className="text-[10px] text-gray-500 mt-1">{costVip} pts</p>
189
+ </div>
190
+ <div className="p-4 bg-white rounded-xl border border-gray-200 shadow-sm flex flex-col items-center text-center opacity-50">
191
+ <div className="w-10 h-10 bg-green-50 rounded-full flex items-center justify-center text-green-500 mb-2"><Gift className="w-5 h-5" /></div>
192
+ <p className="text-xs font-bold text-gray-800">{t.rewardCoupon}</p>
193
+ <p className="text-[10px] text-gray-500 mt-1">500 pts</p>
194
+ </div>
195
+ </div>
196
+ </div>
197
+ </>
198
+ )}
199
+
200
+ {activeTab === 'bookings' && (
201
+ <div className="space-y-4">
202
+ {myBookings.length === 0 ? (
203
+ <div className="text-center py-10 text-gray-400">
204
+ <Calendar className="w-12 h-12 mx-auto mb-3 opacity-30" />
205
+ <p>{t.noBookings}</p>
206
+ </div>
207
+ ) : (
208
+ myBookings.map(lead => (
209
+ <div key={lead.id} className="bg-white p-4 rounded-xl shadow-sm border border-gray-100">
210
+ <div className="flex items-center justify-between mb-2">
211
+ <div className="flex items-center gap-2">
212
+ <FileText className="w-4 h-4 text-gray-400" />
213
+ <span className="text-sm font-bold text-gray-800">{lead.service}</span>
214
+ </div>
215
+ {getStatusBadge(lead.status)}
216
+ </div>
217
+ <div className="text-xs text-gray-500 space-y-1 pl-6">
218
+ <p>Date: {lead.date || 'Pending'}</p>
219
+ <p>Budget: {lead.budget}</p>
220
+ <p className="text-[10px] text-gray-400 mt-2">{new Date(lead.timestamp).toLocaleString()}</p>
221
+ </div>
222
+ </div>
223
+ ))
224
+ )}
225
+ </div>
226
+ )}
227
+
228
+ </div>
229
+ </div>
230
+ </div>
231
+ );
232
+ };
constants/._translations.ts ADDED
Binary file (4.1 kB). View file
 
constants/translations.ts ADDED
@@ -0,0 +1,965 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ import { Language } from '../types';
4
+
5
+ const en = {
6
+ title: "Romantic Life",
7
+ subtitle: "Xiamen Wedding Photography",
8
+ heroTitle: "AI Wedding Fitting Room",
9
+ heroDesc: "Try 100+ premier wedding styles instantly. From Classic to Avant-Garde, find your perfect look with Romantic Life.",
10
+ beta: "AI Fitting v2.0",
11
+
12
+ // Marketing
13
+ promoBanner: "🎉 Spring Wedding Expo: Book online today and get a Free Makeup Trial + ¥500 Voucher!",
14
+ promoEnds: "Offer ends in:",
15
+ tagHot: "HOT",
16
+ tagNew: "NEW",
17
+ tagRec: "EDITOR'S PICK",
18
+ floatConsult: "Consult",
19
+
20
+ // PDD Viral Copy
21
+ pddSlashBtn: "Help Me Unlock",
22
+ pddSlashTitle: "Unlock for FREE!",
23
+ pddSlashDesc: "Invite 1 friend to cut the price to ¥0!",
24
+ pddSlashProgress: "Unlocked 98%...",
25
+ pddSlashCta: "Invite Friend to Cut",
26
+ pddSlashSuccess: "Cut Successfully!",
27
+ pddRedPacketTitle: "Congratulations!",
28
+ pddRedPacketDesc: "You received a Wedding Cash Voucher",
29
+ pddRedPacketCta: "Open Now",
30
+ pddWithdrawTitle: "Withdraw ¥2000",
31
+ pddWithdrawDesc: "Only ¥20 left to withdraw! Share to boost.",
32
+ pddGroupJoin: "Join Group",
33
+ pddGroupJoined: "people joined",
34
+
35
+ // AI & Ticker
36
+ aiScanning: "AI Face Scanning...",
37
+ aiAnalyzing: "Analyzing facial features & skin tone...",
38
+ aiMatching: "Matching best wedding styles...",
39
+ aiBestMatch: "AI MATCH 98%",
40
+ tickerBooked: "booked",
41
+ tickerJustNow: "Just now",
42
+ tickerMinsAgo: "mins ago",
43
+
44
+ // Analysis Modal (NEW)
45
+ analysisTitle: "AI DIAGNOSIS",
46
+ analysisSubtitle: "Biometric Esthetics Report",
47
+ lblFaceShape: "Face Shape",
48
+ lblSkinTone: "Skin Tone Analysis",
49
+ lblRecVibe: "Recommended Vibe",
50
+ aiConfidence: "AI MATCHING CONFIDENCE",
51
+
52
+ // Steps
53
+ step1: "Upload Photo",
54
+ step2: "Select Style",
55
+ step3: "Personalize",
56
+
57
+ // Upload
58
+ uploadTitle: "Upload Your Portrait",
59
+ uploadDesc: "Drag & drop or click. Best results with clear face visibility.",
60
+ changePhoto: "Change Photo",
61
+
62
+ // Styles
63
+ styleSearchPlace: "Search styles...",
64
+ customStyle: "Custom Style",
65
+ customStyleDesc: "Upload reference photo",
66
+ luckyStyle: "Lucky Pick",
67
+ luckyStyleDesc: "Random style selection",
68
+ clearSelection: "Clear selection",
69
+ usingCustom: "Using Custom Reference",
70
+ usingCustomDesc: "Applying this style to your photo.",
71
+ sectFavorites: "My Favorites", // NEW
72
+ browseAll: "Browse All Styles", // NEW
73
+ viewCategories: "View Categories", // NEW
74
+
75
+ // VIP
76
+ vipLocked: "VIP Style",
77
+ vipUnlockTitle: "Unlock VIP Styles",
78
+ vipUnlockDesc: "Register as a member to unlock all premium styles for free!",
79
+
80
+ // Personalize
81
+ colorFilters: "Color Grading",
82
+ bgBlur: "Depth of Field",
83
+ bgBlurDesc: "Professional lens blur effect.",
84
+ groupComp: "Group Composition",
85
+ compClassic: "Classic Formal",
86
+ compDynamic: "Candid Moment",
87
+ compCinematic: "Cinematic",
88
+ resolutionTitle: "Image Quality",
89
+ resStandard: "Standard",
90
+ resHigh: "High (4K)",
91
+ resUltra: "Ultra (8K)",
92
+ customInstruct: "Stylist Instructions",
93
+ customInstructPlaceholder: "e.g. 'Add a bouquet of red roses', 'Sunset beach background'...",
94
+ customInstructHint: "Tell our AI stylist specific details you want.",
95
+
96
+ // Actions
97
+ generating: "Designing...",
98
+ genWait: "Our AI stylist is working on your photos...",
99
+ genSelected: "Try This Style",
100
+ genCustom: "Try Custom Style",
101
+ genAll: "Generate All Styles",
102
+ confirmGenAll: "Generate previews for all 100+ styles? This takes a few minutes.",
103
+
104
+ // Results
105
+ gallery: "My Lookbook",
106
+ startOver: "New Customer",
107
+ holdCompare: "Compare Original",
108
+ sliderHint: "Drag to Compare",
109
+ compareMode: "PK Mode",
110
+ compareHint: "Select another style to compare",
111
+ exitCompare: "Exit PK",
112
+ original: "ORIGINAL",
113
+ watermark: "Romantic Life Photography",
114
+ videoBtn: "Video",
115
+ zipBtn: "Download All",
116
+ zipLoading: "Packing...",
117
+ videoLoading: "Recording...",
118
+ emptyGallery: "Your fitting results will appear here",
119
+ emptyGalleryDesc: "Select a style to see how you look in our latest collections.",
120
+ saveRecipe: "Save Recipe",
121
+
122
+ // Marketing & Lead Gen
123
+ bookNow: "Book Consultation",
124
+ bookStyle: "Book This Style",
125
+ genPoster: "Share Poster",
126
+ consultTitle: "Book Your Shoot",
127
+ consultDesc: "Leave your info, our consultant will contact you with an exclusive offer.",
128
+ formName: "Your Name",
129
+ formPhone: "Phone Number",
130
+ formWeChat: "WeChat ID (Optional)",
131
+ formDate: "Wedding Date (Optional)",
132
+ formBudget: "Budget Range",
133
+ formService: "Service Type",
134
+ budget1: "Under ¥5k",
135
+ budget2: "¥5k - ¥10k",
136
+ budget3: "¥10k - ¥20k",
137
+ budget4: "¥20k+",
138
+ service1: "Wedding Photography",
139
+ service2: "Artistic Portrait",
140
+ service3: "Family / Group",
141
+ formSubmit: "Submit Inquiry",
142
+ formSuccess: "Received! VIP Styles Unlocked!",
143
+ posterTitle: "Scan to Book",
144
+ posterBrand: "Romantic Life Photography",
145
+
146
+ // Features (Why Choose Us)
147
+ whyUsTitle: "Why Choose Romantic Life?",
148
+ whyUsDesc: "Experience the premier wedding photography service in Xiamen.",
149
+ feat1Title: "No Hidden Costs",
150
+ feat1Desc: "Transparent pricing. All raw files included.",
151
+ feat2Title: "Satisfaction Guaranteed",
152
+ feat2Desc: "Free reshoot if you are not satisfied.",
153
+ feat3Title: "1-on-1 Service",
154
+ feat3Desc: "Dedicated team for every couple.",
155
+ feat4Title: "Premium Gowns",
156
+ feat4Desc: "1000+ designer dresses to choose from.",
157
+
158
+ // Testimonials
159
+ reviewsTitle: "Client Love Notes",
160
+ reviewsDesc: "See what our happy couples have to say.",
161
+ review1: "The AI fitting saved us so much time! We knew exactly what we wanted before arriving. The actual shoot was even better.",
162
+ review1User: "Alice & Tom",
163
+ review2: "Highly recommend the 'Chinese Traditional' style. The team was professional and the photos are stunning.",
164
+ review2User: "Ms. Zhang",
165
+ review3: "No hidden fees, great service. The makeup artist is a magician! Loved every moment.",
166
+ review3User: "Emily W.",
167
+
168
+ // Footer
169
+ footerAddress: "No. 188, Huandao Road, Siming District, Xiamen",
170
+ footerPhone: "TEL: 0592-8888888",
171
+ footerRights: "© 2024 Romantic Life Photography. All Rights Reserved.",
172
+
173
+ // About Us
174
+ aboutUsBtn: "About Us",
175
+ aboutTitle: "About Romantic Life",
176
+ aboutStory: "Brand Story",
177
+ aboutStoryContent: "Founded in 2004, Xiamen Romantic Life Photography is a premier luxury wedding studio located on the scenic Huandao Road. With over 20 years of experience and serving 100,000+ couples, we combine artistic aesthetics with genuine emotion to capture timeless memories.",
178
+ aboutPhilosophy: "Our Philosophy",
179
+ aboutPhilosophyContent: "We reject assembly-line photography. Every shutter press is a tribute to love. This AI Fitting Room allows you to explore possibilities, but our dedicated team makes them reality.",
180
+ aboutLocation: "Prime Location",
181
+ aboutLocationContent: "Our 5000m² exclusive sea-view base offers private beaches, gardens, and diverse indoor scenes.",
182
+
183
+ // Video Settings
184
+ vidSettings: "Video Settings",
185
+ sectComposition: "Composition",
186
+ sectEffects: "Effects",
187
+ sectAudio: "Audio",
188
+ transEffect: "Transition",
189
+ transSpeed: "Speed",
190
+ textOverlay: "Title Overlay",
191
+ textOverlayDesc: "Show style name",
192
+ watermarkToggle: "Show Brand Watermark",
193
+ videoMusic: "Background Music",
194
+ musicNone: "None",
195
+ musicRomantic: "Romantic Piano",
196
+ musicCinematic: "Cinematic Strings",
197
+ musicUpbeat: "Upbeat Pop",
198
+ cancel: "Cancel",
199
+ genVideo: "Create Video",
200
+ vidFormat: "Format",
201
+ vidResolution: "Resolution",
202
+
203
+ // --- NEW: User Center & Admin ---
204
+ userCenterTitle: "User Center",
205
+ myPoints: "My Points",
206
+ vipStatus: "VIP Status",
207
+ vipActive: "Active",
208
+ vipInactive: "Inactive",
209
+ earnPoints: "Earn Points",
210
+ taskShare: "Share Poster",
211
+ taskInvite: "Invite Friends",
212
+ taskBook: "Book Shoot",
213
+ redeemRewards: "Redeem Rewards",
214
+ rewardVip: "Unlock All Styles (VIP)",
215
+ rewardCoupon: "¥100 Voucher",
216
+
217
+ // Admin & CRM
218
+ adminTitle: "Studio Admin Panel",
219
+ adminLoginTitle: "Staff Login",
220
+ adminPassword: "Password",
221
+ adminLoginBtn: "Login",
222
+ adminTabLeads: "Leads Management",
223
+ adminTabSettings: "App Settings",
224
+ adminTabUsers: "User Management",
225
+ adminTabStaff: "Staff & Permissions",
226
+ adminTotalLeads: "Total Leads",
227
+ adminNewLeads: "New Today",
228
+ adminExport: "Export CSV",
229
+ adminSave: "Save Settings",
230
+ adminPromoText: "Promo Banner Text",
231
+ adminContactPhone: "Contact Phone",
232
+ adminFooterAddr: "Studio Address",
233
+ adminInsights: "Insights",
234
+ adminHotLead: "🔥 High Intent",
235
+
236
+ statusNew: "New",
237
+ statusContacted: "Contacted",
238
+ statusBooked: "Booked",
239
+ actionDelete: "Delete",
240
+ actionUpdate: "Update Status",
241
+
242
+ // Auth
243
+ authLogin: "Login",
244
+ authRegister: "Register",
245
+ authReset: "Reset Password",
246
+ authPhonePlace: "Phone Number",
247
+ authNamePlace: "Your Name",
248
+ authPassPlace: "Password",
249
+ authNewPassPlace: "New Password",
250
+ authConfirmPassPlace: "Confirm Password",
251
+ authCodePlace: "Verification Code (SMS)",
252
+ authSendCode: "Get Code",
253
+ authCodeSent: "Code sent: ",
254
+ authPassMismatch: "Passwords do not match!",
255
+ authNoAccount: "No account? Register",
256
+ authHasAccount: "Have an account? Login",
257
+ authForgotPass: "Forgot Password?",
258
+ authSubmit: "Submit",
259
+ authWelcome: "Welcome!",
260
+ authResetSuccess: "Password reset successful! Please login.",
261
+ authPhoneNotFound: "Phone number not registered.",
262
+
263
+ // User Management
264
+ userPointsEdit: "Edit Points",
265
+ userVipToggle: "VIP Access",
266
+
267
+ // Staff Management
268
+ staffAddBtn: "Add Staff",
269
+ staffSearchPlace: "Enter user phone...",
270
+ staffPromote: "Promote to Staff",
271
+ staffRole: "Role",
272
+ permExport: "Can Export Data",
273
+ permDelete: "Can Delete Data",
274
+ permSettings: "Can Edit Settings",
275
+ permUsers: "Can Manage Users",
276
+
277
+ // Toasts
278
+ toastCopy: "Link copied to clipboard!",
279
+ toastSaved: "Settings saved successfully!",
280
+ toastWelcome: "Welcome back, Admin!",
281
+ toastDeleted: "Lead deleted.",
282
+ toastPromoted: "User promoted to Staff!",
283
+ toastUpdated: "Permissions updated!",
284
+ toastFeedback: "Thank you for your feedback!",
285
+
286
+ // Feedback
287
+ feedbackTitle: "User Feedback",
288
+ feedbackDesc: "We value your opinion. Help us improve.",
289
+ feedbackTypeBug: "Bug Report",
290
+ feedbackTypeSugg: "Suggestion",
291
+ feedbackTypeOther: "Other",
292
+ feedbackContentPlace: "Please describe your issue or suggestion...",
293
+ feedbackSubmit: "Submit Feedback",
294
+ feedbackRating: "Rate your experience",
295
+
296
+ // My Bookings
297
+ tabProfile: "Profile",
298
+ tabBookings: "My Bookings",
299
+ noBookings: "No bookings yet.",
300
+
301
+ // Tips & Nudges
302
+ genTips: [
303
+ "Did you know? We have a private beach for sunset shoots.",
304
+ "Tip: Match your accessories to the style for best results.",
305
+ "Our makeup artists are trained in Korean & Japanese styles.",
306
+ "Fun Fact: We've shot over 10,000 couples at the Botanical Garden.",
307
+ "Relax your shoulders for a more natural pose.",
308
+ "We provide fresh flowers for all garden shoots."
309
+ ],
310
+ pointNudgeDownload: "+10 Pts",
311
+ pointNudgeShare: "Share for +10 Pts!",
312
+
313
+ // New Live Effects
314
+ liveEffect: "Live Photo",
315
+ effectBreath: "Breathing",
316
+ effectGlitch: "Glitch",
317
+ effectParticles: "Particles",
318
+
319
+ // New Subject Types
320
+ subjType: "Subject",
321
+ subjFemale: "Bride / Female",
322
+ subjMale: "Groom / Male",
323
+ subjCouple: "Couple",
324
+ subjChild: "Child",
325
+ subjFamily: "Family",
326
+
327
+ // Style Names
328
+ styles: {
329
+ korean: "Korean Minimalist",
330
+ british: "British Royal",
331
+ euro_american: "Western Vogue",
332
+ hollywood: "Classic Hollywood",
333
+ chinese: "Chinese Traditional",
334
+ gufeng: "Ancient Hanfu",
335
+ japanese: "Japanese Airy",
336
+ ethnic: "Ethnic Vibes",
337
+ exotic: "Exotic Morocco",
338
+ cinematic: "Cinematic Mood",
339
+ film: "Vintage Film",
340
+ blackwhite: "Classic B&W",
341
+ artistic: "Fine Art",
342
+ illustration: "Illustration",
343
+ oil_painting: "Oil Painting",
344
+ anime: "Anime Dream",
345
+ glitch: "Cyber Glitch",
346
+ ins: "Insta Chic",
347
+ magazine: "Magazine Cover",
348
+ minimalist: "Modern Minimalist",
349
+ fashion: "High Fashion",
350
+ retro: "Retro HK",
351
+ literary: "Literary",
352
+ campus: "Campus Love",
353
+ fairytale: "Fairy Tale",
354
+ sweet: "Sweet Pink",
355
+ story: "Love Story",
356
+ documentary: "Documentary",
357
+ fresh: "Nature Fresh",
358
+ garden: "Secret Garden",
359
+ sexy: "Glamour",
360
+ personality: "Unique Pop",
361
+ bohemian: "Bohemian",
362
+ art_deco: "Art Deco",
363
+ masquerade: "Masquerade Ball",
364
+ steampunk: "Steampunk",
365
+ galactic: "Galactic",
366
+ cyberpunk: "Cyberpunk",
367
+ gothic: "Gothic Romance",
368
+ beach: "Island Beach",
369
+ winter: "Winter Snow",
370
+ underwater: "Underwater",
371
+ rustic: "Rustic Barn",
372
+ elf: "Elven Magic",
373
+ indian: "Indian Luxury",
374
+ thai: "Thai Royal",
375
+ double_exposure: "Double Exposure",
376
+
377
+ // Douyin Viral
378
+ old_money: "Old Money Aesthetic",
379
+ phoenix_crown: "Phoenix Crown",
380
+ balletcore: "Balletcore",
381
+ dopamine: "Dopamine",
382
+ clean_fit: "Clean Fit",
383
+
384
+ // Groom Styles
385
+ groom_classic: "Classic Tuxedo",
386
+ groom_modern: "Modern Suit",
387
+ groom_hanfu: "Scholar Hanfu",
388
+ groom_retro: "Retro Gent",
389
+ groom_cyber: "Cyber Warrior",
390
+
391
+ // Child Styles
392
+ child_prince: "Little Prince",
393
+ child_princess: "Little Princess",
394
+ child_suit: "Mini Gentleman",
395
+ child_traditional: "Eastern Kid",
396
+ child_fairy: "Forest Fairy",
397
+
398
+ // NEW 38 STYLES
399
+ mermaid: "Mermaid Core",
400
+ angel: "Angel Wings",
401
+ demon: "Dark Fantasy",
402
+ vampire: "Vampire Gothic",
403
+ victorian: "Victorian Era",
404
+ renaissance: "Renaissance Art",
405
+ egyptian: "Ancient Egypt",
406
+ greek: "Greek Goddess",
407
+ roman: "Roman Holiday",
408
+ viking: "Nordic Viking",
409
+ y2k: "Y2K Millennium",
410
+ neon: "Neon Lights",
411
+ denim: "Denim Casual",
412
+ suit_bride: "Bridal Suit",
413
+ sketch: "Pencil Sketch",
414
+ pixel: "Pixel Art",
415
+ clay: "Claymation",
416
+ pop_art: "Pop Art",
417
+ autumn: "Golden Autumn",
418
+ sakura: "Cherry Blossom",
419
+ rainforest: "Tropical Rainforest",
420
+ barbie: "Pink Doll",
421
+ wes: "Pastel Symmetry",
422
+ cottagecore: "Cottagecore",
423
+ dark_academia: "Dark Academia",
424
+ light_academia: "Light Academia",
425
+ mob_wife: "Mob Wife Aesthetic",
426
+ all_red: "Monochrome Red",
427
+ all_black: "Gothic Black",
428
+ pastel: "Pastel Dream",
429
+ cyber_angel: "Cybernetic Angel",
430
+ hanbok: "Korean Hanbok",
431
+ kimono: "Japanese Kimono",
432
+ cheongsam: "Qipao",
433
+ cowboy: "Western Cowboy",
434
+ disco: "70s Disco",
435
+ jazz: "Jazz Bar",
436
+ sporty: "Sporty Chic"
437
+ }
438
+ };
439
+
440
+ export type TranslationType = typeof en;
441
+
442
+ const zh: TranslationType = {
443
+ title: "厦门浪漫一生",
444
+ subtitle: "婚纱摄影 AI 试衣间",
445
+ heroTitle: "在线试衣 预见你的美",
446
+ heroDesc: "上传照片,一键体验100+种全球热门婚纱风格。先试穿,再拍摄,所见即所得。",
447
+ beta: "AI 拓客版",
448
+ promoBanner: "🎉 春季婚博会限时活动:现在预约进店,赠送“明星级”试妆一次 + 500元现金抵用券!",
449
+ promoEnds: "距活动结束仅剩",
450
+ tagHot: "爆款",
451
+ tagNew: "新品",
452
+ tagRec: "店长推荐",
453
+ floatConsult: "在线咨询",
454
+
455
+ // PDD Viral
456
+ pddSlashBtn: "免费拿",
457
+ pddSlashTitle: "0元解锁 VIP风格",
458
+ pddSlashDesc: "邀请1位好友助力,直接砍到 0元 拿走!",
459
+ pddSlashProgress: "已砍 98.9%...",
460
+ pddSlashCta: "喊好友砍一刀",
461
+ pddSlashSuccess: "砍价成功!",
462
+ pddRedPacketTitle: "恭喜!",
463
+ pddRedPacketDesc: "您获得一个婚纱照现金红包",
464
+ pddRedPacketCta: "立即拆开",
465
+ pddWithdrawTitle: "提现 2000元",
466
+ pddWithdrawDesc: "再分享给 2个 群,即可提现到账!",
467
+ pddGroupJoin: "去拼团",
468
+ pddGroupJoined: "人已拼",
469
+
470
+ aiScanning: "AI 智能面部扫描中...",
471
+ aiAnalyzing: "正在分析五官骨相与肤色...",
472
+ aiMatching: "正在匹配最适合您的婚纱风格...",
473
+ aiBestMatch: "AI 契合度 98%",
474
+ tickerBooked: "预约了",
475
+ tickerJustNow: "刚刚",
476
+ tickerMinsAgo: "分钟前",
477
+
478
+ // Analysis Modal (NEW - CHINESE)
479
+ analysisTitle: "AI 形象诊断报告",
480
+ analysisSubtitle: "面部生物美学分析",
481
+ lblFaceShape: "面部轮廓",
482
+ lblSkinTone: "肤色质感",
483
+ lblRecVibe: "推荐风格气质",
484
+ aiConfidence: "AI 匹配置信度",
485
+
486
+ step1: "上传客照",
487
+ step2: "选择风格",
488
+ step3: "定制细节",
489
+ uploadTitle: "上传您的照片",
490
+ uploadDesc: "支持自拍或生活照,五官清晰效果最佳",
491
+ changePhoto: "更换照片",
492
+ styleSearchPlace: "搜索风格...",
493
+ customStyle: "自定义风格",
494
+ customStyleDesc: "上传网图/样片",
495
+ luckyStyle: "手气不错",
496
+ luckyStyleDesc: "随机匹配一款风格",
497
+ clearSelection: "清除选择",
498
+ usingCustom: "使用自定义样片",
499
+ usingCustomDesc: "我们将复刻这张样片的风格。",
500
+ sectFavorites: "我的收藏", // NEW
501
+ browseAll: "浏览全部风格", // NEW
502
+ viewCategories: "查看分类", // NEW
503
+
504
+ vipLocked: "VIP 专享",
505
+ vipUnlockTitle: "解锁 VIP 风格",
506
+ vipUnlockDesc: "简单注册/留资,即可免费解锁全部高级风格!",
507
+
508
+ colorFilters: "后期滤镜",
509
+ bgBlur: "大光圈虚化",
510
+ bgBlurDesc: "模拟单反镜头景深效果",
511
+ groupComp: "多人拍摄模式",
512
+ compClassic: "经典合影",
513
+ compDynamic: "互动抓拍",
514
+ compCinematic: "电影叙事",
515
+ resolutionTitle: "画质选择",
516
+ resStandard: "标准 (HD)",
517
+ resHigh: "高清 (4K)",
518
+ resUltra: "超清 (8K)",
519
+ customInstruct: "修图师指令",
520
+ customInstructPlaceholder: "例如:'要手捧红玫瑰','背景换成鼓浪屿海边'...",
521
+ customInstructHint: "像告诉修图师一样告诉AI您的需求。",
522
+ generating: "正在设计中...",
523
+ genWait: "AI修图师正在为您生成样片,请稍候...",
524
+ genSelected: "生成该风格",
525
+ genCustom: "生成自定义",
526
+ genAll: "一键生成全套样片",
527
+ confirmGenAll: "确定生成全套100+种风格吗?这将制作您的专属电子相册。",
528
+ gallery: "我的试衣间",
529
+ startOver: "接待下一位",
530
+ holdCompare: "对比原图",
531
+ sliderHint: "拖动对比",
532
+ compareMode: "风格PK",
533
+ compareHint: "请点击下方选择另一张进行对比",
534
+ exitCompare: "退出PK",
535
+ original: "原图",
536
+ watermark: "厦门浪漫一生婚纱摄影",
537
+ videoBtn: "生成视频",
538
+ zipBtn: "打包下载",
539
+ zipLoading: "打包中...",
540
+ videoLoading: "渲染中...",
541
+ emptyGallery: "试衣效果将在这里显示",
542
+ emptyGalleryDesc: "选择左侧风格,为客户展示拍摄效果。",
543
+ saveRecipe: "保存配方",
544
+
545
+ // Marketing & Lead Gen
546
+ bookNow: "预约进店",
547
+ bookStyle: "拍这套",
548
+ genPoster: "生成种草海报",
549
+ consultTitle: "预约拍摄档期",
550
+ consultDesc: "留下您的联系方式,获取本季最新优惠报价。",
551
+ formName: "您的称呼",
552
+ formPhone: "联系电话",
553
+ formWeChat: "微信号 (选填)",
554
+ formDate: "婚期/拍摄日期 (选填)",
555
+ formBudget: "预算范围",
556
+ formService: "拍摄类型",
557
+ budget1: "5000元以下",
558
+ budget2: "5000 - 10000元",
559
+ budget3: "10000 - 20000元",
560
+ budget4: "20000元以上",
561
+ service1: "婚纱摄影",
562
+ service2: "艺术写真",
563
+ service3: "全家福/多人",
564
+ formSubmit: "提交预约,解锁VIP",
565
+ formSuccess: "预约成功!VIP风格已解锁!",
566
+ posterTitle: "扫码咨询",
567
+ posterBrand: "厦门浪漫一生婚纱摄影",
568
+
569
+ // Features
570
+ whyUsTitle: "为什么选择浪漫一生?",
571
+ whyUsDesc: "专注高端婚纱摄影20年,服务超10万对新人。",
572
+ feat1Title: "无隐形消费",
573
+ feat1Desc: "价格透明,所有底片全送。",
574
+ feat2Title: "不满意重拍",
575
+ feat2Desc: "拍摄不满意无条件重拍,直到满意为止。",
576
+ feat3Title: "一客一选",
577
+ feat3Desc: "专属团队一对一服务,拒绝流水线。",
578
+ feat4Title: "高定礼服",
579
+ feat4Desc: "全馆1000+套品牌礼服任选。",
580
+
581
+ // Testimonials
582
+ reviewsTitle: "客户真实评价",
583
+ reviewsDesc: "听听我们的新人怎么说",
584
+ review1: "AI试衣太方便了,不用去店里就能选好风格,省了一整天时间!",
585
+ review1User: "林小姐 & 张先生",
586
+ review2: "极力推荐‘中式喜嫁’风格,非常喜庆,家人都很喜欢。服务团队也很专业。",
587
+ review2User: "张女士",
588
+ review3: "没有任何隐形消费,化妆师技术超好,成片效果比AI生成的还要惊艳!",
589
+ review3User: "Emily W.",
590
+
591
+ // Footer
592
+ footerAddress: "厦门市思明区环岛路188号",
593
+ footerPhone: "咨询热线: 0592-8888888",
594
+ footerRights: "© 2024 厦门浪漫一生婚纱摄影 版权所有",
595
+
596
+ // About Us
597
+ aboutUsBtn: "关于我们",
598
+ aboutTitle: "关于浪漫一生",
599
+ aboutStory: "品牌故事",
600
+ aboutStoryContent: "厦门浪漫一生婚纱摄影成立于2004年,是坐落于环岛路的高端婚纱摄影品牌。20年来,我们服务了超过10万对新人。我们坚持用镜头捕捉最真实的情感,将艺术美学与幸福瞬间完美融合。",
601
+ aboutPhilosophy: "服务理念",
602
+ aboutPhilosophyContent: "我们拒绝流水线式的拍摄。每一次快门都是对爱情的礼赞。这个AI试衣间只是开始,我们专业的摄影团队将把无限的想象变为触手可及的现实。",
603
+ aboutLocation: "黄金地段",
604
+ aboutLocationContent: "我们拥有5000平米独家海景基地,私人沙滩、花园及多样化室内实景,满足您对婚纱照的所有幻想。",
605
+
606
+ vidSettings: "视频参数",
607
+ sectComposition: "画面构图",
608
+ sectEffects: "特效调整",
609
+ sectAudio: "音频配乐",
610
+ transEffect: "转场",
611
+ transSpeed: "速度",
612
+ textOverlay: "风格字幕",
613
+ textOverlayDesc: "显示风格名称",
614
+ watermarkToggle: "显示品牌水印",
615
+ videoMusic: "背景音乐",
616
+ musicNone: "无音乐",
617
+ musicRomantic: "浪漫钢琴",
618
+ musicCinematic: "电影弦乐",
619
+ musicUpbeat: "欢快流行",
620
+ cancel: "取消",
621
+ genVideo: "导出视频",
622
+ vidFormat: "视频格式",
623
+ vidResolution: "分辨率",
624
+
625
+ // User Center & Admin
626
+ userCenterTitle: "用户中心",
627
+ myPoints: "我的积分",
628
+ vipStatus: "VIP 状态",
629
+ vipActive: "已激活",
630
+ vipInactive: "未激活",
631
+ earnPoints: "赚取积分",
632
+ taskShare: "分享海报 (+10)",
633
+ taskInvite: "邀请好友 (+50)",
634
+ taskBook: "预约拍摄 (+100)",
635
+ redeemRewards: "积分兑换",
636
+ rewardVip: "解锁全套风格 (VIP)",
637
+ rewardCoupon: "100元现金券",
638
+
639
+ // Admin CRM
640
+ adminTitle: "影楼管理后台",
641
+ adminLoginTitle: "员工登录",
642
+ adminPassword: "密码",
643
+ adminLoginBtn: "登录",
644
+ adminTabLeads: "客资管理",
645
+ adminTabSettings: "软件设置",
646
+ adminTabUsers: "用户管理",
647
+ adminTabStaff: "员工权限",
648
+ adminTotalLeads: "总客资数",
649
+ adminNewLeads: "今日新增",
650
+ adminExport: "导出 CSV",
651
+ adminSave: "保存设置",
652
+ adminPromoText: "顶部活动文案",
653
+ adminContactPhone: "联系电话",
654
+ adminFooterAddr: "店铺地址",
655
+ adminInsights: "AI画像分析",
656
+ adminHotLead: "🔥 高意向",
657
+
658
+ statusNew: "新客",
659
+ statusContacted: "已联系",
660
+ statusBooked: "已成交",
661
+ actionDelete: "删除",
662
+ actionUpdate: "更新状态",
663
+
664
+ // Auth
665
+ authLogin: "用户登录",
666
+ authRegister: "注册会员",
667
+ authReset: "重置密码",
668
+ authPhonePlace: "手机号码",
669
+ authNamePlace: "您的称呼",
670
+ authPassPlace: "输入密码",
671
+ authNewPassPlace: "设置新密码",
672
+ authConfirmPassPlace: "确认密码",
673
+ authCodePlace: "短信验证码",
674
+ authSendCode: "获取验证码",
675
+ authCodeSent: "验证码已发送: ",
676
+ authPassMismatch: "两次密码不一致!",
677
+ authNoAccount: "没有账号?去注册",
678
+ authHasAccount: "已有账号?去登录",
679
+ authForgotPass: "忘记密码?",
680
+ authSubmit: "确定",
681
+ authWelcome: "欢迎回来!",
682
+ authResetSuccess: "密码重置成功!请重新登录。",
683
+ authPhoneNotFound: "手机号未注册",
684
+
685
+ // User Management
686
+ userPointsEdit: "修改积分",
687
+ userVipToggle: "VIP权限",
688
+
689
+ // Staff Management
690
+ staffAddBtn: "添加员工",
691
+ staffSearchPlace: "输入用户手机号...",
692
+ staffPromote: "设为员工",
693
+ staffRole: "角色",
694
+ permExport: "导出数据",
695
+ permDelete: "删除数据",
696
+ permSettings: "修改设置",
697
+ permUsers: "管理用户",
698
+
699
+ toastCopy: "链接已复制!",
700
+ toastSaved: "设置已保存!",
701
+ toastWelcome: "欢迎回来,店长!",
702
+ toastDeleted: "客资已删除。",
703
+ toastPromoted: "已成功设为员工!",
704
+ toastUpdated: "权限已更新!",
705
+ toastFeedback: "感谢您的反馈!",
706
+
707
+ // Feedback
708
+ feedbackTitle: "用户反馈",
709
+ feedbackDesc: "您的意见是我们进步的动力",
710
+ feedbackTypeBug: "功能故障",
711
+ feedbackTypeSugg: "产品建议",
712
+ feedbackTypeOther: "其他问题",
713
+ feedbackContentPlace: "请详细描述您遇到的问题或建议...",
714
+ feedbackSubmit: "提交反馈",
715
+ feedbackRating: "您对本次体验满意吗?",
716
+
717
+ // My Bookings
718
+ tabProfile: "个人资料",
719
+ tabBookings: "我的预约",
720
+ noBookings: "暂无预约记录。",
721
+
722
+ genTips: [
723
+ "您知道吗?我们拥有环岛路私人海滩拍摄基地。",
724
+ "小贴士:搭配与风格相符的配���效果更佳哦。",
725
+ "我们的化妆师团队均定期赴韩、日进修。",
726
+ "有趣的事实:我们在植物园已经拍摄了超过一万对新人。",
727
+ "放松肩膀,您的体态会更加自然优雅。",
728
+ "花园拍摄我们提供当日空运的鲜花道具。"
729
+ ],
730
+ pointNudgeDownload: "+10 积分",
731
+ pointNudgeShare: "分享赚 +10 积分!",
732
+
733
+ // New Live Effects
734
+ liveEffect: "动态照片",
735
+ effectBreath: "呼吸",
736
+ effectGlitch: "故障",
737
+ effectParticles: "粒子",
738
+
739
+ // New Subject Types
740
+ subjType: "主角类型",
741
+ subjFemale: "新娘/女士",
742
+ subjMale: "新郎/男士",
743
+ subjCouple: "情侣/夫妻",
744
+ subjChild: "儿童/宝宝",
745
+ subjFamily: "全家福",
746
+
747
+ styles: {
748
+ korean: "韩式唯美",
749
+ british: "英伦皇室",
750
+ euro_american: "欧美时尚",
751
+ hollywood: "好莱坞大片",
752
+ chinese: "中式喜嫁",
753
+ gufeng: "古风汉服",
754
+ japanese: "日系小清新",
755
+ ethnic: "民族风情",
756
+ exotic: "异域风情",
757
+ cinematic: "电影质感",
758
+ film: "胶片复古",
759
+ blackwhite: "经典黑白",
760
+ artistic: "艺术画意",
761
+ illustration: "手绘插画",
762
+ oil_painting: "古典油画",
763
+ anime: "唯美动漫",
764
+ glitch: "故障艺术",
765
+ ins: "Ins网红风",
766
+ magazine: "杂志大片",
767
+ minimalist: "极简主义",
768
+ fashion: "潮流街拍",
769
+ retro: "复古港风",
770
+ literary: "文艺青年",
771
+ campus: "校园回忆",
772
+ fairytale: "迪士尼童话",
773
+ sweet: "甜美可爱",
774
+ story: "故事剧情",
775
+ documentary: "纪实抓拍",
776
+ fresh: "森系氧气",
777
+ garden: "莫奈花园",
778
+ sexy: "性感私房",
779
+ personality: "个性搞怪",
780
+ bohemian: "波西米亚",
781
+ art_deco: "盖茨比奢华",
782
+ masquerade: "假面舞会",
783
+ steampunk: "蒸汽朋克",
784
+ galactic: "银河科幻",
785
+ cyberpunk: "赛博朋克",
786
+ gothic: "暗黑哥特",
787
+ beach: "海岛旅拍",
788
+ winter: "冰雪奇缘",
789
+ underwater: "唯美水下",
790
+ rustic: "美式庄园",
791
+ elf: "梦幻精灵",
792
+ indian: "印度风情",
793
+ thai: "泰式风情",
794
+ double_exposure: "双重曝光",
795
+
796
+ // Douyin Viral
797
+ old_money: "老钱风",
798
+ phoenix_crown: "凤冠霞帔",
799
+ balletcore: "芭蕾风",
800
+ dopamine: "多巴胺",
801
+ clean_fit: "极简智性",
802
+
803
+ // Groom Styles
804
+ groom_classic: "经典黑西装",
805
+ groom_modern: "现代轻奢",
806
+ groom_hanfu: "书卷汉服",
807
+ groom_retro: "复古绅士",
808
+ groom_cyber: "赛博战士",
809
+
810
+ // Child Styles
811
+ child_prince: "在逃小王子",
812
+ child_princess: "迪士尼公主",
813
+ child_suit: "小小绅士",
814
+ child_traditional: "国潮萌娃",
815
+ child_fairy: "森林小精灵",
816
+
817
+ // NEW 38 STYLES (CHINESE)
818
+ mermaid: "人鱼传说",
819
+ angel: "天使之翼",
820
+ demon: "暗黑恶魔",
821
+ vampire: "暮光之城",
822
+ victorian: "维多利亚",
823
+ renaissance: "文艺复兴",
824
+ egyptian: "埃及艳后",
825
+ greek: "希腊女神",
826
+ roman: "罗马假日",
827
+ viking: "北欧维京",
828
+ y2k: "Y2K千禧风",
829
+ neon: "霓虹光影",
830
+ denim: "丹宁牛仔",
831
+ suit_bride: "新娘西装",
832
+ sketch: "素描手绘",
833
+ pixel: "像素艺术",
834
+ clay: "粘土动画",
835
+ pop_art: "波普艺术",
836
+ autumn: "金秋童话",
837
+ sakura: "樱花恋人",
838
+ rainforest: "热带雨林",
839
+ barbie: "芭比粉红",
840
+ wes: "韦斯安德森",
841
+ cottagecore: "田园牧歌",
842
+ dark_academia: "暗黑学院",
843
+ light_academia: "明亮学院",
844
+ mob_wife: "黑帮大嫂",
845
+ all_red: "纯红视觉",
846
+ all_black: "纯黑哥特",
847
+ pastel: "马卡龙梦境",
848
+ cyber_angel: "机械天使",
849
+ hanbok: "传统韩服",
850
+ kimono: "传统和服",
851
+ cheongsam: "海派旗袍",
852
+ cowboy: "西部牛仔",
853
+ disco: "复古迪斯科",
854
+ jazz: "爵士名伶",
855
+ sporty: "运动活力"
856
+ }
857
+ };
858
+
859
+ const createTranslation = (overrides: Partial<TranslationType> & { styles?: Partial<TranslationType['styles']> }): TranslationType => {
860
+ return {
861
+ ...en,
862
+ ...overrides,
863
+ styles: {
864
+ ...en.styles,
865
+ ...overrides.styles,
866
+ } as TranslationType['styles']
867
+ };
868
+ };
869
+
870
+ const ja = createTranslation({
871
+ title: "Romantic Life",
872
+ subtitle: "Wedding Studio",
873
+ styleSearchPlace: "スタイルを検索...",
874
+ customStyle: "カスタムスタイル",
875
+ luckyStyle: "ラッキーピック",
876
+ luckyStyleDesc: "スタイルをランダムに選択",
877
+ compareMode: "比較モード",
878
+ compareHint: "別のスタイルを選択してください",
879
+ exitCompare: "終了",
880
+ adminInsights: "AI分析",
881
+ adminHotLead: "🔥 高関心",
882
+ authReset: "パスワードのリセット",
883
+ authNewPassPlace: "新しいパスワード",
884
+ authCodePlace: "認証コード",
885
+ authSendCode: "コードを送信",
886
+ authForgotPass: "パスワードをお忘れですか?",
887
+ sectFavorites: "お気に入り", // NEW
888
+ browseAll: "すべて表示", // NEW
889
+ viewCategories: "カテゴリ表示", // NEW
890
+ });
891
+
892
+ const ko = createTranslation({
893
+ title: "Romantic Life",
894
+ subtitle: "Wedding Studio",
895
+ styleSearchPlace: "스타일 검색...",
896
+ customStyle: "사용자 정의 스타일",
897
+ luckyStyle: "럭키 픽",
898
+ luckyStyleDesc: "스타일 랜덤 선택",
899
+ compareMode: "비교 모드",
900
+ compareHint: "비교할 다른 스타일을 선택하세요",
901
+ exitCompare: "나가기",
902
+ adminInsights: "AI 인사이트",
903
+ adminHotLead: "🔥 높은 관심",
904
+ authReset: "비밀번호 재설정",
905
+ authNewPassPlace: "새 비밀번호",
906
+ authCodePlace: "인증 코드",
907
+ authSendCode: "코드 보내기",
908
+ authForgotPass: "비밀번호를 잊으셨나요?",
909
+ sectFavorites: "즐겨찾기", // NEW
910
+ browseAll: "전체 스타일 보기", // NEW
911
+ viewCategories: "카테고리 보기", // NEW
912
+ });
913
+
914
+ const es = createTranslation({
915
+ title: "Romantic Life",
916
+ subtitle: "Wedding Studio",
917
+ styleSearchPlace: "Buscar estilos...",
918
+ customStyle: "Estilo personalizado",
919
+ luckyStyle: "Elección de suerte",
920
+ luckyStyleDesc: "Selección de estilo aleatorio",
921
+ compareMode: "Modo Comparar",
922
+ compareHint: "Selecciona otro estilo",
923
+ exitCompare: "Salir",
924
+ adminInsights: "Insights IA",
925
+ adminHotLead: "🔥 Alto Interés",
926
+ authReset: "Restablecer contraseña",
927
+ authNewPassPlace: "Nueva contraseña",
928
+ authCodePlace: "Código de verificación",
929
+ authSendCode: "Enviar código",
930
+ authForgotPass: "¿Olvidaste tu contraseña?",
931
+ sectFavorites: "Mis Favoritos", // NEW
932
+ browseAll: "Ver todos", // NEW
933
+ viewCategories: "Ver categorías", // NEW
934
+ });
935
+
936
+ const fr = createTranslation({
937
+ title: "Romantic Life",
938
+ subtitle: "Wedding Studio",
939
+ styleSearchPlace: "Rechercher des styles...",
940
+ customStyle: "Style personnalisé",
941
+ luckyStyle: "Choix chanceux",
942
+ luckyStyleDesc: "Sélection de style aléatoire",
943
+ compareMode: "Mode Comparaison",
944
+ compareHint: "Sélectionnez un autre style",
945
+ exitCompare: "Quitter",
946
+ adminInsights: "Aperçu IA",
947
+ adminHotLead: "🔥 Fort Intérêt",
948
+ authReset: "Réinitialiser le mot de passe",
949
+ authNewPassPlace: "Nouveau mot de passe",
950
+ authCodePlace: "Code de vérification",
951
+ authSendCode: "Envoyer le code",
952
+ authForgotPass: "Mot de passe oublié ?",
953
+ sectFavorites: "Mes Favoris", // NEW
954
+ browseAll: "Tout afficher", // NEW
955
+ viewCategories: "Voir catégories", // NEW
956
+ });
957
+
958
+ export const TRANSLATIONS: Record<Language, TranslationType> = {
959
+ en,
960
+ zh,
961
+ ja,
962
+ ko,
963
+ es,
964
+ fr
965
+ };
index.css ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ /* Custom Global Styles */
6
+ body {
7
+ background-color: #fdfdfd;
8
+ -webkit-tap-highlight-color: transparent;
9
+ overscroll-behavior-y: none;
10
+ }
11
+ .custom-scrollbar::-webkit-scrollbar {
12
+ width: 6px;
13
+ height: 6px;
14
+ }
15
+ .custom-scrollbar::-webkit-scrollbar-track {
16
+ background: transparent;
17
+ }
18
+ .custom-scrollbar::-webkit-scrollbar-thumb {
19
+ background-color: rgba(244, 63, 94, 0.3);
20
+ border-radius: 20px;
21
+ }
22
+ .custom-scrollbar::-webkit-scrollbar-thumb:hover {
23
+ background-color: rgba(244, 63, 94, 0.6);
24
+ }
25
+
26
+ .animate-scan { animation: scan 2s linear infinite; }
27
+ .animate-swing { animation: swing 3s ease-in-out infinite; }
28
+
29
+ /* Live Effects Utility Classes */
30
+ .effect-breath { animation: breathing 5s ease-in-out infinite; }
31
+ .effect-glitch { animation: glitch 0.3s cubic-bezier(.25, .46, .45, .94) both infinite; }
32
+ .effect-particles::before {
33
+ content: '';
34
+ position: absolute;
35
+ top: 0; left: 0; width: 100%; height: 100%;
36
+ background-image: radial-gradient(white 1px, transparent 1px);
37
+ background-size: 20px 20px;
38
+ opacity: 0.3;
39
+ animation: float 3s linear infinite;
40
+ pointer-events: none;
41
+ }
42
+
43
+ /* Low Power Mode Disables */
44
+ body.low-power .effect-breath,
45
+ body.low-power .effect-glitch,
46
+ body.low-power .animate-scan {
47
+ animation: none !important;
48
+ }
index.html ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
6
+ <title>厦门浪漫一生 - AI 婚纱试衣间</title>
7
+ <meta name="description" content="AI Wedding Style Fitting Room - Xiamen Romantic Life Photography">
8
+ <meta name="theme-color" content="#f43f5e">
9
+
10
+ <!-- PWA Config -->
11
+ <link rel="manifest" href="/manifest.json">
12
+ <meta name="apple-mobile-web-app-capable" content="yes">
13
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
14
+ <meta name="apple-mobile-web-app-title" content="Romantic AI">
15
+ <script type="importmap">
16
+ {
17
+ "imports": {
18
+ "react": "https://aistudiocdn.com/react@^19.2.1",
19
+ "react-dom/": "https://aistudiocdn.com/react-dom@^19.2.1/",
20
+ "react/": "https://aistudiocdn.com/react@^19.2.1/",
21
+ "@google/genai": "https://aistudiocdn.com/@google/genai@^1.31.0",
22
+ "lucide-react": "https://aistudiocdn.com/lucide-react@^0.556.0",
23
+ "jszip": "https://aistudiocdn.com/jszip@^3.10.1",
24
+ "canvas-confetti": "https://aistudiocdn.com/canvas-confetti@^1.9.4",
25
+ "zustand": "https://aistudiocdn.com/zustand@^5.0.9",
26
+ "zustand/": "https://aistudiocdn.com/zustand@^5.0.9/",
27
+ "vite": "https://aistudiocdn.com/vite@^7.2.6",
28
+ "@vitejs/plugin-react": "https://aistudiocdn.com/@vitejs/plugin-react@^5.1.1"
29
+ }
30
+ }
31
+ </script>
32
+ <link rel="stylesheet" href="/index.css">
33
+ </head>
34
+ <body>
35
+ <div id="root">
36
+ <!-- Loading Spinner for Initial Load -->
37
+ <div id="loading-spinner" style="display:flex; flex-direction:column; align-items:center; justify-content:center; height:100vh; background-color:#fdfdfd; position:fixed; top:0; left:0; width:100%; z-index:9999;">
38
+ <div style="width:40px; height:40px; border:4px solid #fecdd3; border-top-color:#f43f5e; border-radius:50%; animation:spin 1s linear infinite;"></div>
39
+ <p style="margin-top:16px; font-family:sans-serif; color:#fb7185; font-size:14px; font-weight:bold;">Loading...</p>
40
+ </div>
41
+ <style>@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }</style>
42
+ </div>
43
+ <script type="module" src="/index.tsx"></script>
44
+ </body>
45
+ </html>
index.tsx ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App';
4
+ import './index.css';
5
+ import { ErrorBoundary } from './components/ErrorBoundary';
6
+
7
+ const rootElement = document.getElementById('root');
8
+ if (!rootElement) {
9
+ throw new Error("Could not find root element to mount to");
10
+ }
11
+
12
+ // Register Service Worker for PWA support
13
+ if ('serviceWorker' in navigator) {
14
+ window.addEventListener('load', () => {
15
+ navigator.serviceWorker.register('/service-worker.js')
16
+ .then(registration => {
17
+ console.log('SW registered: ', registration);
18
+ })
19
+ .catch(registrationError => {
20
+ console.log('SW registration failed: ', registrationError);
21
+ });
22
+ });
23
+ }
24
+
25
+ const root = ReactDOM.createRoot(rootElement);
26
+ root.render(
27
+ <React.StrictMode>
28
+ <ErrorBoundary>
29
+ <App />
30
+ </ErrorBoundary>
31
+ </React.StrictMode>
32
+ );
manifest.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "厦门浪漫一生 AI 试衣间",
3
+ "short_name": "Romantic AI",
4
+ "description": "Xiamen Romantic Life Wedding Photography AI Fitting Room",
5
+ "start_url": "/",
6
+ "display": "standalone",
7
+ "background_color": "#ffffff",
8
+ "theme_color": "#f43f5e",
9
+ "orientation": "portrait",
10
+ "icons": [
11
+ {
12
+ "src": "https://api.iconify.design/lucide:camera.svg?color=%23f43f5e",
13
+ "sizes": "192x192",
14
+ "type": "image/svg+xml",
15
+ "purpose": "any maskable"
16
+ },
17
+ {
18
+ "src": "https://api.iconify.design/lucide:camera.svg?color=%23f43f5e",
19
+ "sizes": "512x512",
20
+ "type": "image/svg+xml",
21
+ "purpose": "any maskable"
22
+ }
23
+ ]
24
+ }
metadata.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "name": "Copy of Romantic Life - AI Wedding Fitting Room",
3
+ "description": "Xiamen Romantic Life Wedding Photography AI Fitting Room. Try different wedding styles instantly and book your photoshoot.",
4
+ "requestFramePermissions": []
5
+ }
nginx.conf ADDED
File without changes
package.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ {
3
+ "name": "romantic-life-ai-studio",
4
+ "private": true,
5
+ "version": "2.0.0",
6
+ "type": "module",
7
+ "scripts": {
8
+ "dev": "vite",
9
+ "build": "tsc && vite build",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "@google/genai": "^0.1.0",
14
+ "canvas-confetti": "^1.9.2",
15
+ "jszip": "^3.10.1",
16
+ "lucide-react": "^0.344.0",
17
+ "react": "^18.2.0",
18
+ "react-dom": "^18.2.0",
19
+ "zustand": "^4.5.2"
20
+ },
21
+ "devDependencies": {
22
+ "@types/canvas-confetti": "^1.6.4",
23
+ "@types/react": "^18.2.64",
24
+ "@types/react-dom": "^18.2.21",
25
+ "@vitejs/plugin-react": "^4.2.1",
26
+ "autoprefixer": "^10.4.18",
27
+ "postcss": "^8.4.35",
28
+ "tailwindcss": "^3.4.1",
29
+ "typescript": "^5.4.2",
30
+ "vite": "^5.1.5"
31
+ }
32
+ }
postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }