Spaces:
Paused
Paused
| import { create } from 'zustand'; | |
| import { persist, createJSONStorage } from 'zustand/middleware'; | |
| import { UserAccount, LeadData, AdminConfig, WeddingStyle, GeneratedResult, CompositionMode, Resolution, SubjectType, GenerationStatus, Language, PointHistory, FeedbackItem, GenerationLog } from './types'; | |
| import { TRANSLATIONS } from './constants/translations'; | |
| // --- Default Configuration --- | |
| const DEFAULT_CONFIG: AdminConfig = { | |
| promoText: "🎉 Spring Wedding Expo: Book online today and get a Free Makeup Trial + ¥500 Voucher!", | |
| promoEnds: new Date(Date.now() + 86400000).toISOString(), | |
| contactPhone: '0592-8888888', | |
| showBanner: true, | |
| footerAddress: "No. 188, Huandao Road, Siming District, Xiamen", | |
| logoUrl: "", // Default empty | |
| shareTitle: "Help me choose a wedding style!", | |
| shareDesc: "I found this amazing AI fitting room at Romantic Life.", | |
| shareImage: "", | |
| redPacketMax: 2000, | |
| slashDifficulty: 0.98, | |
| crmApiUrl: '', | |
| geminiApiKey: '', | |
| geminiApiUrl: '', // Default empty | |
| pointsShare: 10, | |
| pointsInvite: 50, | |
| pointsBook: 100, | |
| pointsVipCost: 100, | |
| // Updated to user's domain | |
| qrCodeUrl: "https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=https://xmlove520.dpdns.org" | |
| }; | |
| // --- USER STORE --- | |
| interface UserState { | |
| currentUser: UserAccount | undefined; | |
| allUsers: UserAccount[]; | |
| leads: LeadData[]; | |
| feedback: FeedbackItem[]; | |
| guestFavorites: string[]; // NEW: Favorites for non-logged in users | |
| logs: GenerationLog[]; // NEW: Analytics Logs | |
| // Actions | |
| login: (phone: string, password?: string) => { success: boolean; msg: string }; | |
| register: (phone: string, name: string, pass: string) => { success: boolean; msg: string }; | |
| logout: () => void; | |
| updateUser: (id: string, updates: Partial<UserAccount>) => void; | |
| resetPassword: (phone: string, newPass: string) => boolean; | |
| addPoints: (amount: number, reason: string) => void; | |
| toggleFavorite: (styleId: string) => void; | |
| // Admin Actions | |
| addLead: (lead: LeadData) => void; | |
| updateLeadStatus: (id: string, status: LeadData['status']) => void; | |
| deleteLead: (id: string) => void; | |
| setLeads: (leads: LeadData[]) => void; | |
| // Feedback Actions | |
| addFeedback: (item: FeedbackItem) => void; | |
| // Analytics Actions | |
| addLog: (log: GenerationLog) => void; | |
| } | |
| export const useUserStore = create<UserState>()( | |
| persist( | |
| (set, get) => ({ | |
| currentUser: undefined, | |
| allUsers: [], | |
| leads: [], | |
| feedback: [], | |
| guestFavorites: [], | |
| logs: [], | |
| login: (phone, password) => { | |
| const { allUsers } = get(); | |
| const user = allUsers.find(u => u.phone === phone); | |
| if (user) { | |
| if (user.password && password && user.password !== password) { | |
| return { success: false, msg: "Incorrect password" }; | |
| } | |
| set({ currentUser: user }); | |
| return { success: true, msg: `Welcome ${user.name}` }; | |
| } | |
| return { success: false, msg: "User not found" }; | |
| }, | |
| register: (phone, name, pass) => { | |
| const { allUsers } = get(); | |
| if (allUsers.find(u => u.phone === phone)) { | |
| return { success: false, msg: "Phone already registered" }; | |
| } | |
| const newUser: UserAccount = { | |
| id: 'user_' + Date.now(), | |
| name, | |
| phone, | |
| password: pass, | |
| points: 100, | |
| isVip: false, | |
| joinDate: Date.now(), | |
| history: [], | |
| role: 'user', | |
| redPacketBalance: 1980, | |
| slashProgress: {}, | |
| favorites: [] | |
| }; | |
| set({ allUsers: [...allUsers, newUser], currentUser: newUser }); | |
| return { success: true, msg: "Registration successful" }; | |
| }, | |
| logout: () => set({ currentUser: undefined }), | |
| updateUser: (id, updates) => { | |
| const { allUsers, currentUser } = get(); | |
| const newAllUsers = allUsers.map(u => u.id === id ? { ...u, ...updates } : u); | |
| let newCurrentUser = currentUser; | |
| if (currentUser && currentUser.id === id) { | |
| newCurrentUser = { ...currentUser, ...updates }; | |
| } | |
| set({ allUsers: newAllUsers, currentUser: newCurrentUser }); | |
| }, | |
| resetPassword: (phone, newPass) => { | |
| const { allUsers } = get(); | |
| const index = allUsers.findIndex(u => u.phone === phone); | |
| if (index !== -1) { | |
| const updatedUsers = [...allUsers]; | |
| updatedUsers[index] = { ...updatedUsers[index], password: newPass }; | |
| set({ allUsers: updatedUsers }); | |
| return true; | |
| } | |
| return false; | |
| }, | |
| addPoints: (amount, reason) => { | |
| const { currentUser, updateUser } = get(); | |
| if (!currentUser) return; | |
| const newHistory: PointHistory = { | |
| id: Date.now().toString(), | |
| action: 'share', | |
| points: amount, | |
| timestamp: Date.now(), | |
| desc: reason | |
| }; | |
| updateUser(currentUser.id, { | |
| points: currentUser.points + amount, | |
| history: [...currentUser.history, newHistory] | |
| }); | |
| }, | |
| toggleFavorite: (styleId) => { | |
| const { currentUser, updateUser, guestFavorites } = get(); | |
| if (currentUser) { | |
| // Update User Account | |
| const currentFavs = currentUser.favorites || []; | |
| const newFavs = currentFavs.includes(styleId) | |
| ? currentFavs.filter(id => id !== styleId) | |
| : [...currentFavs, styleId]; | |
| updateUser(currentUser.id, { favorites: newFavs }); | |
| } else { | |
| // Update Guest Favorites | |
| const newFavs = guestFavorites.includes(styleId) | |
| ? guestFavorites.filter(id => id !== styleId) | |
| : [...guestFavorites, styleId]; | |
| set({ guestFavorites: newFavs }); | |
| } | |
| }, | |
| // Lead Actions | |
| addLead: (lead) => set(state => ({ leads: [lead, ...state.leads] })), | |
| updateLeadStatus: (id, status) => set(state => ({ leads: state.leads.map(l => l.id === id ? { ...l, status } : l) })), | |
| deleteLead: (id) => set(state => ({ leads: state.leads.filter(l => l.id !== id) })), | |
| setLeads: (leads) => set({ leads }), | |
| // Feedback Actions | |
| addFeedback: (item) => set(state => ({ feedback: [item, ...state.feedback] })), | |
| // Analytics | |
| addLog: (log) => set(state => ({ logs: [log, ...state.logs].slice(0, 2000) })), // Keep last 2000 logs | |
| }), | |
| { | |
| name: 'rl_user_storage', | |
| partialize: (state) => ({ | |
| allUsers: state.allUsers, | |
| leads: state.leads, | |
| currentUser: state.currentUser, | |
| feedback: state.feedback, | |
| guestFavorites: state.guestFavorites, | |
| logs: state.logs | |
| }), | |
| } | |
| ) | |
| ); | |
| // --- UI STORE --- | |
| interface UIState { | |
| language: Language; | |
| adminConfig: AdminConfig; | |
| // Modals Visibility | |
| modals: { | |
| auth: boolean; | |
| userCenter: boolean; | |
| admin: boolean; | |
| about: boolean; | |
| feedback: boolean; | |
| share: boolean; | |
| consult: boolean; | |
| analysis: boolean; | |
| redPacket: boolean; | |
| slash: boolean; | |
| }; | |
| // Actions | |
| setLanguage: (lang: Language) => void; | |
| setAdminConfig: (config: AdminConfig) => void; | |
| toggleModal: (modal: keyof UIState['modals'], isOpen: boolean) => void; | |
| closeAllModals: () => void; | |
| // Toast | |
| toastMsg: string | null; | |
| showToast: (msg: string) => void; | |
| } | |
| export const useUIStore = create<UIState>()( | |
| persist( | |
| (set) => ({ | |
| language: 'zh', | |
| adminConfig: DEFAULT_CONFIG, | |
| modals: { | |
| auth: false, | |
| userCenter: false, | |
| admin: false, | |
| about: false, | |
| feedback: false, | |
| share: false, | |
| consult: false, | |
| analysis: false, | |
| redPacket: false, | |
| slash: false, | |
| }, | |
| toastMsg: null, | |
| setLanguage: (lang) => set({ language: lang }), | |
| setAdminConfig: (config) => set({ adminConfig: config }), | |
| toggleModal: (modal, isOpen) => set(state => ({ modals: { ...state.modals, [modal]: isOpen } })), | |
| closeAllModals: () => set(state => { | |
| const closed = Object.keys(state.modals).reduce((acc, key) => ({...acc, [key]: false}), {} as any); | |
| return { modals: closed }; | |
| }), | |
| showToast: (msg) => { | |
| set({ toastMsg: msg }); | |
| setTimeout(() => set({ toastMsg: null }), 3000); | |
| } | |
| }), | |
| { | |
| name: 'rl_ui_storage', | |
| partialize: (state) => ({ language: state.language, adminConfig: state.adminConfig }), // Don't persist modal state | |
| } | |
| ) | |
| ); | |
| // --- GENERATION STORE --- | |
| interface GenerationState { | |
| uploadedImages: string[]; | |
| selectedStyle: WeddingStyle | null; | |
| customStyleImage: string | null; | |
| // Config | |
| filter: string; | |
| blurAmount: number; | |
| compositionMode: CompositionMode; | |
| resolution: Resolution; | |
| subjectType: SubjectType; | |
| customPrompt: string; | |
| // Results | |
| results: Record<string, GeneratedResult>; | |
| status: GenerationStatus; | |
| progress: { current: number, total: number, statusMsg?: string } | null; | |
| errorMsg: string | null; | |
| // AI Analysis | |
| isScanning: boolean; | |
| scanStep: number; | |
| recommendedStyleIds: string[]; | |
| analysisResult: { faceShape: string; skinTone: string; bestVibe: string; }; | |
| // Actions | |
| setUploadedImages: (images: string[]) => void; | |
| setSelectedStyle: (style: WeddingStyle | null) => void; | |
| setCustomStyleImage: (img: string | null) => void; | |
| setGenerationConfig: (config: Partial<{ filter: string, blurAmount: number, compositionMode: CompositionMode, resolution: Resolution, subjectType: SubjectType, customPrompt: string }>) => void; | |
| setStatus: (status: GenerationStatus) => void; | |
| setProgress: (progress: GenerationState['progress']) => void; | |
| setErrorMsg: (msg: string | null) => void; | |
| addResult: (result: GeneratedResult) => void; | |
| setResults: (results: Record<string, GeneratedResult>) => void; | |
| resetGeneration: () => void; | |
| // Analysis Actions | |
| startScanning: () => void; | |
| setScanStep: (step: number) => void; | |
| setAnalysisData: (recs: string[], result: { faceShape: string; skinTone: string; bestVibe: string; }) => void; | |
| stopScanning: () => void; | |
| } | |
| export const useGenerationStore = create<GenerationState>()((set) => ({ | |
| uploadedImages: [], | |
| selectedStyle: null, | |
| customStyleImage: null, | |
| filter: 'none', | |
| blurAmount: 0, | |
| compositionMode: 'classic', | |
| resolution: 'standard', | |
| subjectType: 'female', | |
| customPrompt: '', | |
| results: {}, | |
| status: 'idle', | |
| progress: null, | |
| errorMsg: null, | |
| isScanning: false, | |
| scanStep: 0, | |
| recommendedStyleIds: [], | |
| analysisResult: { faceShape: '', skinTone: '', bestVibe: '' }, | |
| setUploadedImages: (images) => set({ uploadedImages: images }), | |
| setSelectedStyle: (style) => set({ selectedStyle: style }), | |
| setCustomStyleImage: (img) => set({ customStyleImage: img }), | |
| setGenerationConfig: (config) => set(state => ({ ...state, ...config })), | |
| setStatus: (status) => set({ status }), | |
| setProgress: (progress) => set({ progress }), | |
| setErrorMsg: (errorMessage) => set({ errorMsg: errorMessage }), | |
| addResult: (result) => set(state => ({ results: { ...state.results, [result.styleId]: result } })), | |
| setResults: (results) => set({ results }), | |
| resetGeneration: () => set({ | |
| status: 'idle', | |
| results: {}, | |
| uploadedImages: [], | |
| selectedStyle: null, | |
| customStyleImage: null, | |
| filter: 'none', | |
| blurAmount: 0, | |
| customPrompt: '', | |
| compositionMode: 'classic', | |
| resolution: 'standard', | |
| progress: null, | |
| recommendedStyleIds: [] | |
| }), | |
| startScanning: () => set({ isScanning: true, scanStep: 0 }), | |
| setScanStep: (step) => set({ scanStep: step }), | |
| setAnalysisData: (recs, result) => set({ recommendedStyleIds: recs, analysisResult: result }), | |
| stopScanning: () => set({ isScanning: false }) | |
| })); |