--- name: state-management description: Panduan lengkap penggunaan Zustand store untuk Tour Planner App. Gunakan skill ini ketika membaca, menulis, atau menambahkan state baru ke global store, atau saat membuat custom hooks. --- # Skill: State Management — Zustand Store ## Overview Aplikasi ini menggunakan Zustand dengan middleware `persist` untuk menyimpan state ke localStorage. File utama: `src/store/tourStore.js` localStorage key: `tour-planner-storage` ## Full Store Shape ```javascript { // === TRIP INFO === trip: { id: null, // string | null name: '', // nama trip user destination: '', // nama destinasi startDate: null, // ISO string endDate: null, // ISO string totalDays: 0, // number coverImage: '', // URL atau path lokal type: 'custom', // 'preset' | 'custom' category: '', // 'pantai' | 'gunung' | 'kota' | 'budaya' }, // === SCHEDULE === // Array of days, index = hari ke-N (0-based) schedule: [ { dayNumber: 1, // display number date: '', // ISO string activities: [ { id: '', // uuid time: '09:00', // HH:mm name: '', location: '', duration: 60, // menit category: '', // 'wisata' | 'makan' | 'transportasi' | 'check-in' | 'lainnya' notes: '', color: '', // CSS color untuk timeline } ] } ], // === PACKING LIST === packingList: { Pakaian: [ { id: '', name: '', qty: 1, checked: false } ], Dokumen: [], Elektronik: [], 'Obat-obatan': [], Toiletries: [], Lainnya: [], }, // === BUDGET === budget: { total: 0, // number (IDR) currency: 'IDR', // 'IDR' | 'USD' | 'EUR' expenses: [ { id: '', // uuid name: '', amount: 0, category: '', // 'transportasi' | 'akomodasi' | 'makan' | 'aktivitas' | 'oleh-oleh' | 'lainnya' day: null, // number (hari ke berapa) | null (umum) date: '', // ISO string opsional notes: '', } ] } } ``` ## Actions API ```javascript const { // Trip actions setTrip, // (tripData: Partial) => void resetTrip, // () => void // Schedule actions initSchedule, // (totalDays: number, startDate: string) => void addActivity, // (dayIndex: number, activity: Activity) => void updateActivity, // (dayIndex: number, activityId: string, data: Partial) => void removeActivity, // (dayIndex: number, activityId: string) => void reorderActivities,// (dayIndex: number, oldIndex: number, newIndex: number) => void moveActivity, // (fromDay: number, toDay: number, activityId: string) => void // Packing actions addPackingItem, // (category: string, item: PackingItem) => void updatePackingItem,// (category: string, id: string, data: Partial) => void togglePackingItem,// (category: string, id: string) => void removePackingItem,// (category: string, id: string) => void loadPackingTemplate, // (tripType: string) => void // Budget actions setTotalBudget, // (amount: number) => void addExpense, // (expense: Expense) => void updateExpense, // (id: string, data: Partial) => void removeExpense, // (id: string) => void // Global resetAll, // () => void — reset semua state } = useTourStore(); ``` ## Custom Hooks Gunakan custom hooks untuk encapsulate logic: ### useTripStore.js ```javascript import { useTourStore } from '../store/tourStore'; export const useTripStore = () => { const { trip, setTrip, resetTrip } = useTourStore(); const updateTrip = (data) => setTrip({ ...trip, ...data }); const isConfigured = trip.id !== null; return { trip, updateTrip, resetTrip, isConfigured }; }; ``` ### useSchedule.js ```javascript import { useTourStore } from '../store/tourStore'; export const useSchedule = (dayIndex) => { const { schedule, addActivity, removeActivity, reorderActivities } = useTourStore(); const day = schedule[dayIndex]; const activities = day?.activities ?? []; const sortedActivities = [...activities].sort((a, b) => a.time.localeCompare(b.time)); return { day, activities: sortedActivities, addActivity: (a) => addActivity(dayIndex, a), removeActivity: (id) => removeActivity(dayIndex, id) }; }; ``` ### useBudget.js ```javascript import { useTourStore } from '../store/tourStore'; export const useBudget = () => { const { budget, setTotalBudget, addExpense, removeExpense } = useTourStore(); const totalSpent = budget.expenses.reduce((sum, e) => sum + e.amount, 0); const remaining = budget.total - totalSpent; const isOverBudget = remaining < 0; const percentUsed = budget.total > 0 ? (totalSpent / budget.total) * 100 : 0; const byCategory = budget.expenses.reduce((acc, e) => { acc[e.category] = (acc[e.category] || 0) + e.amount; return acc; }, {}); return { budget, totalSpent, remaining, isOverBudget, percentUsed, byCategory, setTotalBudget, addExpense, removeExpense }; }; ``` ## Cara Membuat UUID ```javascript // Di utils/helpers.js export const generateId = () => crypto.randomUUID(); ``` ## Contoh Penggunaan di Komponen ```jsx import { useTourStore } from '../store/tourStore'; const SchedulePage = () => { const { schedule, addActivity } = useTourStore(); const handleAddActivity = (dayIndex, activityData) => { addActivity(dayIndex, { id: crypto.randomUUID(), ...activityData }); }; return ( /* ... */ ); }; ``` ## Penting: Jangan Mutasi State Langsung ```javascript // ❌ SALAH state.schedule[0].activities.push(newActivity); // ✅ BENAR (dalam store action) set(state => ({ schedule: state.schedule.map((day, i) => i === dayIndex ? { ...day, activities: [...day.activities, newActivity] } : day ) })); ```