feat: 完善提醒卡片右键菜单功能
- 优化checkbox样式:缩小尺寸(14px)、移除阴影、添加白色填充
- 调整布局:标题和内容左对齐
- 重构右键菜单为垂直分类布局:调整时间/颜色/操作
- 添加菜单边缘保护:自动计算位置避免超出浏览器
- 添加点击外部和ESC键关闭菜单
- 编辑弹窗优先级改为颜色圆点选择器
- 添加priority类型定义
🤖 Generated with Claude Code
This commit is contained in:
parent
1559e603b0
commit
f0cbd0e33c
@ -1,8 +1,47 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Paper, Text, Checkbox, Group, Stack, ActionIcon, Box } from '@mantine/core';
|
||||
import { IconDots, IconArrowForward, IconRepeat, IconRepeatOff } from '@tabler/icons-react';
|
||||
import type { Event, RepeatType } from '../../types';
|
||||
import { useMemo, useState, useCallback, useEffect, useRef } from 'react';
|
||||
import {
|
||||
Paper,
|
||||
Text,
|
||||
Group,
|
||||
Stack,
|
||||
ActionIcon,
|
||||
Box,
|
||||
Menu,
|
||||
Divider,
|
||||
Portal,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconDots,
|
||||
IconArrowForward,
|
||||
IconRepeat,
|
||||
IconCalendar,
|
||||
IconEdit,
|
||||
IconTrash,
|
||||
IconCircleFilled,
|
||||
IconCheck,
|
||||
} from '@tabler/icons-react';
|
||||
import type { Event, PriorityType } from '../../types';
|
||||
import { getRepeatTypeLabel } from '../../utils/repeatCalculator';
|
||||
import { useContextMenuStore } from '../../stores/contextMenu';
|
||||
|
||||
// 重复图标颜色常量
|
||||
const REPEAT_ICON_COLOR = '#999';
|
||||
|
||||
// 优先级颜色映射
|
||||
const PRIORITY_COLORS: Record<PriorityType, string> = {
|
||||
none: 'rgba(0, 0, 0, 0.15)',
|
||||
red: '#dc2626',
|
||||
green: '#16a34a',
|
||||
yellow: '#ca8a04',
|
||||
};
|
||||
|
||||
// 优先级名称映射
|
||||
const PRIORITY_NAMES: Record<PriorityType, string> = {
|
||||
none: '默认',
|
||||
red: '红色',
|
||||
green: '绿色',
|
||||
yellow: '黄色',
|
||||
};
|
||||
|
||||
interface ReminderCardProps {
|
||||
event: Event;
|
||||
@ -12,12 +51,99 @@ interface ReminderCardProps {
|
||||
onPostpone?: () => void;
|
||||
isMissed?: boolean;
|
||||
onMissedToggle?: () => void;
|
||||
onDateChange?: (date: string, repeatType: string, repeatInterval: number | null) => void;
|
||||
onPriorityChange?: (priority: PriorityType) => void;
|
||||
}
|
||||
|
||||
export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, isMissed = false, onMissedToggle }: ReminderCardProps) {
|
||||
export function ReminderCard({
|
||||
event,
|
||||
onToggle,
|
||||
onClick,
|
||||
onDelete,
|
||||
onPostpone,
|
||||
isMissed = false,
|
||||
onMissedToggle,
|
||||
onDateChange,
|
||||
onPriorityChange,
|
||||
}: ReminderCardProps) {
|
||||
const isCompleted = event.is_completed ?? false;
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
const cardRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 使用全局状态管理右键菜单
|
||||
const openEventId = useContextMenuStore((state) => state.openEventId);
|
||||
const { openMenu, closeMenu } = useContextMenuStore();
|
||||
const isMenuOpen = openEventId === event.id;
|
||||
|
||||
// 获取优先级颜色
|
||||
const getPriorityColor = (priority?: PriorityType): string => {
|
||||
return PRIORITY_COLORS[priority || 'none'];
|
||||
};
|
||||
|
||||
// 右键点击处理
|
||||
const handleContextMenu = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
console.log('[ContextMenu] 右键点击事件, eventId:', event.id);
|
||||
|
||||
// 打开菜单
|
||||
openMenu(event.id);
|
||||
}, [event.id, openMenu]);
|
||||
|
||||
// 点击外部关闭菜单
|
||||
useEffect(() => {
|
||||
if (!isMenuOpen) return;
|
||||
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
// 如果点击不在当前卡片和菜单内,关闭菜单
|
||||
if (!target.closest(`[data-reminder-card="${event.id}"]`) &&
|
||||
!target.closest('[data-context-menu]')) {
|
||||
closeMenu();
|
||||
}
|
||||
};
|
||||
|
||||
// ESC键关闭菜单
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeMenu();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleMouseDown);
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleMouseDown);
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [isMenuOpen, event.id, closeMenu]);
|
||||
|
||||
// 计算菜单位置(带边缘保护)
|
||||
const getMenuPosition = () => {
|
||||
if (!cardRef.current) return { left: 0, top: 0 };
|
||||
|
||||
const cardRect = cardRef.current.getBoundingClientRect();
|
||||
const menuWidth = 180; // 菜单宽度
|
||||
const menuHeight = 150; // 估算菜单高度
|
||||
const padding = 8; // 边缘padding
|
||||
|
||||
// 计算左侧位置
|
||||
let left = cardRect.left;
|
||||
if (left + menuWidth > window.innerWidth - padding) {
|
||||
left = window.innerWidth - menuWidth - padding;
|
||||
}
|
||||
if (left < padding) left = padding;
|
||||
|
||||
// 计算顶部位置(优先显示在下方)
|
||||
let top = cardRect.bottom + 4;
|
||||
if (top + menuHeight > window.innerHeight - padding) {
|
||||
// 如果下方空间不足,显示在上方
|
||||
top = cardRect.top - menuHeight - 4;
|
||||
}
|
||||
if (top < padding) top = padding;
|
||||
|
||||
return { left, top };
|
||||
};
|
||||
|
||||
// 计算时间信息
|
||||
const timeInfo = useMemo(() => {
|
||||
@ -33,7 +159,6 @@ export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, i
|
||||
const eventDate = new Date(dateStr);
|
||||
|
||||
// 检查是否包含有效时间(小时和分钟不全为0)
|
||||
// 对于本地格式(如 "2025-02-05T00:00:00")和 ISO 格式都适用
|
||||
const hours = eventDate.getHours();
|
||||
const minutes = eventDate.getMinutes();
|
||||
const hasExplicitTime = hours !== 0 || minutes !== 0;
|
||||
@ -45,7 +170,6 @@ export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, i
|
||||
// 格式化显示
|
||||
let timeStr: string;
|
||||
if (hasExplicitTime) {
|
||||
// 有设置时间,显示日期+时间
|
||||
timeStr = eventDate.toLocaleString('zh-CN', {
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
@ -54,7 +178,6 @@ export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, i
|
||||
hour12: false,
|
||||
});
|
||||
} else {
|
||||
// 没有设置时间(00:00),只显示日期
|
||||
timeStr = eventDate.toLocaleString('zh-CN', {
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
@ -92,156 +215,213 @@ export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, i
|
||||
return 'rgba(0, 0, 0, 0.06)';
|
||||
};
|
||||
|
||||
// 获取循环图标颜色
|
||||
const getRepeatIconColor = (type: RepeatType) => {
|
||||
const colors: Record<RepeatType, string> = {
|
||||
daily: '#3b82f6', // 蓝色
|
||||
weekly: '#22c55e', // 绿色
|
||||
monthly: '#a855f7', // 紫色
|
||||
yearly: '#f59e0b', // 橙色
|
||||
none: '#999',
|
||||
// 获取优先级的checkbox边框样式
|
||||
const getCheckboxBorderStyle = () => {
|
||||
const priority = event.priority || 'none';
|
||||
const priorityColor = getPriorityColor(priority);
|
||||
const isHighPriority = priority !== 'none';
|
||||
return {
|
||||
borderColor: priorityColor,
|
||||
borderWidth: isHighPriority ? '2px' : '1px',
|
||||
borderStyle: 'solid' as const,
|
||||
};
|
||||
return colors[type] || '#999';
|
||||
};
|
||||
|
||||
// 处理日期调整
|
||||
const handleDateAdjust = useCallback((days: number) => {
|
||||
const now = new Date();
|
||||
const targetDate = new Date(now);
|
||||
targetDate.setDate(targetDate.getDate() + days);
|
||||
|
||||
// 格式化为 YYYY-MM-DD
|
||||
const formattedDate = targetDate.toISOString().split('T')[0];
|
||||
onDateChange?.(formattedDate, event.repeat_type, event.repeat_interval ?? null);
|
||||
closeMenu();
|
||||
}, [onDateChange, event.repeat_type, event.repeat_interval, closeMenu]);
|
||||
|
||||
// 处理优先级设置
|
||||
const handlePriorityChange = useCallback((priority: PriorityType) => {
|
||||
onPriorityChange?.(priority);
|
||||
closeMenu();
|
||||
}, [onPriorityChange, closeMenu]);
|
||||
|
||||
return (
|
||||
<Paper
|
||||
p="sm"
|
||||
radius={2}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
opacity: isAnimating ? 0 : isCompleted ? 0.4 : 1,
|
||||
transform: isHovered ? 'translateY(-1px)' : 'translateY(0)',
|
||||
background: getBackground(),
|
||||
border: `1px solid ${getBorderColor()}`,
|
||||
borderLeft: isMissed && !isCompleted ? '3px solid #c41c1c' : undefined,
|
||||
transition: 'all 0.3s ease',
|
||||
animation: isAnimating ? 'reminder-card-fadeOut 0.3s ease-out forwards' : 'none',
|
||||
}}
|
||||
>
|
||||
<style>{`
|
||||
@keyframes reminder-card-pulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.15); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
@keyframes reminder-card-fadeOut {
|
||||
0% { opacity: 1; transform: translateX(0); }
|
||||
100% { opacity: 0; transform: translateX(20px); }
|
||||
}
|
||||
`}</style>
|
||||
{/* 正常提醒卡片:左右分栏布局 */}
|
||||
{!isMissed ? (
|
||||
<Group justify="space-between" wrap="nowrap" align="flex-start">
|
||||
{/* 左侧:Checkbox + 标题 + 内容 */}
|
||||
<Box
|
||||
style={{ flex: 1, minWidth: 0, cursor: 'pointer' }}
|
||||
onClick={(e) => {
|
||||
// 阻止事件冒泡到 Card,避免重复触发
|
||||
e.stopPropagation();
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
<Group gap="sm" wrap="nowrap" align="flex-start">
|
||||
{/* Checkbox - 单独处理,不触发卡片点击 */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
paddingTop: 2,
|
||||
animation: isAnimating ? 'reminder-card-pulse 0.3s ease-out' : 'none',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
// 阻止事件冒泡,避免触发卡片点击
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isCompleted}
|
||||
onChange={(e) => {
|
||||
<>
|
||||
<Paper
|
||||
ref={cardRef}
|
||||
p="sm"
|
||||
radius={2}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
onContextMenu={handleContextMenu}
|
||||
data-reminder-card={event.id}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
opacity: isAnimating ? 0 : isCompleted ? 0.4 : 1,
|
||||
transform: isHovered ? 'translateY(-1px)' : 'translateY(0)',
|
||||
background: getBackground(),
|
||||
border: `1px solid ${getBorderColor()}`,
|
||||
borderLeft: isMissed && !isCompleted ? '3px solid #c41c1c' : undefined,
|
||||
transition: 'all 0.3s ease',
|
||||
animation: isAnimating ? 'reminder-card-fadeOut 0.3s ease-out forwards' : 'none',
|
||||
}}
|
||||
>
|
||||
<style>{`
|
||||
@keyframes reminder-card-pulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.15); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
@keyframes reminder-card-fadeOut {
|
||||
0% { opacity: 1; transform: translateX(0); }
|
||||
100% { opacity: 0; transform: translateX(20px); }
|
||||
}
|
||||
/* 自定义checkbox样式,去掉默认边框 */
|
||||
.reminder-checkbox input[type="checkbox"] {
|
||||
border-radius: 50% !important;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
.reminder-checkbox .mantine-Checkbox-icon {
|
||||
color: inherit !important;
|
||||
}
|
||||
`}</style>
|
||||
{/* 正常提醒卡片:左右分栏布局 */}
|
||||
{!isMissed ? (
|
||||
<Group justify="space-between" wrap="nowrap" align="flex-start">
|
||||
{/* 左侧:Checkbox + 标题 + 内容 */}
|
||||
<Box
|
||||
style={{ flex: 1, minWidth: 0, cursor: 'pointer' }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
<Box style={{ display: 'flex', alignItems: 'flex-start' }}>
|
||||
{/* Checkbox */}
|
||||
<div
|
||||
className="reminder-checkbox"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
animation: isAnimating ? 'reminder-card-pulse 0.3s ease-out' : 'none',
|
||||
width: 14,
|
||||
height: 14,
|
||||
borderRadius: '50%',
|
||||
border: `1px solid ${getPriorityColor(event.priority || 'none')}`,
|
||||
background: isCompleted ? getPriorityColor(event.priority || 'none') : '#ffffff',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
flexShrink: 0,
|
||||
position: 'relative',
|
||||
marginTop: 2,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggle();
|
||||
}}
|
||||
size="xs"
|
||||
color="#1a1a1a"
|
||||
>
|
||||
{/* 已完成时显示的对勾 */}
|
||||
{isCompleted && (
|
||||
<span
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
color: '#fff',
|
||||
fontSize: '8px',
|
||||
fontWeight: 'bold',
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
✓
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 标题和内容垂直排列 */}
|
||||
<Box style={{ flex: 1, marginLeft: 8 }}>
|
||||
{/* 标题 */}
|
||||
<Text
|
||||
fw={400}
|
||||
size="xs"
|
||||
lineClamp={1}
|
||||
style={{
|
||||
textDecoration: isCompleted ? 'line-through' : 'none',
|
||||
color: getTextColor(),
|
||||
letterSpacing: '0.03em',
|
||||
transition: 'color 0.2s ease',
|
||||
}}
|
||||
>
|
||||
{event.title}
|
||||
</Text>
|
||||
|
||||
{/* 内容在标题下方 */}
|
||||
{event.content && (
|
||||
<Text
|
||||
size="xs"
|
||||
c={isCompleted ? '#bbb' : '#999'}
|
||||
lineClamp={1}
|
||||
style={{
|
||||
marginTop: 2,
|
||||
opacity: isCompleted ? 0.6 : 1,
|
||||
transition: 'opacity 0.2s ease',
|
||||
}}
|
||||
>
|
||||
{event.content}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* 右侧:循环图标 + 日期时间 */}
|
||||
<Box style={{ flex: '0 0 auto', minWidth: 0, paddingLeft: 12, display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
{event.repeat_type !== 'none' && (
|
||||
<Box
|
||||
style={{
|
||||
animation: isAnimating ? 'reminder-card-pulse 0.3s ease-out' : 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: REPEAT_ICON_COLOR,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 标题 */}
|
||||
<Text
|
||||
fw={400}
|
||||
size="xs"
|
||||
lineClamp={1}
|
||||
style={{
|
||||
textDecoration: isCompleted ? 'line-through' : 'none',
|
||||
color: getTextColor(),
|
||||
letterSpacing: '0.03em',
|
||||
transition: 'color 0.2s ease',
|
||||
}}
|
||||
>
|
||||
{event.title}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
{/* 内容在标题下方 */}
|
||||
{event.content && (
|
||||
title={getRepeatTypeLabel(event.repeat_type)}
|
||||
>
|
||||
<IconRepeat size={12} />
|
||||
</Box>
|
||||
)}
|
||||
<Text
|
||||
size="xs"
|
||||
c={isCompleted ? '#bbb' : '#999'}
|
||||
lineClamp={1}
|
||||
style={{
|
||||
marginLeft: 28,
|
||||
opacity: isCompleted ? 0.6 : 1,
|
||||
transition: 'opacity 0.2s ease',
|
||||
}}
|
||||
c={getTimeColor()}
|
||||
style={{ letterSpacing: '0.05em', whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{event.content}
|
||||
{timeInfo.timeStr}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* 右侧:循环图标 + 日期时间 */}
|
||||
<Box style={{ flex: '0 0 auto', minWidth: 0, paddingLeft: 12, display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
{event.repeat_type !== 'none' && (
|
||||
<Box
|
||||
</Box>
|
||||
</Group>
|
||||
) : (
|
||||
/* 逾期提醒卡片 */
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Group gap="sm" style={{ flex: 1, minWidth: 0 }}>
|
||||
<div
|
||||
className="reminder-checkbox"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: getRepeatIconColor(event.repeat_type),
|
||||
justifyContent: 'center',
|
||||
animation: isAnimating ? 'reminder-card-pulse 0.3s ease-out' : 'none',
|
||||
width: 14,
|
||||
height: 14,
|
||||
borderRadius: '50%',
|
||||
border: `1px solid ${getPriorityColor(event.priority || 'none')}`,
|
||||
background: isCompleted ? getPriorityColor(event.priority || 'none') : '#ffffff',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
flexShrink: 0,
|
||||
position: 'relative',
|
||||
}}
|
||||
title={getRepeatTypeLabel(event.repeat_type)}
|
||||
>
|
||||
<IconRepeat size={12} />
|
||||
</Box>
|
||||
)}
|
||||
<Text
|
||||
size="xs"
|
||||
c={getTimeColor()}
|
||||
style={{ letterSpacing: '0.05em', whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{timeInfo.timeStr}
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
) : (
|
||||
/* 逾期提醒卡片:保持原有结构 */
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Group gap="sm" style={{ flex: 1, minWidth: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
animation: isAnimating ? 'reminder-card-pulse 0.3s ease-out' : 'none',
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isCompleted}
|
||||
onChange={(e) => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (isMissed && !isCompleted) {
|
||||
setIsAnimating(true);
|
||||
@ -254,103 +434,257 @@ export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, i
|
||||
onToggle();
|
||||
}
|
||||
}}
|
||||
size="xs"
|
||||
color="#1a1a1a"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Stack gap={2} style={{ flex: 1, minWidth: 0 }} onClick={onClick}>
|
||||
<Group gap={4} wrap="nowrap">
|
||||
<Text
|
||||
fw={400}
|
||||
size="xs"
|
||||
lineClamp={1}
|
||||
style={{
|
||||
textDecoration: isCompleted ? 'line-through' : 'none',
|
||||
color: getTextColor(),
|
||||
letterSpacing: '0.03em',
|
||||
transition: 'color 0.2s ease',
|
||||
}}
|
||||
>
|
||||
{event.title}
|
||||
</Text>
|
||||
{event.repeat_type !== 'none' && (
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexShrink: 0,
|
||||
color: getRepeatIconColor(event.repeat_type),
|
||||
}}
|
||||
title={getRepeatTypeLabel(event.repeat_type)}
|
||||
>
|
||||
<IconRepeat size={10} />
|
||||
</Box>
|
||||
)}
|
||||
</Group>
|
||||
<Text
|
||||
size="xs"
|
||||
c={getTimeColor()}
|
||||
style={{ letterSpacing: '0.05em' }}
|
||||
>
|
||||
{timeInfo.timeStr}
|
||||
</Text>
|
||||
{event.content && (
|
||||
{/* 已完成时显示的对勾 */}
|
||||
{isCompleted && (
|
||||
<span
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
color: '#fff',
|
||||
fontSize: '10px',
|
||||
fontWeight: 'bold',
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
✓
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Stack gap={2} style={{ flex: 1, minWidth: 0 }} onClick={onClick}>
|
||||
<Group gap={4} wrap="nowrap">
|
||||
<Text
|
||||
fw={400}
|
||||
size="xs"
|
||||
lineClamp={1}
|
||||
style={{
|
||||
textDecoration: isCompleted ? 'line-through' : 'none',
|
||||
color: getTextColor(),
|
||||
letterSpacing: '0.03em',
|
||||
transition: 'color 0.2s ease',
|
||||
}}
|
||||
>
|
||||
{event.title}
|
||||
</Text>
|
||||
{event.repeat_type !== 'none' && (
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexShrink: 0,
|
||||
color: REPEAT_ICON_COLOR,
|
||||
}}
|
||||
title={getRepeatTypeLabel(event.repeat_type)}
|
||||
>
|
||||
<IconRepeat size={10} />
|
||||
</Box>
|
||||
)}
|
||||
</Group>
|
||||
<Text
|
||||
size="xs"
|
||||
c={isCompleted ? '#bbb' : '#999'}
|
||||
lineClamp={1}
|
||||
style={{
|
||||
opacity: isCompleted ? 0.6 : 1,
|
||||
transition: 'opacity 0.2s ease',
|
||||
}}
|
||||
c={getTimeColor()}
|
||||
style={{ letterSpacing: '0.05em' }}
|
||||
>
|
||||
{event.content}
|
||||
{timeInfo.timeStr}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Group>
|
||||
{event.content && (
|
||||
<Text
|
||||
size="xs"
|
||||
c={isCompleted ? '#bbb' : '#999'}
|
||||
lineClamp={1}
|
||||
style={{
|
||||
opacity: isCompleted ? 0.6 : 1,
|
||||
transition: 'opacity 0.2s ease',
|
||||
}}
|
||||
>
|
||||
{event.content}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Group>
|
||||
|
||||
<Group gap={4}>
|
||||
{onPostpone && (
|
||||
<Group gap={4}>
|
||||
{onPostpone && (
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
color="orange"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onPostpone();
|
||||
}}
|
||||
style={{
|
||||
color: '#e67e22',
|
||||
opacity: isHovered ? 1 : 0,
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
title="顺延到今天"
|
||||
>
|
||||
<IconArrowForward size={12} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
color="orange"
|
||||
color="gray"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onPostpone();
|
||||
onClick();
|
||||
}}
|
||||
style={{
|
||||
color: '#e67e22',
|
||||
color: '#999',
|
||||
opacity: isHovered ? 1 : 0,
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
title="顺延到今天"
|
||||
title="编辑"
|
||||
>
|
||||
<IconArrowForward size={12} />
|
||||
<IconDots size={12} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClick();
|
||||
}}
|
||||
style={{
|
||||
color: '#999',
|
||||
opacity: isHovered ? 1 : 0,
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
title="编辑"
|
||||
>
|
||||
<IconDots size={12} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Group>
|
||||
</Group>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{/* 右键菜单 - 使用Portal渲染到body */}
|
||||
{isMenuOpen && cardRef.current && (
|
||||
<Portal>
|
||||
<Box
|
||||
data-context-menu
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: getMenuPosition().left,
|
||||
top: getMenuPosition().top,
|
||||
zIndex: 1000,
|
||||
background: '#fff',
|
||||
border: '1px solid rgba(0, 0, 0, 0.08)',
|
||||
borderRadius: 4,
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
|
||||
padding: '6px 8px',
|
||||
minWidth: 180,
|
||||
}}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* 调整时间 */}
|
||||
<Box style={{ marginBottom: 6 }}>
|
||||
<Text size="xs" c="#999" fw={400} style={{ marginBottom: 4, paddingLeft: 4, letterSpacing: '0.05em' }}>
|
||||
调整时间
|
||||
</Text>
|
||||
<Group gap={4}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 24,
|
||||
color: '#666',
|
||||
background: 'transparent',
|
||||
}}
|
||||
onClick={() => handleDateAdjust(0)}
|
||||
>
|
||||
<Text size="xs">今天</Text>
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 24,
|
||||
color: '#666',
|
||||
background: 'transparent',
|
||||
}}
|
||||
onClick={() => handleDateAdjust(1)}
|
||||
>
|
||||
<Text size="xs">明天</Text>
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 24,
|
||||
color: '#666',
|
||||
background: 'transparent',
|
||||
}}
|
||||
onClick={() => handleDateAdjust(7)}
|
||||
>
|
||||
<Text size="xs">一周后</Text>
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
<Divider my={4} style={{ borderColor: 'rgba(0, 0, 0, 0.06)' }} />
|
||||
|
||||
{/* 颜色 */}
|
||||
<Box style={{ marginBottom: 6 }}>
|
||||
<Text size="xs" c="#999" fw={400} style={{ marginBottom: 4, paddingLeft: 4, letterSpacing: '0.05em' }}>
|
||||
颜色
|
||||
</Text>
|
||||
<Group gap={4}>
|
||||
{(Object.keys(PRIORITY_COLORS) as PriorityType[]).map((priority) => (
|
||||
<ActionIcon
|
||||
key={priority}
|
||||
variant={event.priority === priority ? 'filled' : 'subtle'}
|
||||
size="xs"
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 24,
|
||||
background: event.priority === priority ? PRIORITY_COLORS[priority] : 'transparent',
|
||||
color: event.priority === priority ? '#fff' : '#666',
|
||||
border: `1px solid ${event.priority === priority ? 'transparent' : 'rgba(0, 0, 0, 0.1)'}`,
|
||||
}}
|
||||
onClick={() => handlePriorityChange(priority)}
|
||||
>
|
||||
<Text size="xs" fw={400}>
|
||||
{PRIORITY_NAMES[priority]}
|
||||
</Text>
|
||||
</ActionIcon>
|
||||
))}
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
<Divider my={4} style={{ borderColor: 'rgba(0, 0, 0, 0.06)' }} />
|
||||
|
||||
{/* 操作 */}
|
||||
<Group gap={4}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 24,
|
||||
color: '#666',
|
||||
background: 'transparent',
|
||||
}}
|
||||
onClick={() => {
|
||||
onClick();
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<Text size="xs" fw={400}>编辑</Text>
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 24,
|
||||
color: '#dc2626',
|
||||
background: 'transparent',
|
||||
}}
|
||||
onClick={() => {
|
||||
onDelete?.();
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<Text size="xs" fw={400}>删除</Text>
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Box>
|
||||
</Portal>
|
||||
)}
|
||||
</Paper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useMemo, useRef, useState, useEffect } from 'react';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Stack,
|
||||
Text,
|
||||
@ -18,7 +18,7 @@ import {
|
||||
} from '@tabler/icons-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ReminderCard } from './ReminderCard';
|
||||
import type { Event } from '../../types';
|
||||
import type { Event, PriorityType } from '../../types';
|
||||
|
||||
interface ReminderListProps {
|
||||
events: Event[];
|
||||
@ -27,6 +27,8 @@ interface ReminderListProps {
|
||||
onAddClick: () => void;
|
||||
onDelete?: (event: Event) => void;
|
||||
onPostpone?: (event: Event) => void;
|
||||
onDateChange?: (event: Event, date: string, repeatType: string, repeatInterval: number | null) => void;
|
||||
onPriorityChange?: (event: Event, priority: PriorityType) => void;
|
||||
}
|
||||
|
||||
export function ReminderList({
|
||||
@ -36,6 +38,8 @@ export function ReminderList({
|
||||
onAddClick,
|
||||
onDelete,
|
||||
onPostpone,
|
||||
onDateChange,
|
||||
onPriorityChange,
|
||||
}: ReminderListProps) {
|
||||
const navigate = useNavigate();
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
@ -299,6 +303,8 @@ export function ReminderList({
|
||||
onDelete={onDelete ? () => onDelete(event) : undefined}
|
||||
onPostpone={onPostpone ? () => onPostpone(event) : undefined}
|
||||
onMissedToggle={triggerArchiveShake}
|
||||
onDateChange={onDateChange ? (date, repeatType, repeatInterval) => onDateChange(event, date, repeatType, repeatInterval) : undefined}
|
||||
onPriorityChange={onPriorityChange ? (priority) => onPriorityChange(event, priority) : undefined}
|
||||
isMissed={true}
|
||||
/>
|
||||
))}
|
||||
@ -329,6 +335,8 @@ export function ReminderList({
|
||||
onDelete={onDelete ? () => onDelete(event) : undefined}
|
||||
onPostpone={onPostpone ? () => onPostpone(event) : undefined}
|
||||
onMissedToggle={triggerArchiveShake}
|
||||
onDateChange={onDateChange ? (date, repeatType, repeatInterval) => onDateChange(event, date, repeatType, repeatInterval) : undefined}
|
||||
onPriorityChange={onPriorityChange ? (priority) => onPriorityChange(event, priority) : undefined}
|
||||
isMissed={true}
|
||||
/>
|
||||
))}
|
||||
@ -376,6 +384,8 @@ export function ReminderList({
|
||||
onClick={() => onEventClick(event)}
|
||||
onToggle={() => onToggleComplete(event)}
|
||||
onDelete={onDelete ? () => onDelete(event) : undefined}
|
||||
onDateChange={onDateChange ? (date, repeatType, repeatInterval) => onDateChange(event, date, repeatType, repeatInterval) : undefined}
|
||||
onPriorityChange={onPriorityChange ? (priority) => onPriorityChange(event, priority) : undefined}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
@ -405,6 +415,8 @@ export function ReminderList({
|
||||
onClick={() => onEventClick(event)}
|
||||
onToggle={() => onToggleComplete(event)}
|
||||
onDelete={onDelete ? () => onDelete(event) : undefined}
|
||||
onDateChange={onDateChange ? (date, repeatType, repeatInterval) => onDateChange(event, date, repeatType, repeatInterval) : undefined}
|
||||
onPriorityChange={onPriorityChange ? (priority) => onPriorityChange(event, priority) : undefined}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
@ -434,6 +446,8 @@ export function ReminderList({
|
||||
onClick={() => onEventClick(event)}
|
||||
onToggle={() => onToggleComplete(event)}
|
||||
onDelete={onDelete ? () => onDelete(event) : undefined}
|
||||
onDateChange={onDateChange ? (date, repeatType, repeatInterval) => onDateChange(event, date, repeatType, repeatInterval) : undefined}
|
||||
onPriorityChange={onPriorityChange ? (priority) => onPriorityChange(event, priority) : undefined}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
Switch,
|
||||
Select,
|
||||
Stack,
|
||||
Box,
|
||||
} from '@mantine/core';
|
||||
import { DatePickerInput, TimeInput } from '@mantine/dates';
|
||||
import { IconLogout, IconSettings } from '@tabler/icons-react';
|
||||
@ -21,7 +22,7 @@ import { AnniversaryList } from '../components/anniversary/AnniversaryList';
|
||||
import { ReminderList } from '../components/reminder/ReminderList';
|
||||
import { NoteEditor } from '../components/note/NoteEditor';
|
||||
import { FloatingAIChat } from '../components/ai/FloatingAIChat';
|
||||
import type { Event, EventType, RepeatType } from '../types';
|
||||
import type { Event, EventType, RepeatType, PriorityType } from '../types';
|
||||
import { calculateNextReminderDate } from '../utils/repeatCalculator';
|
||||
|
||||
export function HomePage() {
|
||||
@ -48,6 +49,7 @@ export function HomePage() {
|
||||
const [formIsLunar, setFormIsLunar] = useState(false);
|
||||
const [formRepeatType, setFormRepeatType] = useState<RepeatType>('none');
|
||||
const [formIsHoliday, setFormIsHoliday] = useState(false);
|
||||
const [formPriority, setFormPriority] = useState<PriorityType>('none');
|
||||
|
||||
// Initialize auth and data on mount
|
||||
useEffect(() => {
|
||||
@ -70,6 +72,7 @@ export function HomePage() {
|
||||
setFormIsLunar(event.is_lunar);
|
||||
setFormRepeatType(event.repeat_type);
|
||||
setFormIsHoliday(event.is_holiday || false);
|
||||
setFormPriority(event.priority || 'none');
|
||||
// 提取时间(如果日期中包含时间信息)
|
||||
const eventDate = new Date(event.date);
|
||||
const hours = eventDate.getHours();
|
||||
@ -89,6 +92,7 @@ export function HomePage() {
|
||||
setFormIsLunar(false);
|
||||
setFormRepeatType('none');
|
||||
setFormIsHoliday(type === 'anniversary');
|
||||
setFormPriority('none');
|
||||
open();
|
||||
};
|
||||
|
||||
@ -123,6 +127,7 @@ export function HomePage() {
|
||||
// 计算下一次提醒日期
|
||||
next_reminder_date: calculateNextReminderDate(dateStr, formRepeatType, undefined),
|
||||
is_holiday: formIsHoliday ?? false,
|
||||
priority: formPriority,
|
||||
};
|
||||
|
||||
// 只有当 content 有值时才包含
|
||||
@ -180,14 +185,80 @@ export function HomePage() {
|
||||
// 构建新的本地时间字符串
|
||||
const newDateStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}T${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00`;
|
||||
|
||||
const result = await updateEventById(event.id, {
|
||||
// 构建更新数据
|
||||
const updateData: Record<string, any> = {
|
||||
date: newDateStr,
|
||||
});
|
||||
priority: event.priority || 'none',
|
||||
};
|
||||
|
||||
// 如果是重复提醒,同步更新 next_reminder_date
|
||||
if (event.repeat_type && event.repeat_type !== 'none') {
|
||||
const nextReminderDate = calculateNextReminderDate(
|
||||
newDateStr,
|
||||
event.repeat_type as RepeatType,
|
||||
event.repeat_interval || undefined
|
||||
);
|
||||
updateData.next_reminder_date = nextReminderDate;
|
||||
}
|
||||
|
||||
const result = await updateEventById(event.id, updateData);
|
||||
if (result.error) {
|
||||
console.error('顺延失败:', result.error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDateChange = async (
|
||||
event: Event,
|
||||
date: string,
|
||||
repeatType: string,
|
||||
repeatInterval: number | null
|
||||
) => {
|
||||
if (event.type !== 'reminder') return;
|
||||
// 保留原事件的时间信息
|
||||
const originalDate = new Date(event.date);
|
||||
const hours = originalDate.getHours();
|
||||
const minutes = originalDate.getMinutes();
|
||||
|
||||
// 构建新的本地时间字符串
|
||||
const newDateStr = `${date}T${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00`;
|
||||
|
||||
// 构建更新数据
|
||||
const updateData: Record<string, any> = {
|
||||
date: newDateStr,
|
||||
priority: event.priority || 'none',
|
||||
};
|
||||
|
||||
// 如果是重复提醒,同步更新 next_reminder_date
|
||||
if (repeatType && repeatType !== 'none') {
|
||||
const nextReminderDate = calculateNextReminderDate(
|
||||
newDateStr,
|
||||
repeatType as RepeatType,
|
||||
repeatInterval || undefined
|
||||
);
|
||||
updateData.next_reminder_date = nextReminderDate;
|
||||
}
|
||||
|
||||
// 合并更新
|
||||
const result = await updateEventById(event.id, updateData);
|
||||
if (result.error) {
|
||||
console.error('日期调整失败:', result.error);
|
||||
}
|
||||
fetchEvents(); // 刷新列表以更新UI
|
||||
};
|
||||
|
||||
const handlePriorityChange = async (event: Event, priority: PriorityType) => {
|
||||
if (event.type !== 'reminder') return;
|
||||
// 合并更新 priority 和 date,确保 updated_at 被更新
|
||||
const result = await updateEventById(event.id, {
|
||||
priority,
|
||||
date: event.date,
|
||||
});
|
||||
if (result.error) {
|
||||
console.error('优先级设置失败:', result.error);
|
||||
}
|
||||
fetchEvents(); // 刷新列表以更新UI
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setFormType('anniversary');
|
||||
setFormTitle('');
|
||||
@ -197,6 +268,7 @@ export function HomePage() {
|
||||
setFormIsLunar(false);
|
||||
setFormRepeatType('none');
|
||||
setFormIsHoliday(false);
|
||||
setFormPriority('none');
|
||||
setSelectedEvent(null);
|
||||
setIsEdit(false);
|
||||
};
|
||||
@ -286,6 +358,8 @@ export function HomePage() {
|
||||
onAddClick={() => handleAddClick('reminder')}
|
||||
onDelete={handleDelete}
|
||||
onPostpone={handlePostpone}
|
||||
onDateChange={handleDateChange}
|
||||
onPriorityChange={handlePriorityChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -460,6 +534,38 @@ export function HomePage() {
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Priority (only for reminders) */}
|
||||
{formType === 'reminder' && (
|
||||
<Box>
|
||||
<Text size="xs" c="#666" style={{ letterSpacing: '0.05em', marginBottom: 8 }}>
|
||||
优先级
|
||||
</Text>
|
||||
<Group gap={8}>
|
||||
{([
|
||||
{ value: 'none' as const, color: 'rgba(0, 0, 0, 0.15)' },
|
||||
{ value: 'red' as const, color: '#dc2626' },
|
||||
{ value: 'green' as const, color: '#16a34a' },
|
||||
{ value: 'yellow' as const, color: '#ca8a04' },
|
||||
].map((item) => (
|
||||
<Box
|
||||
key={item.value}
|
||||
onClick={() => setFormPriority(item.value)}
|
||||
style={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: '50%',
|
||||
background: item.color,
|
||||
border: formPriority === item.value ? '2px solid #1a1a1a' : '1px solid rgba(0, 0, 0, 0.1)',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
transform: formPriority === item.value ? 'scale(1.1)' : 'scale(1)',
|
||||
}}
|
||||
/>
|
||||
)))}
|
||||
</Group>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Holiday switch (only for anniversaries) */}
|
||||
{formType === 'anniversary' && (
|
||||
<Switch
|
||||
|
||||
40
src/stores/contextMenu.ts
Normal file
40
src/stores/contextMenu.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
// 右键菜单状态管理
|
||||
interface ContextMenuState {
|
||||
// 当前打开菜单的事件ID,null表示没有菜单打开
|
||||
openEventId: string | null;
|
||||
// 打开菜单
|
||||
openMenu: (eventId: string) => void;
|
||||
// 关闭菜单
|
||||
closeMenu: () => void;
|
||||
// 切换菜单
|
||||
toggleMenu: (eventId: string) => void;
|
||||
// 点击其他位置关闭菜单
|
||||
closeOnClickOutside: (eventId: string) => void;
|
||||
}
|
||||
|
||||
export const useContextMenuStore = create<ContextMenuState>((set) => ({
|
||||
openEventId: null,
|
||||
|
||||
openMenu: (eventId: string) => {
|
||||
set({ openEventId: eventId });
|
||||
},
|
||||
|
||||
closeMenu: () => {
|
||||
set({ openEventId: null });
|
||||
},
|
||||
|
||||
toggleMenu: (eventId: string) => {
|
||||
set((state) => ({
|
||||
openEventId: state.openEventId === eventId ? null : eventId,
|
||||
}));
|
||||
},
|
||||
|
||||
// 点击其他位置时,如果点击的是不同的事件ID则关闭菜单
|
||||
closeOnClickOutside: (clickedEventId: string) => {
|
||||
set((state) => ({
|
||||
openEventId: state.openEventId === clickedEventId ? state.openEventId : null,
|
||||
}));
|
||||
},
|
||||
}));
|
||||
@ -13,6 +13,9 @@ export type EventType = 'anniversary' | 'reminder';
|
||||
// Repeat types: 'daily'每天, 'weekly'每周, 'monthly'每月, 'yearly'每年, 'none'不重复
|
||||
export type RepeatType = 'daily' | 'weekly' | 'monthly' | 'yearly' | 'none';
|
||||
|
||||
// Priority types: 'none'无色, 'red'红色, 'green'绿色, 'yellow'黄色
|
||||
export type PriorityType = 'none' | 'red' | 'green' | 'yellow';
|
||||
|
||||
// Unified event type (matches backend API)
|
||||
export interface Event {
|
||||
id: string;
|
||||
@ -27,6 +30,7 @@ export interface Event {
|
||||
next_reminder_date?: string | null; // 下一次提醒日期(计算用)
|
||||
is_holiday?: boolean; // Only for anniversaries
|
||||
is_completed?: boolean; // Only for reminders
|
||||
priority?: PriorityType; // Priority level for reminders
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user