9b044449a0
- profiles get an optional birthday (MM-DD); photos of that person taken on the date are also copied into Birthdays/<person>/<year>/ - app-wide anniversaries (label + MM-DD); any photo taken on the date is copied into Anniversaries/<label>/<year>/ (including faceless photos and videos) - copy (not move) so normal person/date sorting is preserved - CaptureDate gains day; new collection path builder; scanner skips the new folders - UI: birthday input in profile create/edit + new Anniversaries manager Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
82 lines
3.1 KiB
TypeScript
82 lines
3.1 KiB
TypeScript
import { useState } from 'react'
|
||
import { useStore } from '../store'
|
||
import { useT } from '../i18n'
|
||
import type { Anniversary } from '@shared/types'
|
||
|
||
/** 기념일(앱 전체 공통) 등록/삭제. 해당 날짜 사진을 Anniversaries/이름/연도 폴더에 모음 */
|
||
export function AnniversaryManager(): JSX.Element {
|
||
const t = useT()
|
||
const anniversaries = useStore((s) => s.anniversaries)
|
||
const updateSettings = useStore((s) => s.updateSettings)
|
||
const [label, setLabel] = useState('')
|
||
const [date, setDate] = useState('') // YYYY-MM-DD
|
||
|
||
const add = async () => {
|
||
const l = label.trim()
|
||
if (!l || !date) return
|
||
const item: Anniversary = { id: crypto.randomUUID(), label: l, date: date.slice(5) } // MM-DD
|
||
await updateSettings({ anniversaries: [...anniversaries, item] })
|
||
setLabel('')
|
||
setDate('')
|
||
}
|
||
const remove = async (id: string) => {
|
||
await updateSettings({ anniversaries: anniversaries.filter((a) => a.id !== id) })
|
||
}
|
||
|
||
// MM-DD → 보기용 "MM.DD"
|
||
const fmt = (mmdd: string): string => mmdd.replace('-', '.')
|
||
|
||
return (
|
||
<div className="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4">
|
||
<h2 className="font-semibold dark:text-slate-100 mb-1">🎉 {t('anniv.section')}</h2>
|
||
<p className="text-[11px] text-slate-400 mb-3">{t('anniv.hint')}</p>
|
||
|
||
<div className="flex gap-2 mb-3">
|
||
<input
|
||
className="flex-1 border border-slate-300 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-100 rounded-lg px-3 py-2 text-sm"
|
||
placeholder={t('anniv.labelPlaceholder')}
|
||
value={label}
|
||
onChange={(e) => setLabel(e.target.value)}
|
||
onKeyDown={(e) => e.key === 'Enter' && add()}
|
||
/>
|
||
<input
|
||
type="date"
|
||
className="border border-slate-300 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-100 rounded-lg px-2 py-2 text-sm"
|
||
value={date}
|
||
onChange={(e) => setDate(e.target.value)}
|
||
/>
|
||
<button
|
||
className="bg-brand text-white rounded-lg px-4 text-sm font-medium disabled:opacity-40"
|
||
onClick={add}
|
||
disabled={!label.trim() || !date}
|
||
>
|
||
{t('common.add')}
|
||
</button>
|
||
</div>
|
||
|
||
{anniversaries.length === 0 ? (
|
||
<p className="text-xs text-slate-400">{t('anniv.empty')}</p>
|
||
) : (
|
||
<div className="flex flex-wrap gap-2">
|
||
{anniversaries.map((a) => (
|
||
<div
|
||
key={a.id}
|
||
className="flex items-center gap-2 pl-3 pr-2 py-1 rounded-full border border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-700/50"
|
||
>
|
||
<span className="text-xs font-medium dark:text-slate-200">{a.label}</span>
|
||
<span className="text-[11px] text-slate-400 tabular-nums">{fmt(a.date)}</span>
|
||
<button
|
||
className="text-slate-400 hover:text-red-500 text-xs leading-none"
|
||
onClick={() => remove(a.id)}
|
||
title="삭제"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|