auto winn
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
---
|
||||
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<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
|
||||
```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
|
||||
)
|
||||
}));
|
||||
```
|
||||
Reference in New Issue
Block a user