- Configure project metadata and dependencies - Setup Vite with React and TypeScript - Add environment configuration and .gitignore - Implement initial application entry point and assets
183 lines
6.1 KiB
TypeScript
183 lines
6.1 KiB
TypeScript
import { create } from "zustand";
|
|
import { persist } from "zustand/middleware";
|
|
|
|
export interface Reminder {
|
|
id: string;
|
|
title: string;
|
|
time: string;
|
|
frequency: "روزانه" | "هفتگی";
|
|
productId?: string;
|
|
completedDates: string[]; // List of ISO dates when this was completed
|
|
}
|
|
|
|
export interface HealthLog {
|
|
id: string;
|
|
date: string;
|
|
appetite: "عالی" | "متوسط" | "کم";
|
|
energy: "زیاد" | "نرمال" | "بیحال";
|
|
digestion: "نرمال" | "حساس" | "مشکلدار";
|
|
note?: string;
|
|
}
|
|
|
|
export interface PetConsumption {
|
|
productId: string;
|
|
packageSize: number;
|
|
remaining: number;
|
|
}
|
|
|
|
export interface PetProfile {
|
|
id: string;
|
|
name: string;
|
|
type: "سگ" | "گربه";
|
|
breed: string;
|
|
age: number;
|
|
weight: number;
|
|
activityLevel: "کم" | "متوسط" | "زیاد";
|
|
medicalConditions: string[];
|
|
image?: string;
|
|
reminders: Reminder[];
|
|
logs: HealthLog[];
|
|
consumptions: PetConsumption[];
|
|
}
|
|
|
|
interface PetStore {
|
|
pets: PetProfile[];
|
|
activePetId: string | null;
|
|
addPet: (pet: Omit<PetProfile, "id">) => void;
|
|
removePet: (id: string) => void;
|
|
setActivePet: (id: string) => void;
|
|
getActivePet: () => PetProfile | null;
|
|
updatePet: (id: string, updates: Partial<PetProfile>) => void;
|
|
addReminder: (petId: string, reminder: Omit<Reminder, "id" | "completedDates">) => void;
|
|
toggleReminder: (petId: string, reminderId: string, date: string) => void;
|
|
addHealthLog: (petId: string, log: Omit<HealthLog, "id" | "date">) => void;
|
|
}
|
|
|
|
export const usePetStore = create<PetStore>()(
|
|
persist(
|
|
(set, get) => ({
|
|
pets: [
|
|
{
|
|
id: "lucy",
|
|
name: "لوسی",
|
|
type: "سگ",
|
|
breed: "گلدن رتریور",
|
|
age: 3,
|
|
weight: 28,
|
|
activityLevel: "زیاد",
|
|
medicalConditions: ["حساسیت فصلی"],
|
|
reminders: [],
|
|
logs: [],
|
|
consumptions: []
|
|
},
|
|
{
|
|
id: "rocky",
|
|
name: "رکی",
|
|
type: "سگ",
|
|
breed: "ژرمن شپرد",
|
|
age: 5,
|
|
weight: 35,
|
|
activityLevel: "متوسط",
|
|
medicalConditions: ["دیسپلازی لگن"],
|
|
reminders: [],
|
|
logs: [],
|
|
consumptions: []
|
|
}
|
|
],
|
|
activePetId: "lucy",
|
|
addPet: (pet) => {
|
|
// TODO: [BACKEND_API] POST /api/pets | Payload: Omit<PetProfile, "id"> | Expected: PetProfile | Errors: [400, 401]
|
|
const id = Math.random().toString(36).substring(7);
|
|
const newPet = {
|
|
...pet,
|
|
id,
|
|
reminders: pet.reminders || [],
|
|
logs: pet.logs || [],
|
|
consumptions: pet.consumptions || []
|
|
};
|
|
set((state) => ({
|
|
pets: [...state.pets, newPet],
|
|
activePetId: state.activePetId || id,
|
|
}));
|
|
},
|
|
removePet: (id) => {
|
|
// TODO: [BACKEND_API] DELETE /api/pets/{id} | Expected: { success: boolean } | Errors: [401, 404]
|
|
set((state) => {
|
|
const newPets = state.pets.filter((p) => p.id !== id);
|
|
return {
|
|
pets: newPets,
|
|
activePetId: state.activePetId === id ? (newPets[0]?.id || null) : state.activePetId,
|
|
};
|
|
});
|
|
},
|
|
setActivePet: (id) => set({ activePetId: id }),
|
|
getActivePet: () => {
|
|
const { pets, activePetId } = get();
|
|
return pets.find((p) => p.id === activePetId) || null;
|
|
},
|
|
updatePet: (id, updates) => {
|
|
// TODO: [BACKEND_API] PATCH /api/pets/{id} | Payload: Partial<PetProfile> | Expected: PetProfile | Errors: [400, 401, 404]
|
|
set((state) => ({
|
|
pets: state.pets.map((p) => (p.id === id ? { ...p, ...updates } : p)),
|
|
}));
|
|
},
|
|
addReminder: (petId, reminder) => {
|
|
// TODO: [BACKEND_API] POST /api/pets/{petId}/reminders | Payload: Omit<Reminder, "id"> | Expected: Reminder | Errors: [400, 401, 404]
|
|
const id = Math.random().toString(36).substring(7);
|
|
set((state) => ({
|
|
pets: state.pets.map((p) =>
|
|
p.id === petId ? { ...p, reminders: [...(p.reminders || []), { ...reminder, id, completedDates: [] }] } : p
|
|
)
|
|
}));
|
|
},
|
|
toggleReminder: (petId, reminderId, date) => {
|
|
// TODO: [BACKEND_API] POST /api/pets/{petId}/reminders/{reminderId}/toggle | Payload: { date: string } | Expected: { success: boolean, newRemaining?: number } | Errors: [401, 404]
|
|
set((state) => ({
|
|
pets: state.pets.map((p) => {
|
|
if (p.id !== petId) return p;
|
|
|
|
const reminder = p.reminders?.find(r => r.id === reminderId);
|
|
if (!reminder) return p;
|
|
|
|
const isCompleting = !reminder.completedDates.includes(date);
|
|
|
|
// 1. Update Reminders
|
|
const newReminders = p.reminders.map(r => {
|
|
if (r.id !== reminderId) return r;
|
|
return {
|
|
...r,
|
|
completedDates: isCompleting
|
|
? [...r.completedDates, date]
|
|
: r.completedDates.filter(d => d !== date)
|
|
};
|
|
});
|
|
|
|
// 2. Update Consumptions (decrement if completing and product is linked)
|
|
let newConsumptions = p.consumptions || [];
|
|
if (isCompleting && reminder.productId) {
|
|
newConsumptions = newConsumptions.map(c =>
|
|
c.productId === reminder.productId
|
|
? { ...c, remaining: Math.max(0, c.remaining - 1) }
|
|
: c
|
|
);
|
|
}
|
|
|
|
return { ...p, reminders: newReminders, consumptions: newConsumptions };
|
|
})
|
|
}));
|
|
},
|
|
addHealthLog: (petId, log) => {
|
|
// TODO: [BACKEND_API] POST /api/pets/{petId}/health-logs | Payload: Omit<HealthLog, "id" | "date"> | Expected: HealthLog | Errors: [400, 401, 404]
|
|
const id = Math.random().toString(36).substring(7);
|
|
const date = new Date().toISOString();
|
|
set((state) => ({
|
|
pets: state.pets.map((p) =>
|
|
p.id === petId ? { ...p, logs: [{ ...log, id, date }, ...(p.logs || [])] } : p
|
|
)
|
|
}));
|
|
},
|
|
}),
|
|
{ name: "canina-pets" }
|
|
)
|
|
);
|