5.9 KiB
5.9 KiB
name, description
| name | description |
|---|---|
| state-management | 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
{
// === 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
const {
// Trip actions
setTrip, // (tripData: Partial<trip>) => void
resetTrip, // () => void
// Schedule actions
initSchedule, // (totalDays: number, startDate: string) => void
addActivity, // (dayIndex: number, activity: Activity) => void
updateActivity, // (dayIndex: number, activityId: string, data: Partial<Activity>) => 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<PackingItem>) => 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<Expense>) => void
removeExpense, // (id: string) => void
// Global
resetAll, // () => void — reset semua state
} = useTourStore();
Custom Hooks
Gunakan custom hooks untuk encapsulate logic:
useTripStore.js
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
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
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
// Di utils/helpers.js
export const generateId = () => crypto.randomUUID();
Contoh Penggunaan di Komponen
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
// ❌ 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
)
}));