feat: 实现Home页四区布局
- 实现纪念日列表组件 (AnniversaryCard, AnniversaryList) - 实现提醒列表组件 (ReminderCard, ReminderList) - 实现便签编辑区 (NoteEditor) 带自动保存 - 实现AI对话框 (AIChatBox) 支持自然语言解析 - 更新HomePage实现四区布局和添加/编辑弹窗 - 更新类型定义和数据Store Co-Authored-By: Claude (MiniMax-M2.1) <noreply@anthropic.com>
This commit is contained in:
parent
d3de5d8598
commit
4dbf9b0bbc
168
src/components/ai/AIChatBox.tsx
Normal file
168
src/components/ai/AIChatBox.tsx
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import { useState, useRef, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Paper,
|
||||||
|
Text,
|
||||||
|
Stack,
|
||||||
|
Group,
|
||||||
|
TextInput,
|
||||||
|
ActionIcon,
|
||||||
|
Loader,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Badge,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { IconSend, IconSparkles } from '@tabler/icons-react';
|
||||||
|
import { api } from '../../services/api';
|
||||||
|
import { useAppStore } from '../../stores';
|
||||||
|
import type { AIConversation } from '../../types';
|
||||||
|
|
||||||
|
interface AIChatBoxProps {
|
||||||
|
onEventCreated?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AIChatBox({ onEventCreated }: AIChatBoxProps) {
|
||||||
|
const [message, setMessage] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [conversation, setConversation] = useState<AIConversation | null>(null);
|
||||||
|
const [history, setHistory] = useState<AIConversation[]>([]);
|
||||||
|
const addConversation = useAppStore((state) => state.addConversation);
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
scrollRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, [history]);
|
||||||
|
|
||||||
|
const handleSend = async () => {
|
||||||
|
if (!message.trim() || loading) return;
|
||||||
|
|
||||||
|
const userMessage = message.trim();
|
||||||
|
setMessage('');
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await api.ai.parse(userMessage);
|
||||||
|
|
||||||
|
const newConversation: AIConversation = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
user_id: '',
|
||||||
|
message: userMessage,
|
||||||
|
response: result.response,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setConversation(newConversation);
|
||||||
|
setHistory((prev) => [...prev, newConversation]);
|
||||||
|
addConversation(newConversation);
|
||||||
|
|
||||||
|
if (onEventCreated) {
|
||||||
|
onEventCreated();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AI parse error:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSend();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper p="md" withBorder radius="md" h="100%">
|
||||||
|
<Stack gap="sm" h="100%">
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Group gap={6}>
|
||||||
|
<IconSparkles size={16} color="orange" />
|
||||||
|
<Text fw={500} size="sm">
|
||||||
|
AI 助手
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Chat history */}
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
overflowY: 'auto',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{history.length === 0 ? (
|
||||||
|
<Stack align="center" justify="center" h="100%" gap="xs">
|
||||||
|
<IconSparkles size={32} color="gray" />
|
||||||
|
<Text size="sm" c="dimmed" ta="center">
|
||||||
|
告诉我你想记住的事情
|
||||||
|
<br />
|
||||||
|
例如:下周三见客户、每月15号还房贷
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
history.map((conv) => (
|
||||||
|
<Box key={conv.id}>
|
||||||
|
<Text size="xs" c="dimmed" mb={4}>
|
||||||
|
你
|
||||||
|
</Text>
|
||||||
|
<Card withBorder padding="sm" radius="sm" mb="xs">
|
||||||
|
<Text size="sm">{conv.message}</Text>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Text size="xs" c="blue" mb={4}>
|
||||||
|
AI
|
||||||
|
</Text>
|
||||||
|
<Card withBorder padding="sm" radius="sm" bg="blue.0">
|
||||||
|
<Text size="sm" style={{ whiteSpace: 'pre-wrap' }}>
|
||||||
|
{conv.response}
|
||||||
|
</Text>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loading && (
|
||||||
|
<Group gap={8}>
|
||||||
|
<Loader size="xs" />
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
AI 正在思考...
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div ref={scrollRef} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Input */}
|
||||||
|
<Group gap="xs">
|
||||||
|
<TextInput
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
placeholder="输入你的提醒事项..."
|
||||||
|
size="xs"
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<ActionIcon
|
||||||
|
size="lg"
|
||||||
|
variant="filled"
|
||||||
|
color="blue"
|
||||||
|
onClick={handleSend}
|
||||||
|
disabled={!message.trim() || loading}
|
||||||
|
>
|
||||||
|
<IconSend size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
58
src/components/anniversary/AnniversaryCard.tsx
Normal file
58
src/components/anniversary/AnniversaryCard.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { Card, Text, Badge, Group, ActionIcon, Stack } from '@mantine/core';
|
||||||
|
import { IconHeart, IconRepeat } from '@tabler/icons-react';
|
||||||
|
import type { Event } from '../../types';
|
||||||
|
|
||||||
|
interface AnniversaryCardProps {
|
||||||
|
event: Event;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AnniversaryCard({ event, onClick }: AnniversaryCardProps) {
|
||||||
|
const isLunar = event.is_lunar;
|
||||||
|
const repeatType = event.repeat_type;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
shadow="sm"
|
||||||
|
padding="sm"
|
||||||
|
radius="md"
|
||||||
|
onClick={onClick}
|
||||||
|
style={{ cursor: 'pointer', transition: 'transform 0.2s' }}
|
||||||
|
onMouseEnter={(e) => (e.currentTarget.style.transform = 'translateY(-2px)')}
|
||||||
|
onMouseLeave={(e) => (e.currentTarget.style.transform = 'translateY(0)')}
|
||||||
|
>
|
||||||
|
<Group justify="space-between" wrap="nowrap">
|
||||||
|
<Stack gap={4}>
|
||||||
|
<Group gap={6}>
|
||||||
|
<IconHeart size={14} color="pink" />
|
||||||
|
<Text fw={500} size="sm" lineClamp={1}>
|
||||||
|
{event.title}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{new Date(event.date).toLocaleDateString('zh-CN')}
|
||||||
|
{isLunar && ' (农历)'}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Group gap={6}>
|
||||||
|
{repeatType !== 'none' && (
|
||||||
|
<Badge
|
||||||
|
size="xs"
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
leftSection={<IconRepeat size={10} />}
|
||||||
|
>
|
||||||
|
{repeatType === 'yearly' ? '每年' : '每月'}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{event.is_holiday && (
|
||||||
|
<Badge size="xs" variant="light" color="green">
|
||||||
|
节假日
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
58
src/components/anniversary/AnniversaryList.tsx
Normal file
58
src/components/anniversary/AnniversaryList.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { Stack, Text, Paper, Group, Button } from '@mantine/core';
|
||||||
|
import { IconPlus } from '@tabler/icons-react';
|
||||||
|
import { AnniversaryCard } from './AnniversaryCard';
|
||||||
|
import type { Event } from '../../types';
|
||||||
|
|
||||||
|
interface AnniversaryListProps {
|
||||||
|
events: Event[];
|
||||||
|
onEventClick: (event: Event) => void;
|
||||||
|
onAddClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AnniversaryList({ events, onEventClick, onAddClick }: AnniversaryListProps) {
|
||||||
|
const anniversaries = events.filter((e) => e.type === 'anniversary');
|
||||||
|
|
||||||
|
if (anniversaries.length === 0) {
|
||||||
|
return (
|
||||||
|
<Paper p="md" withBorder radius="md" h="100%">
|
||||||
|
<Stack align="center" justify="center" h="100%">
|
||||||
|
<Text c="dimmed" size="sm">
|
||||||
|
暂无纪念日
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
size="xs"
|
||||||
|
leftSection={<IconPlus size={14} />}
|
||||||
|
onClick={onAddClick}
|
||||||
|
>
|
||||||
|
添加纪念日
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper p="md" withBorder radius="md" h="100%">
|
||||||
|
<Group justify="space-between" mb="sm">
|
||||||
|
<Text fw={500} size="sm">
|
||||||
|
纪念日
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
size="xs"
|
||||||
|
leftSection={<IconPlus size={14} />}
|
||||||
|
onClick={onAddClick}
|
||||||
|
>
|
||||||
|
添加
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Stack gap="xs" style={{ maxHeight: 'calc(100% - 40px)', overflowY: 'auto' }}>
|
||||||
|
{anniversaries.map((event) => (
|
||||||
|
<AnniversaryCard key={event.id} event={event} onClick={() => onEventClick(event)} />
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
99
src/components/note/NoteEditor.tsx
Normal file
99
src/components/note/NoteEditor.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { Paper, Textarea, Group, Text, Stack } from '@mantine/core';
|
||||||
|
import { useDebouncedValue } from '@mantine/hooks';
|
||||||
|
import { useAppStore } from '../../stores';
|
||||||
|
|
||||||
|
interface NoteEditorProps {
|
||||||
|
onSave?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NoteEditor({ onSave }: NoteEditorProps) {
|
||||||
|
const notes = useAppStore((state) => state.notes);
|
||||||
|
const updateNotesContent = useAppStore((state) => state.updateNotesContent);
|
||||||
|
const saveNotes = useAppStore((state) => state.saveNotes);
|
||||||
|
const fetchNotes = useAppStore((state) => state.fetchNotes);
|
||||||
|
|
||||||
|
const [content, setContent] = useState('');
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [lastSaved, setLastSaved] = useState<Date | null>(null);
|
||||||
|
|
||||||
|
// Initialize content from notes
|
||||||
|
useEffect(() => {
|
||||||
|
if (notes) {
|
||||||
|
setContent(notes.content);
|
||||||
|
}
|
||||||
|
}, [notes]);
|
||||||
|
|
||||||
|
// Fetch notes on mount
|
||||||
|
useEffect(() => {
|
||||||
|
fetchNotes();
|
||||||
|
}, [fetchNotes]);
|
||||||
|
|
||||||
|
// Auto-save with debounce
|
||||||
|
const [debouncedContent] = useDebouncedValue(content, 1000);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (debouncedContent !== undefined && notes) {
|
||||||
|
handleSave(debouncedContent);
|
||||||
|
}
|
||||||
|
}, [debouncedContent]);
|
||||||
|
|
||||||
|
const handleSave = useCallback(
|
||||||
|
async (value: string) => {
|
||||||
|
if (!notes) return;
|
||||||
|
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
updateNotesContent(value);
|
||||||
|
await saveNotes(value);
|
||||||
|
setLastSaved(new Date());
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save note:', error);
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[notes, updateNotesContent, saveNotes]
|
||||||
|
);
|
||||||
|
|
||||||
|
const formatLastSaved = () => {
|
||||||
|
if (!lastSaved) return '未保存';
|
||||||
|
const now = new Date();
|
||||||
|
const diff = now.getTime() - lastSaved.getTime();
|
||||||
|
if (diff < 1000) return '刚刚保存';
|
||||||
|
if (diff < 60000) return `${Math.floor(diff / 1000)}秒前保存`;
|
||||||
|
return lastSaved.toLocaleTimeString('zh-CN');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper p="md" withBorder radius="md" h="100%">
|
||||||
|
<Stack gap="sm" h="100%">
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Text fw={500} size="sm">
|
||||||
|
便签
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c={saving ? 'yellow' : lastSaved ? 'dimmed' : 'gray'}>
|
||||||
|
{saving ? '保存中...' : formatLastSaved()}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
value={content}
|
||||||
|
onChange={(e) => setContent(e.target.value)}
|
||||||
|
placeholder="在这里记录你的想法..."
|
||||||
|
autosize
|
||||||
|
minRows={8}
|
||||||
|
maxRows={20}
|
||||||
|
styles={{
|
||||||
|
input: {
|
||||||
|
border: 'none',
|
||||||
|
resize: 'none',
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
'&:focus': { outline: 'none' },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
54
src/components/reminder/ReminderCard.tsx
Normal file
54
src/components/reminder/ReminderCard.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { Card, Text, Checkbox, Group, Stack } from '@mantine/core';
|
||||||
|
import type { Event } from '../../types';
|
||||||
|
|
||||||
|
interface ReminderCardProps {
|
||||||
|
event: Event;
|
||||||
|
onToggle: () => void;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReminderCard({ event, onToggle, onClick }: ReminderCardProps) {
|
||||||
|
const isCompleted = event.is_completed ?? false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
shadow="sm"
|
||||||
|
padding="sm"
|
||||||
|
radius="md"
|
||||||
|
onClick={onClick}
|
||||||
|
style={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
opacity: isCompleted ? 0.6 : 1,
|
||||||
|
transition: 'transform 0.2s',
|
||||||
|
textDecoration: isCompleted ? 'line-through' : 'none',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => (e.currentTarget.style.transform = 'translateY(-2px)')}
|
||||||
|
onMouseLeave={(e) => (e.currentTarget.style.transform = 'translateY(0)')}
|
||||||
|
>
|
||||||
|
<Group justify="space-between" wrap="nowrap">
|
||||||
|
<Stack gap={4}>
|
||||||
|
<Text fw={500} size="sm" lineClamp={1}>
|
||||||
|
{event.title}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{new Date(event.date).toLocaleString('zh-CN')}
|
||||||
|
</Text>
|
||||||
|
{event.content && (
|
||||||
|
<Text size="xs" c="dimmed" lineClamp={1}>
|
||||||
|
{event.content}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
checked={isCompleted}
|
||||||
|
onChange={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onToggle();
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
172
src/components/reminder/ReminderList.tsx
Normal file
172
src/components/reminder/ReminderList.tsx
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Stack, Text, Paper, Group, Button } from '@mantine/core';
|
||||||
|
import { IconPlus } from '@tabler/icons-react';
|
||||||
|
import { ReminderCard } from './ReminderCard';
|
||||||
|
import type { Event } from '../../types';
|
||||||
|
|
||||||
|
interface ReminderListProps {
|
||||||
|
events: Event[];
|
||||||
|
onEventClick: (event: Event) => void;
|
||||||
|
onToggleComplete: (event: Event) => void;
|
||||||
|
onAddClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReminderList({
|
||||||
|
events,
|
||||||
|
onEventClick,
|
||||||
|
onToggleComplete,
|
||||||
|
onAddClick,
|
||||||
|
}: ReminderListProps) {
|
||||||
|
const grouped = useMemo(() => {
|
||||||
|
const now = new Date();
|
||||||
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||||
|
const tomorrow = new Date(today);
|
||||||
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||||
|
|
||||||
|
const reminders = events.filter((e) => e.type === 'reminder');
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
today: [] as Event[],
|
||||||
|
tomorrow: [] as Event[],
|
||||||
|
later: [] as Event[],
|
||||||
|
missed: [] as Event[],
|
||||||
|
};
|
||||||
|
|
||||||
|
reminders.forEach((event) => {
|
||||||
|
const eventDate = new Date(event.date);
|
||||||
|
|
||||||
|
if (event.is_completed) return;
|
||||||
|
|
||||||
|
if (eventDate < today) {
|
||||||
|
result.missed.push(event);
|
||||||
|
} else if (eventDate < tomorrow) {
|
||||||
|
result.today.push(event);
|
||||||
|
} else {
|
||||||
|
result.later.push(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort by date
|
||||||
|
Object.keys(result).forEach((key) => {
|
||||||
|
result[key as keyof typeof result].sort(
|
||||||
|
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, [events]);
|
||||||
|
|
||||||
|
const hasReminders =
|
||||||
|
grouped.today.length > 0 ||
|
||||||
|
grouped.tomorrow.length > 0 ||
|
||||||
|
grouped.later.length > 0 ||
|
||||||
|
grouped.missed.length > 0;
|
||||||
|
|
||||||
|
if (!hasReminders) {
|
||||||
|
return (
|
||||||
|
<Paper p="md" withBorder radius="md" h="100%">
|
||||||
|
<Stack align="center" justify="center" h="100%">
|
||||||
|
<Text c="dimmed" size="sm">
|
||||||
|
暂无提醒
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
size="xs"
|
||||||
|
leftSection={<IconPlus size={14} />}
|
||||||
|
onClick={onAddClick}
|
||||||
|
>
|
||||||
|
添加提醒
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper p="md" withBorder radius="md" h="100%">
|
||||||
|
<Group justify="space-between" mb="sm">
|
||||||
|
<Text fw={500} size="sm">
|
||||||
|
提醒
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
size="xs"
|
||||||
|
leftSection={<IconPlus size={14} />}
|
||||||
|
onClick={onAddClick}
|
||||||
|
>
|
||||||
|
添加
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Stack gap="xs" style={{ maxHeight: 'calc(100% - 40px)', overflowY: 'auto' }}>
|
||||||
|
{/* Missed reminders */}
|
||||||
|
{grouped.missed.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Text size="xs" c="red" fw={500}>
|
||||||
|
已错过
|
||||||
|
</Text>
|
||||||
|
{grouped.missed.map((event) => (
|
||||||
|
<ReminderCard
|
||||||
|
key={event.id}
|
||||||
|
event={event}
|
||||||
|
onClick={() => onEventClick(event)}
|
||||||
|
onToggle={() => onToggleComplete(event)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Today's reminders */}
|
||||||
|
{grouped.today.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Text size="xs" c="blue" fw={500}>
|
||||||
|
今天
|
||||||
|
</Text>
|
||||||
|
{grouped.today.map((event) => (
|
||||||
|
<ReminderCard
|
||||||
|
key={event.id}
|
||||||
|
event={event}
|
||||||
|
onClick={() => onEventClick(event)}
|
||||||
|
onToggle={() => onToggleComplete(event)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tomorrow's reminders */}
|
||||||
|
{grouped.tomorrow.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Text size="xs" c="teal" fw={500}>
|
||||||
|
明天
|
||||||
|
</Text>
|
||||||
|
{grouped.tomorrow.map((event) => (
|
||||||
|
<ReminderCard
|
||||||
|
key={event.id}
|
||||||
|
event={event}
|
||||||
|
onClick={() => onEventClick(event)}
|
||||||
|
onToggle={() => onToggleComplete(event)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Later reminders */}
|
||||||
|
{grouped.later.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Text size="xs" c="gray" fw={500}>
|
||||||
|
更久之后
|
||||||
|
</Text>
|
||||||
|
{grouped.later.map((event) => (
|
||||||
|
<ReminderCard
|
||||||
|
key={event.id}
|
||||||
|
event={event}
|
||||||
|
onClick={() => onEventClick(event)}
|
||||||
|
onToggle={() => onToggleComplete(event)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,36 +1,164 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Container, Grid, Title, Button, Group, Text } from '@mantine/core';
|
import {
|
||||||
import { IconLogout } from '@tabler/icons-react';
|
Container,
|
||||||
|
Grid,
|
||||||
|
Title,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Text,
|
||||||
|
Modal,
|
||||||
|
TextInput,
|
||||||
|
Textarea,
|
||||||
|
Switch,
|
||||||
|
Select,
|
||||||
|
Stack,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { DateInput, TimeInput } from '@mantine/dates';
|
||||||
|
import { IconLogout, IconPlus } from '@tabler/icons-react';
|
||||||
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import { useAppStore } from '../stores';
|
import { useAppStore } from '../stores';
|
||||||
|
import { AnniversaryList } from '../components/anniversary/AnniversaryList';
|
||||||
|
import { ReminderList } from '../components/reminder/ReminderList';
|
||||||
|
import { NoteEditor } from '../components/note/NoteEditor';
|
||||||
|
import { AIChatBox } from '../components/ai/AIChatBox';
|
||||||
|
import type { Event, EventType, RepeatType } from '../types';
|
||||||
|
|
||||||
export function HomePage() {
|
export function HomePage() {
|
||||||
const user = useAppStore((state) => state.user);
|
const user = useAppStore((state) => state.user);
|
||||||
const logout = useAppStore((state) => state.logout);
|
const logout = useAppStore((state) => state.logout);
|
||||||
const checkAuth = useAppStore((state) => state.checkAuth);
|
const checkAuth = useAppStore((state) => state.checkAuth);
|
||||||
|
const events = useAppStore((state) => state.events);
|
||||||
|
const fetchEvents = useAppStore((state) => state.fetchEvents);
|
||||||
|
const createEvent = useAppStore((state) => state.createEvent);
|
||||||
|
const updateEventById = useAppStore((state) => state.updateEventById);
|
||||||
|
const deleteEventById = useAppStore((state) => state.deleteEventById);
|
||||||
|
|
||||||
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
|
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
|
||||||
|
const [isEdit, setIsEdit] = useState(false);
|
||||||
|
|
||||||
|
// Form state
|
||||||
|
const [formType, setFormType] = useState<EventType>('anniversary');
|
||||||
|
const [formTitle, setFormTitle] = useState('');
|
||||||
|
const [formContent, setFormContent] = useState('');
|
||||||
|
const [formDate, setFormDate] = useState<Date | null>(null);
|
||||||
|
const [formTime, setFormTime] = useState('');
|
||||||
|
const [formIsLunar, setFormIsLunar] = useState(false);
|
||||||
|
const [formRepeatType, setFormRepeatType] = useState<RepeatType>('none');
|
||||||
|
const [formIsHoliday, setFormIsHoliday] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkAuth();
|
checkAuth();
|
||||||
}, [checkAuth]);
|
fetchEvents();
|
||||||
|
}, [checkAuth, fetchEvents]);
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
await logout();
|
await logout();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEventClick = (event: Event) => {
|
||||||
|
setSelectedEvent(event);
|
||||||
|
setIsEdit(true);
|
||||||
|
setFormType(event.type);
|
||||||
|
setFormTitle(event.title);
|
||||||
|
setFormContent(event.content || '');
|
||||||
|
setFormDate(new Date(event.date));
|
||||||
|
setFormIsLunar(event.is_lunar);
|
||||||
|
setFormRepeatType(event.repeat_type);
|
||||||
|
setFormIsHoliday(event.is_holiday || false);
|
||||||
|
open();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddClick = (type: EventType) => {
|
||||||
|
setSelectedEvent(null);
|
||||||
|
setIsEdit(false);
|
||||||
|
setFormType(type);
|
||||||
|
setFormTitle('');
|
||||||
|
setFormContent('');
|
||||||
|
setFormDate(null);
|
||||||
|
setFormTime('');
|
||||||
|
setFormIsLunar(false);
|
||||||
|
setFormRepeatType('none');
|
||||||
|
setFormIsHoliday(type === 'anniversary');
|
||||||
|
open();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formTitle.trim() || !formDate) return;
|
||||||
|
|
||||||
|
const dateStr = formTime
|
||||||
|
? new Date(formDate.setHours(parseInt(formTime.split(':')[0]), parseInt(formTime.split(':')[1])))
|
||||||
|
: formDate;
|
||||||
|
|
||||||
|
const eventData = {
|
||||||
|
type: formType,
|
||||||
|
title: formTitle,
|
||||||
|
content: formContent || undefined,
|
||||||
|
date: dateStr.toISOString(),
|
||||||
|
is_lunar: formIsLunar,
|
||||||
|
repeat_type: formRepeatType,
|
||||||
|
is_holiday: formIsHoliday || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEdit && selectedEvent) {
|
||||||
|
await updateEventById(selectedEvent.id, eventData);
|
||||||
|
} else {
|
||||||
|
await createEvent(eventData);
|
||||||
|
}
|
||||||
|
|
||||||
|
close();
|
||||||
|
resetForm();
|
||||||
|
fetchEvents();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (!selectedEvent) return;
|
||||||
|
await deleteEventById(selectedEvent.id);
|
||||||
|
close();
|
||||||
|
resetForm();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleComplete = async (event: Event) => {
|
||||||
|
if (event.type !== 'reminder') return;
|
||||||
|
await updateEventById(event.id, {
|
||||||
|
is_completed: !event.is_completed,
|
||||||
|
});
|
||||||
|
fetchEvents();
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
setFormType('anniversary');
|
||||||
|
setFormTitle('');
|
||||||
|
setFormContent('');
|
||||||
|
setFormDate(null);
|
||||||
|
setFormTime('');
|
||||||
|
setFormIsLunar(false);
|
||||||
|
setFormRepeatType('none');
|
||||||
|
setFormIsHoliday(false);
|
||||||
|
setSelectedEvent(null);
|
||||||
|
setIsEdit(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAIEventCreated = () => {
|
||||||
|
fetchEvents();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container size="xl" py="md" style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
<Container size="xl" py="md" h="100vh" style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb="md" style={{ flexShrink: 0 }}>
|
||||||
<Title order={2} c="blue">
|
<Title order={2} c="blue">
|
||||||
掐日子
|
掐日子
|
||||||
</Title>
|
</Title>
|
||||||
<Group>
|
<Group>
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
{user?.email}
|
{user?.nickname || user?.email}
|
||||||
</Text>
|
</Text>
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
color="gray"
|
color="gray"
|
||||||
leftSection={<IconLogout size={16} />}
|
size="xs"
|
||||||
|
leftSection={<IconLogout size={14} />}
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
>
|
>
|
||||||
退出
|
退出
|
||||||
@ -38,12 +166,158 @@ export function HomePage() {
|
|||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{/* Main Content - Placeholder for now */}
|
{/* Main Content - 4 panel layout */}
|
||||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
<Grid grow style={{ flex: 1, minHeight: 0 }}>
|
||||||
<Text c="dimmed">
|
{/* Left column - 40% */}
|
||||||
Home Page - 待开发
|
<Grid.Col span={4}>
|
||||||
</Text>
|
<Stack gap="md" h="100%">
|
||||||
</div>
|
{/* Anniversary list */}
|
||||||
|
<div style={{ flex: 1, minHeight: 0 }}>
|
||||||
|
<AnniversaryList
|
||||||
|
events={events}
|
||||||
|
onEventClick={handleEventClick}
|
||||||
|
onAddClick={() => handleAddClick('anniversary')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Reminder list */}
|
||||||
|
<div style={{ flex: 1, minHeight: 0 }}>
|
||||||
|
<ReminderList
|
||||||
|
events={events}
|
||||||
|
onEventClick={handleEventClick}
|
||||||
|
onToggleComplete={handleToggleComplete}
|
||||||
|
onAddClick={() => handleAddClick('reminder')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
{/* Right column - 60% */}
|
||||||
|
<Grid.Col span={8}>
|
||||||
|
<Stack gap="md" h="100%">
|
||||||
|
{/* Note editor */}
|
||||||
|
<div style={{ flex: 1, minHeight: 0 }}>
|
||||||
|
<NoteEditor />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* AI Chat */}
|
||||||
|
<div style={{ flex: 1, minHeight: 0 }}>
|
||||||
|
<AIChatBox onEventCreated={handleAIEventCreated} />
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Add/Edit Event Modal */}
|
||||||
|
<Modal
|
||||||
|
opened={opened}
|
||||||
|
onClose={close}
|
||||||
|
title={
|
||||||
|
<Group gap={8}>
|
||||||
|
<IconPlus size={18} />
|
||||||
|
<Text fw={500}>{isEdit ? '编辑事件' : '添加事件'}</Text>
|
||||||
|
</Group>
|
||||||
|
}
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Event type */}
|
||||||
|
<Select
|
||||||
|
label="类型"
|
||||||
|
data={[
|
||||||
|
{ value: 'anniversary', label: '纪念日' },
|
||||||
|
{ value: 'reminder', label: '提醒' },
|
||||||
|
]}
|
||||||
|
value={formType}
|
||||||
|
onChange={(value) => value && setFormType(value as EventType)}
|
||||||
|
disabled={isEdit}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<TextInput
|
||||||
|
label="标题"
|
||||||
|
placeholder="输入标题"
|
||||||
|
value={formTitle}
|
||||||
|
onChange={(e) => setFormTitle(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Content (only for reminders) */}
|
||||||
|
{formType === 'reminder' && (
|
||||||
|
<Textarea
|
||||||
|
label="内容"
|
||||||
|
placeholder="输入详细内容"
|
||||||
|
value={formContent}
|
||||||
|
onChange={(e) => setFormContent(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Date */}
|
||||||
|
<DateInput
|
||||||
|
label="日期"
|
||||||
|
placeholder="选择日期"
|
||||||
|
value={formDate}
|
||||||
|
onChange={setFormDate}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Time (only for reminders) */}
|
||||||
|
{formType === 'reminder' && (
|
||||||
|
<TimeInput
|
||||||
|
label="时间"
|
||||||
|
placeholder="选择时间"
|
||||||
|
value={formTime}
|
||||||
|
onChange={(e) => setFormTime(e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Lunar switch */}
|
||||||
|
<Switch
|
||||||
|
label="农历日期"
|
||||||
|
checked={formIsLunar}
|
||||||
|
onChange={(e) => setFormIsLunar(e.currentTarget.checked)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Repeat type */}
|
||||||
|
<Select
|
||||||
|
label="重复"
|
||||||
|
data={[
|
||||||
|
{ value: 'none', label: '不重复' },
|
||||||
|
{ value: 'yearly', label: '每年' },
|
||||||
|
{ value: 'monthly', label: '每月' },
|
||||||
|
]}
|
||||||
|
value={formRepeatType}
|
||||||
|
onChange={(value) => value && setFormRepeatType(value as RepeatType)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Holiday switch (only for anniversaries) */}
|
||||||
|
{formType === 'anniversary' && (
|
||||||
|
<Switch
|
||||||
|
label="节假日"
|
||||||
|
checked={formIsHoliday}
|
||||||
|
onChange={(e) => setFormIsHoliday(e.currentTarget.checked)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<Group justify="space-between" mt="md">
|
||||||
|
{isEdit && (
|
||||||
|
<Button color="red" variant="light" onClick={handleDelete}>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Group ml="auto">
|
||||||
|
<Button variant="subtle" onClick={close}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSubmit} disabled={!formTitle.trim() || !formDate}>
|
||||||
|
{isEdit ? '保存' : '添加'}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { persist } from 'zustand/middleware';
|
import { persist } from 'zustand/middleware';
|
||||||
import type { User, Event, Note, AIConversation } from '../types';
|
import type { User, Event, Note, AIConversation, EventType } from '../types';
|
||||||
import { api } from '../services/api';
|
import { api } from '../services/api';
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
@ -24,6 +24,15 @@ interface AppState {
|
|||||||
setNotes: (notes: Note | null) => void;
|
setNotes: (notes: Note | null) => void;
|
||||||
updateNotesContent: (content: string) => void;
|
updateNotesContent: (content: string) => void;
|
||||||
setConversations: (conversations: AIConversation[]) => void;
|
setConversations: (conversations: AIConversation[]) => void;
|
||||||
|
addConversation: (conversation: AIConversation) => void;
|
||||||
|
|
||||||
|
// Data fetch actions
|
||||||
|
fetchEvents: (type?: EventType) => Promise<void>;
|
||||||
|
fetchNotes: () => Promise<void>;
|
||||||
|
saveNotes: (content: string) => Promise<void>;
|
||||||
|
createEvent: (event: Partial<Event>) => Promise<{ error: any }>;
|
||||||
|
updateEventById: (id: string, event: Partial<Event>) => Promise<{ error: any }>;
|
||||||
|
deleteEventById: (id: string) => Promise<{ error: any }>;
|
||||||
|
|
||||||
// Auth actions
|
// Auth actions
|
||||||
login: (email: string, password: string) => Promise<{ error: any }>;
|
login: (email: string, password: string) => Promise<{ error: any }>;
|
||||||
@ -59,6 +68,72 @@ export const useAppStore = create<AppState>()(
|
|||||||
notes: state.notes ? { ...state.notes, content } : null,
|
notes: state.notes ? { ...state.notes, content } : null,
|
||||||
})),
|
})),
|
||||||
setConversations: (conversations) => set({ conversations }),
|
setConversations: (conversations) => set({ conversations }),
|
||||||
|
addConversation: (conversation) => set((state) => ({
|
||||||
|
conversations: [conversation, ...state.conversations],
|
||||||
|
})),
|
||||||
|
|
||||||
|
// Data fetch actions
|
||||||
|
fetchEvents: async (type) => {
|
||||||
|
try {
|
||||||
|
const events = await api.events.list(type);
|
||||||
|
set({ events });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch events:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchNotes: async () => {
|
||||||
|
try {
|
||||||
|
const notes = await api.notes.get();
|
||||||
|
set({ notes });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch notes:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
saveNotes: async (content) => {
|
||||||
|
try {
|
||||||
|
const notes = await api.notes.save(content);
|
||||||
|
set({ notes });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save notes:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
createEvent: async (event) => {
|
||||||
|
try {
|
||||||
|
const newEvent = await api.events.create(event);
|
||||||
|
set((state) => ({ events: [...state.events, newEvent] }));
|
||||||
|
return { error: null };
|
||||||
|
} catch (error: any) {
|
||||||
|
return { error: error.message || '创建失败' };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateEventById: async (id, event) => {
|
||||||
|
try {
|
||||||
|
const updated = await api.events.update(id, event);
|
||||||
|
set((state) => ({
|
||||||
|
events: state.events.map((e) => (e.id === id ? updated : e)),
|
||||||
|
}));
|
||||||
|
return { error: null };
|
||||||
|
} catch (error: any) {
|
||||||
|
return { error: error.message || '更新失败' };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteEventById: async (id) => {
|
||||||
|
try {
|
||||||
|
await api.events.delete(id);
|
||||||
|
set((state) => ({
|
||||||
|
events: state.events.filter((e) => e.id !== id),
|
||||||
|
}));
|
||||||
|
return { error: null };
|
||||||
|
} catch (error: any) {
|
||||||
|
return { error: error.message || '删除失败' };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Auth actions
|
// Auth actions
|
||||||
login: async (email, password) => {
|
login: async (email, password) => {
|
||||||
|
|||||||
@ -12,32 +12,22 @@ export type EventType = 'anniversary' | 'reminder';
|
|||||||
|
|
||||||
export type RepeatType = 'yearly' | 'monthly' | 'none';
|
export type RepeatType = 'yearly' | 'monthly' | 'none';
|
||||||
|
|
||||||
export interface BaseEvent {
|
// Unified event type (matches backend API)
|
||||||
|
export interface Event {
|
||||||
id: string;
|
id: string;
|
||||||
user_id: string;
|
user_id: string;
|
||||||
|
type: EventType;
|
||||||
title: string;
|
title: string;
|
||||||
date: string; // ISO date string
|
content?: string; // Only for reminders
|
||||||
|
date: string; // For anniversaries: date, For reminders: reminder time
|
||||||
is_lunar: boolean;
|
is_lunar: boolean;
|
||||||
repeat_type: RepeatType;
|
repeat_type: RepeatType;
|
||||||
|
is_holiday?: boolean; // Only for anniversaries
|
||||||
|
is_completed?: boolean; // Only for reminders
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Anniversary extends BaseEvent {
|
|
||||||
type: 'anniversary';
|
|
||||||
is_holiday: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Reminder extends BaseEvent {
|
|
||||||
type: 'reminder';
|
|
||||||
content: string;
|
|
||||||
reminder_time: string; // ISO datetime string
|
|
||||||
is_completed: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Combined event type for lists
|
|
||||||
export type Event = Anniversary | Reminder;
|
|
||||||
|
|
||||||
// Note types
|
// Note types
|
||||||
export interface Note {
|
export interface Note {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user