refactor
This commit is contained in:
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"name": "home-assistant-frontend",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-router-dom": "^6.28.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^5.4.11"
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
.app {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid #2a2f3a;
|
||||
background: #151922;
|
||||
}
|
||||
|
||||
.app-header h1 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.app-header nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.app-header nav a {
|
||||
padding: 0.45rem 0.9rem;
|
||||
border-radius: 8px;
|
||||
color: #a8b0bd;
|
||||
}
|
||||
|
||||
.app-header nav a.active {
|
||||
background: #2b3445;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.app-header {
|
||||
padding: 0.55rem 0.75rem;
|
||||
gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.app-header h1 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-header nav {
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
flex-wrap: nowrap;
|
||||
gap: 0.35rem;
|
||||
padding-bottom: 0.1rem;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.app-header nav::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-header nav a {
|
||||
padding: 0.4rem 0.65rem;
|
||||
font-size: 0.85rem;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import { NavLink, Route, Routes } from "react-router-dom";
|
||||
import PomodoroWidget from "./components/PomodoroWidget";
|
||||
import { PomodoroProvider } from "./context/PomodoroContext";
|
||||
import { useVisualViewportHeight } from "./hooks/useVisualViewport";
|
||||
import Character from "./pages/Character";
|
||||
import Chat from "./pages/Chat";
|
||||
import Fitness from "./pages/Fitness";
|
||||
import Reminders from "./pages/Reminders";
|
||||
import Shopping from "./pages/Shopping";
|
||||
import Memory from "./pages/Memory";
|
||||
import Pomodoro from "./pages/Pomodoro";
|
||||
import "./App.css";
|
||||
|
||||
export default function App() {
|
||||
useVisualViewportHeight();
|
||||
|
||||
return (
|
||||
<PomodoroProvider>
|
||||
<div className="app">
|
||||
<header className="app-header">
|
||||
<h1>Home AI Assistant</h1>
|
||||
<nav>
|
||||
<NavLink to="/" end>
|
||||
Чат
|
||||
</NavLink>
|
||||
<NavLink to="/pomodoro">Помидоро</NavLink>
|
||||
<NavLink to="/character">Персонаж</NavLink>
|
||||
<NavLink to="/memory">Память</NavLink>
|
||||
<NavLink to="/fitness">Фитнес</NavLink>
|
||||
<NavLink to="/shopping">Покупки</NavLink>
|
||||
<NavLink to="/reminders">Календарь</NavLink>
|
||||
<PomodoroWidget compact />
|
||||
</nav>
|
||||
</header>
|
||||
<main className="app-main">
|
||||
<Routes>
|
||||
<Route path="/" element={<Chat />} />
|
||||
<Route path="/pomodoro" element={<Pomodoro />} />
|
||||
<Route path="/character" element={<Character />} />
|
||||
<Route path="/memory" element={<Memory />} />
|
||||
<Route path="/fitness" element={<Fitness />} />
|
||||
<Route path="/shopping" element={<Shopping />} />
|
||||
<Route path="/reminders" element={<Reminders />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
</PomodoroProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,555 +0,0 @@
|
||||
const API_BASE = import.meta.env.VITE_API_URL ?? "";
|
||||
|
||||
export interface ChatSession {
|
||||
id: number;
|
||||
title: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
id: number;
|
||||
role: string;
|
||||
content: string;
|
||||
tool_calls_json?: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface SessionDetail extends ChatSession {
|
||||
messages: ChatMessage[];
|
||||
}
|
||||
|
||||
export interface PomodoroCycle {
|
||||
completed_work_sessions: number;
|
||||
sessions_until_long_break: number;
|
||||
task_note: string;
|
||||
work_duration_min: number;
|
||||
short_break_min: number;
|
||||
long_break_min: number;
|
||||
auto_advance: boolean;
|
||||
chat_notify_seq: number;
|
||||
}
|
||||
|
||||
export interface PomodoroStatus {
|
||||
status: string;
|
||||
phase: string;
|
||||
duration_min: number;
|
||||
task_note: string;
|
||||
elapsed_seconds: number;
|
||||
remaining_seconds: number;
|
||||
session_id: number | null;
|
||||
started_at?: string | null;
|
||||
finished_at?: string | null;
|
||||
cycle: PomodoroCycle;
|
||||
}
|
||||
|
||||
export interface CharacterCardData {
|
||||
name: string;
|
||||
description: string;
|
||||
personality: string;
|
||||
scenario: string;
|
||||
first_mes: string;
|
||||
mes_example: string;
|
||||
system_prompt: string;
|
||||
post_history_instructions: string;
|
||||
tags: string[];
|
||||
creator: string;
|
||||
creator_notes: string;
|
||||
alternate_greetings: string[];
|
||||
character_version: string;
|
||||
}
|
||||
|
||||
export interface CharacterCardV2 {
|
||||
spec: string;
|
||||
spec_version: string;
|
||||
data: CharacterCardData;
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
name?: string;
|
||||
age?: string;
|
||||
timezone?: string;
|
||||
language?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface MemoryFact {
|
||||
id: number;
|
||||
category: string;
|
||||
content: string;
|
||||
importance: number;
|
||||
source?: string;
|
||||
updated_at?: string | null;
|
||||
}
|
||||
|
||||
export interface FitnessComputed {
|
||||
bmr: number;
|
||||
tdee: number;
|
||||
bmi: number;
|
||||
}
|
||||
|
||||
export interface FitnessProfile {
|
||||
sex?: string;
|
||||
age?: number;
|
||||
height_cm?: number;
|
||||
weight_kg?: number;
|
||||
activity_level?: string;
|
||||
goal?: string;
|
||||
target_weight_kg?: number | null;
|
||||
weekly_workouts?: number;
|
||||
calorie_target?: number;
|
||||
protein_g?: number;
|
||||
fat_g?: number;
|
||||
carbs_g?: number;
|
||||
water_l?: number;
|
||||
computed?: FitnessComputed;
|
||||
}
|
||||
|
||||
export interface FoodLogItem {
|
||||
id: number;
|
||||
meal_type: string;
|
||||
description: string;
|
||||
calories: number;
|
||||
protein_g: number;
|
||||
fat_g: number;
|
||||
carbs_g: number;
|
||||
estimated: boolean;
|
||||
logged_at?: string;
|
||||
}
|
||||
|
||||
export interface WaterLogItem {
|
||||
id: number;
|
||||
amount_ml: number;
|
||||
logged_at?: string;
|
||||
}
|
||||
|
||||
export interface WorkoutLogItem {
|
||||
id: number;
|
||||
title: string;
|
||||
notes?: string;
|
||||
duration_min?: number | null;
|
||||
exercises?: unknown[];
|
||||
logged_at?: string;
|
||||
}
|
||||
|
||||
export interface FitnessDailySummary {
|
||||
date: string;
|
||||
totals: {
|
||||
calories: number;
|
||||
protein_g: number;
|
||||
fat_g: number;
|
||||
carbs_g: number;
|
||||
water_ml: number;
|
||||
};
|
||||
targets: {
|
||||
calories: number;
|
||||
protein_g: number;
|
||||
fat_g: number;
|
||||
carbs_g: number;
|
||||
water_ml: number;
|
||||
};
|
||||
meals: FoodLogItem[];
|
||||
water: WaterLogItem[];
|
||||
workouts: WorkoutLogItem[];
|
||||
}
|
||||
|
||||
export interface BodyMetric {
|
||||
id: number;
|
||||
weight_kg: number;
|
||||
recorded_at?: string;
|
||||
}
|
||||
|
||||
export interface FitnessReminder {
|
||||
id: number;
|
||||
kind: string;
|
||||
hour: number;
|
||||
minute: number;
|
||||
interval_hours?: number | null;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface FitnessDayOverview {
|
||||
date: string;
|
||||
has_data: boolean;
|
||||
totals: FitnessDailySummary["totals"];
|
||||
targets: FitnessDailySummary["targets"];
|
||||
meal_count: number;
|
||||
workout_count: number;
|
||||
}
|
||||
|
||||
export interface FitnessHistory {
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
days: number;
|
||||
summaries: FitnessDayOverview[];
|
||||
}
|
||||
|
||||
export interface FitnessSnapshot {
|
||||
profile: FitnessProfile | null;
|
||||
today: FitnessDailySummary;
|
||||
history?: FitnessHistory;
|
||||
body_metrics: BodyMetric[];
|
||||
reminders: FitnessReminder[];
|
||||
}
|
||||
|
||||
export interface MemorySnapshot {
|
||||
profile: UserProfile;
|
||||
facts: MemoryFact[];
|
||||
session_summary?: string;
|
||||
total_facts: number;
|
||||
}
|
||||
|
||||
export interface PomodoroHistoryItem {
|
||||
id: number;
|
||||
status: string;
|
||||
phase: string;
|
||||
duration_min: number;
|
||||
task_note: string;
|
||||
result: string | null;
|
||||
completed: boolean;
|
||||
elapsed_seconds: number;
|
||||
finished_at: string | null;
|
||||
}
|
||||
|
||||
async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
||||
const response = await fetch(`${API_BASE}${path}`, options);
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(text || response.statusText);
|
||||
}
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
health: () => request<{ status: string }>("/api/v1/health"),
|
||||
|
||||
listSessions: () => request<ChatSession[]>("/api/v1/chat/sessions"),
|
||||
|
||||
createSession: (title = "Новый чат") =>
|
||||
request<ChatSession>("/api/v1/chat/sessions", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ title }),
|
||||
}),
|
||||
|
||||
getSession: (id: number) => request<SessionDetail>(`/api/v1/chat/sessions/${id}`),
|
||||
|
||||
deleteSession: (id: number) =>
|
||||
request<{ ok: boolean }>(`/api/v1/chat/sessions/${id}`, { method: "DELETE" }),
|
||||
|
||||
sendMessage: async function* (sessionId: number, content: string) {
|
||||
const response = await fetch(`${API_BASE}/api/v1/chat/sessions/${sessionId}/messages`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ content }),
|
||||
});
|
||||
|
||||
if (!response.ok || !response.body) {
|
||||
const detail = await response.text().catch(() => "");
|
||||
throw new Error(detail || `Ошибка отправки (${response.status})`);
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
|
||||
const flushParts = function* (parts: string[]) {
|
||||
for (const part of parts) {
|
||||
if (!part.trim()) continue;
|
||||
const lines = part.split("\n");
|
||||
let event = "message";
|
||||
let data = "";
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("event: ")) event = line.slice(7);
|
||||
if (line.startsWith("data: ")) data = line.slice(6);
|
||||
}
|
||||
|
||||
if (data) {
|
||||
yield { event, data: JSON.parse(data) };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
let done = false;
|
||||
let value: Uint8Array | undefined;
|
||||
try {
|
||||
({ done, value } = await reader.read());
|
||||
} catch {
|
||||
throw new Error(
|
||||
"Соединение прервалось (таймаут прокси). Обновите чат — ответ мог уже сохраниться.",
|
||||
);
|
||||
}
|
||||
|
||||
if (value) {
|
||||
buffer += decoder.decode(value, { stream: !done });
|
||||
}
|
||||
|
||||
const parts = buffer.split("\n\n");
|
||||
buffer = parts.pop() ?? "";
|
||||
yield* flushParts(parts);
|
||||
|
||||
if (done) {
|
||||
if (buffer.trim()) {
|
||||
yield* flushParts([buffer]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
},
|
||||
|
||||
pomodoroStatus: () => request<PomodoroStatus>("/api/v1/pomodoro/status"),
|
||||
|
||||
pomodoroStart: (duration_min: number, task_note: string) =>
|
||||
request<PomodoroStatus>("/api/v1/pomodoro/start", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ duration_min, task_note }),
|
||||
}),
|
||||
|
||||
pomodoroPause: () =>
|
||||
request<PomodoroStatus>("/api/v1/pomodoro/pause", { method: "POST" }),
|
||||
|
||||
pomodoroResume: () =>
|
||||
request<PomodoroStatus>("/api/v1/pomodoro/resume", { method: "POST" }),
|
||||
|
||||
pomodoroStop: (result: string, completed: boolean) =>
|
||||
request<PomodoroStatus>("/api/v1/pomodoro/stop", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ result, completed }),
|
||||
}),
|
||||
|
||||
pomodoroHistory: () => request<PomodoroHistoryItem[]>("/api/v1/pomodoro/history"),
|
||||
|
||||
pomodoroResetCycle: (clear_task = false) =>
|
||||
request<PomodoroStatus>(`/api/v1/pomodoro/cycle/reset?clear_task=${clear_task}`, {
|
||||
method: "POST",
|
||||
}),
|
||||
|
||||
pomodoroSkip: () =>
|
||||
request<PomodoroStatus>("/api/v1/pomodoro/skip", { method: "POST" }),
|
||||
|
||||
pomodoroStartShortBreak: (duration_min?: number) =>
|
||||
request<PomodoroStatus>(
|
||||
`/api/v1/pomodoro/break/short/start${duration_min ? `?duration_min=${duration_min}` : ""}`,
|
||||
{ method: "POST" }
|
||||
),
|
||||
|
||||
pomodoroStartLongBreak: (duration_min?: number) =>
|
||||
request<PomodoroStatus>(
|
||||
`/api/v1/pomodoro/break/long/start${duration_min ? `?duration_min=${duration_min}` : ""}`,
|
||||
{ method: "POST" }
|
||||
),
|
||||
|
||||
getCharacter: () => request<CharacterCardV2>("/api/v1/character"),
|
||||
|
||||
saveCharacter: (card: CharacterCardV2) =>
|
||||
request<CharacterCardV2>("/api/v1/character", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(card),
|
||||
}),
|
||||
|
||||
getMemorySnapshot: (sessionId?: number) =>
|
||||
request<MemorySnapshot>(
|
||||
`/api/v1/memory${sessionId ? `?session_id=${sessionId}` : ""}`
|
||||
),
|
||||
|
||||
updateProfile: (updates: UserProfile) =>
|
||||
request<{ ok: boolean; profile: UserProfile }>("/api/v1/profile", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ updates }),
|
||||
}),
|
||||
|
||||
createMemoryFact: (payload: {
|
||||
content: string;
|
||||
category?: string;
|
||||
importance?: number;
|
||||
session_id?: number;
|
||||
}) =>
|
||||
request<{ ok: boolean; memory_id: number }>("/api/v1/memory/facts", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
|
||||
forgetMemoryFact: (id: number) =>
|
||||
request<{ ok: boolean }>(`/api/v1/memory/facts/${id}`, { method: "DELETE" }),
|
||||
|
||||
getFitnessSnapshot: () => request<FitnessSnapshot>("/api/v1/fitness"),
|
||||
|
||||
getFitnessSummary: (day?: string) =>
|
||||
request<FitnessDailySummary>(
|
||||
`/api/v1/fitness/summary${day ? `?day=${encodeURIComponent(day)}` : ""}`
|
||||
),
|
||||
|
||||
getFitnessHistory: (days = 7, end?: string) => {
|
||||
const params = new URLSearchParams({ days: String(days) });
|
||||
if (end) params.set("end", end);
|
||||
return request<FitnessHistory>(`/api/v1/fitness/history?${params}`);
|
||||
},
|
||||
|
||||
updateFitnessProfile: (updates: Partial<FitnessProfile>) =>
|
||||
request<{ ok: boolean; profile: FitnessProfile }>("/api/v1/fitness/profile", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(updates),
|
||||
}),
|
||||
|
||||
deleteFitnessMeal: (id: number) =>
|
||||
request<{ ok: boolean }>(`/api/v1/fitness/meals/${id}`, { method: "DELETE" }),
|
||||
|
||||
deleteFitnessWater: (id: number) =>
|
||||
request<{ ok: boolean }>(`/api/v1/fitness/water/${id}`, { method: "DELETE" }),
|
||||
|
||||
updateFitnessReminder: (
|
||||
kind: string,
|
||||
updates: { enabled?: boolean; hour?: number; minute?: number; interval_hours?: number }
|
||||
) =>
|
||||
request<{ ok: boolean }>(`/api/v1/fitness/reminders/${kind}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(updates),
|
||||
}),
|
||||
|
||||
getShoppingSnapshot: () => request<ShoppingSnapshot>("/api/v1/shopping"),
|
||||
|
||||
createShoppingList: (name: string) =>
|
||||
request<{ ok: boolean; list: ShoppingList }>("/api/v1/shopping/lists", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name }),
|
||||
}),
|
||||
|
||||
renameShoppingList: (listId: number, name: string) =>
|
||||
request<{ ok: boolean; list: ShoppingList }>(`/api/v1/shopping/lists/${listId}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name }),
|
||||
}),
|
||||
|
||||
deleteShoppingList: (listId: number) =>
|
||||
request<{ ok: boolean }>(`/api/v1/shopping/lists/${listId}`, { method: "DELETE" }),
|
||||
|
||||
addShoppingItems: (payload: {
|
||||
list_id?: number;
|
||||
list_name?: string;
|
||||
items: { text: string; quantity?: number; unit?: string }[];
|
||||
}) =>
|
||||
request<{ ok: boolean }>("/api/v1/shopping/items", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
|
||||
setShoppingItemChecked: (itemId: number, checked: boolean) =>
|
||||
request<{ ok: boolean }>(`/api/v1/shopping/items/${itemId}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ checked }),
|
||||
}),
|
||||
|
||||
removeShoppingItem: (itemId: number) =>
|
||||
request<{ ok: boolean }>(`/api/v1/shopping/items/${itemId}`, { method: "DELETE" }),
|
||||
|
||||
clearShoppingChecked: (listId: number) =>
|
||||
request<{ ok: boolean }>(`/api/v1/shopping/lists/${listId}/clear-checked`, {
|
||||
method: "POST",
|
||||
}),
|
||||
|
||||
getRemindersSnapshot: () => request<RemindersSnapshot>("/api/v1/reminders"),
|
||||
|
||||
getRemindersCalendar: (year: number, month: number) =>
|
||||
request<RemindersCalendar>(`/api/v1/reminders/calendar?year=${year}&month=${month}`),
|
||||
|
||||
createReminder: (payload: ReminderCreatePayload) =>
|
||||
request<{ ok: boolean; reminder: Reminder }>("/api/v1/reminders", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
|
||||
updateReminder: (id: number, payload: Partial<ReminderCreatePayload> & { enabled?: boolean }) =>
|
||||
request<{ ok: boolean; reminder: Reminder }>(`/api/v1/reminders/${id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
|
||||
deleteReminder: (id: number) =>
|
||||
request<{ ok: boolean }>(`/api/v1/reminders/${id}`, { method: "DELETE" }),
|
||||
|
||||
completeReminder: (id: number) =>
|
||||
request<{ ok: boolean; reminder: Reminder }>(`/api/v1/reminders/${id}/complete`, {
|
||||
method: "POST",
|
||||
}),
|
||||
};
|
||||
|
||||
export interface ShoppingListItem {
|
||||
id: number;
|
||||
list_id: number;
|
||||
text: string;
|
||||
quantity: number | null;
|
||||
unit: string;
|
||||
checked: boolean;
|
||||
sort_order: number;
|
||||
}
|
||||
|
||||
export interface ShoppingList {
|
||||
id: number;
|
||||
name: string;
|
||||
sort_order: number;
|
||||
item_count: number;
|
||||
unchecked_count: number;
|
||||
items?: ShoppingListItem[];
|
||||
}
|
||||
|
||||
export interface ShoppingSnapshot {
|
||||
lists: ShoppingList[];
|
||||
list_count: number;
|
||||
total_items: number;
|
||||
unchecked_items: number;
|
||||
}
|
||||
|
||||
export interface Reminder {
|
||||
id: number;
|
||||
title: string;
|
||||
notes: string;
|
||||
due_at: string;
|
||||
due_at_local: string;
|
||||
all_day: boolean;
|
||||
recurrence: string;
|
||||
enabled: boolean;
|
||||
completed_at: string | null;
|
||||
timezone: string;
|
||||
created_at: string | null;
|
||||
}
|
||||
|
||||
export interface RemindersSnapshot {
|
||||
notify_seq: number;
|
||||
upcoming: Reminder[];
|
||||
upcoming_count: number;
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
export interface RemindersCalendar {
|
||||
year: number;
|
||||
month: number;
|
||||
timezone: string;
|
||||
reminders: Reminder[];
|
||||
}
|
||||
|
||||
export interface ReminderCreatePayload {
|
||||
title: string;
|
||||
due_at: string;
|
||||
notes?: string;
|
||||
all_day?: boolean;
|
||||
recurrence?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user