feat: add birthday calendar feature with settings and localization
This commit is contained in:
@@ -41,9 +41,11 @@ import { ResizeHandle } from "@/components/layout/resize-handle";
|
||||
import { sanitizeOutgoingCalendarEventData } from "@/lib/calendar-event-normalization";
|
||||
import { getEventStartDate } from "@/lib/calendar-utils";
|
||||
import { useTaskStore } from "@/stores/task-store";
|
||||
import { useContactStore } from "@/stores/contact-store";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { CalendarEvent, CalendarParticipant } from "@/lib/jmap/types";
|
||||
import { getUserParticipantId } from "@/lib/calendar-participants";
|
||||
import { generateBirthdayEvents, createBirthdayCalendar, BIRTHDAY_CALENDAR_ID } from "@/lib/birthday-calendar";
|
||||
import { debug } from "@/lib/debug";
|
||||
|
||||
type PendingScopeAction =
|
||||
@@ -69,10 +71,11 @@ export default function CalendarPage() {
|
||||
setSelectedDate, setViewMode, toggleCalendarVisibility, updateCalendar,
|
||||
refreshAllSubscriptions, icalSubscriptions,
|
||||
} = useCalendarStore();
|
||||
const { firstDayOfWeek, timeFormat, showWeekNumbers, enableCalendarTasks, showTasksOnCalendar, calendarHoverPreview } = useSettingsStore();
|
||||
const { firstDayOfWeek, timeFormat, showWeekNumbers, enableCalendarTasks, showTasksOnCalendar, calendarHoverPreview, showBirthdayCalendar, birthdayCalendarColor, updateSetting } = useSettingsStore();
|
||||
const taskStore = useTaskStore();
|
||||
const fetchTasksFn = useTaskStore(state => state.fetchTasks);
|
||||
const { identities } = useIdentityStore();
|
||||
const contacts = useContactStore((s) => s.contacts);
|
||||
const normalizedViewMode = isCalendarViewMode(viewMode) ? viewMode : "month";
|
||||
|
||||
const currentUserEmails = useMemo(() =>
|
||||
@@ -148,6 +151,13 @@ export default function CalendarPage() {
|
||||
return () => clearInterval(interval);
|
||||
}, [client, refreshAllSubscriptions]);
|
||||
|
||||
// Auto-add birthday calendar to selected IDs when enabled
|
||||
useEffect(() => {
|
||||
if (showBirthdayCalendar && !selectedCalendarIds.includes(BIRTHDAY_CALENDAR_ID)) {
|
||||
toggleCalendarVisibility(BIRTHDAY_CALENDAR_ID);
|
||||
}
|
||||
}, [showBirthdayCalendar]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const dateRange = useMemo(() => {
|
||||
const d = selectedDate;
|
||||
switch (normalizedViewMode) {
|
||||
@@ -743,14 +753,31 @@ export default function CalendarPage() {
|
||||
return () => window.removeEventListener("keydown", handleKey);
|
||||
}, [navigatePrev, navigateNext, goToToday, setViewMode, openCreateModal, showEventModal, detailEvent, enableCalendarTasks]);
|
||||
|
||||
const visibleEvents = useMemo(() =>
|
||||
events.filter((e) => {
|
||||
const birthdayEvents = useMemo(() => {
|
||||
if (!showBirthdayCalendar || !dateRange) return [];
|
||||
return generateBirthdayEvents(contacts, dateRange.start, dateRange.end);
|
||||
}, [showBirthdayCalendar, contacts, dateRange]);
|
||||
|
||||
const birthdayCalendarName = (() => {
|
||||
try { return t('birthday_calendar'); } catch { return 'Birthdays'; }
|
||||
})();
|
||||
|
||||
const allCalendars = useMemo(() => {
|
||||
if (!showBirthdayCalendar) return calendars;
|
||||
return [...calendars, createBirthdayCalendar(birthdayCalendarName, birthdayCalendarColor)];
|
||||
}, [calendars, showBirthdayCalendar, birthdayCalendarName, birthdayCalendarColor]);
|
||||
|
||||
const visibleEvents = useMemo(() => {
|
||||
const filtered = events.filter((e) => {
|
||||
if (!e.start || !e.calendarIds) return false;
|
||||
const calIds = Object.keys(e.calendarIds);
|
||||
return calIds.some((id) => selectedCalendarIds.includes(id));
|
||||
}),
|
||||
[events, selectedCalendarIds]
|
||||
);
|
||||
});
|
||||
if (showBirthdayCalendar && selectedCalendarIds.includes(BIRTHDAY_CALENDAR_ID)) {
|
||||
return [...filtered, ...birthdayEvents];
|
||||
}
|
||||
return filtered;
|
||||
}, [events, selectedCalendarIds, showBirthdayCalendar, birthdayEvents]);
|
||||
|
||||
useEffect(() => {
|
||||
const hiddenEvents = events.filter((event) => {
|
||||
@@ -799,7 +826,7 @@ export default function CalendarPage() {
|
||||
<CalendarMonthView
|
||||
selectedDate={selectedDate}
|
||||
events={visibleEvents}
|
||||
calendars={calendars}
|
||||
calendars={allCalendars}
|
||||
onSelectDate={handleSelectDate}
|
||||
onSelectEvent={handleSelectEvent}
|
||||
onHoverEvent={handleHoverEvent}
|
||||
@@ -815,7 +842,7 @@ export default function CalendarPage() {
|
||||
<CalendarWeekView
|
||||
selectedDate={selectedDate}
|
||||
events={visibleEvents}
|
||||
calendars={calendars}
|
||||
calendars={allCalendars}
|
||||
onSelectDate={handleSelectDate}
|
||||
onSelectEvent={handleSelectEvent}
|
||||
onHoverEvent={handleHoverEvent}
|
||||
@@ -834,7 +861,7 @@ export default function CalendarPage() {
|
||||
<CalendarDayView
|
||||
selectedDate={selectedDate}
|
||||
events={visibleEvents}
|
||||
calendars={calendars}
|
||||
calendars={allCalendars}
|
||||
onSelectEvent={handleSelectEvent}
|
||||
onHoverEvent={handleHoverEvent}
|
||||
onHoverLeave={handleHoverLeave}
|
||||
@@ -851,7 +878,7 @@ export default function CalendarPage() {
|
||||
<CalendarAgendaView
|
||||
selectedDate={selectedDate}
|
||||
events={visibleEvents}
|
||||
calendars={calendars}
|
||||
calendars={allCalendars}
|
||||
onSelectEvent={handleSelectEvent}
|
||||
onHoverEvent={handleHoverEvent}
|
||||
onHoverLeave={handleHoverLeave}
|
||||
@@ -942,10 +969,14 @@ export default function CalendarPage() {
|
||||
showWeekNumbers={showWeekNumbers}
|
||||
/>
|
||||
<CalendarSidebarPanel
|
||||
calendars={calendars}
|
||||
calendars={allCalendars}
|
||||
selectedCalendarIds={selectedCalendarIds}
|
||||
onToggleVisibility={toggleCalendarVisibility}
|
||||
onColorChange={client ? (calendarId, color) => {
|
||||
if (calendarId === BIRTHDAY_CALENDAR_ID) {
|
||||
updateSetting('birthdayCalendarColor', color);
|
||||
return;
|
||||
}
|
||||
updateCalendar(client, calendarId, { color });
|
||||
} : undefined}
|
||||
onSubscribe={() => setShowSubscriptionModal(true)}
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
import { useState, useRef, useEffect, useMemo } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Globe, ListTodo, Pencil, RefreshCw, Share2, Trash2 } from "lucide-react";
|
||||
import { Globe, ListTodo, Pencil, RefreshCw, Share2, Trash2, Cake } from "lucide-react";
|
||||
import { cn, formatDateTime } from "@/lib/utils";
|
||||
import type { Calendar } from "@/lib/jmap/types";
|
||||
import { CalendarColorPicker } from "@/components/settings/calendar-management-settings";
|
||||
import { useCalendarStore } from "@/stores/calendar-store";
|
||||
import { useSettingsStore } from "@/stores/settings-store";
|
||||
import { useTaskStore } from "@/stores/task-store";
|
||||
import { BIRTHDAY_CALENDAR_ID } from "@/lib/birthday-calendar";
|
||||
import { toast } from "@/stores/toast-store";
|
||||
import type { IJMAPClient } from '@/lib/jmap/client-interface';
|
||||
|
||||
@@ -164,6 +165,9 @@ export function CalendarSidebarPanel({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{cal.id === BIRTHDAY_CALENDAR_ID && (
|
||||
<Cake className="w-3 h-3 text-muted-foreground flex-shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Subscription context menu on right-click */}
|
||||
|
||||
@@ -19,6 +19,7 @@ export function CalendarSettings() {
|
||||
showWeekNumbers,
|
||||
enableCalendarTasks,
|
||||
showTasksOnCalendar,
|
||||
showBirthdayCalendar,
|
||||
calendarHoverPreview,
|
||||
updateSetting,
|
||||
} = useSettingsStore();
|
||||
@@ -98,6 +99,16 @@ export function CalendarSettings() {
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
label={t('show_birthday_calendar')}
|
||||
description={t('show_birthday_calendar_desc')}
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={showBirthdayCalendar}
|
||||
onChange={(checked) => updateSetting('showBirthdayCalendar', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
{isFeatureEnabled('calendarTasksEnabled') && (
|
||||
<>
|
||||
<SettingItem
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
import type { ContactCard, CalendarEvent, Calendar, PartialDate, Timestamp } from '@/lib/jmap/types';
|
||||
import { getContactDisplayName } from '@/stores/contact-store';
|
||||
import { format, eachYearOfInterval, parseISO } from 'date-fns';
|
||||
|
||||
export const BIRTHDAY_CALENDAR_ID = '__birthday-calendar__';
|
||||
export const BIRTHDAY_CALENDAR_COLOR = '#eab308'; // Yellow
|
||||
|
||||
/**
|
||||
* Virtual calendar object for the contact birthday calendar.
|
||||
*/
|
||||
export function createBirthdayCalendar(name?: string, color?: string): Calendar {
|
||||
return {
|
||||
id: BIRTHDAY_CALENDAR_ID,
|
||||
name: name || 'Birthdays',
|
||||
description: null,
|
||||
color: color || BIRTHDAY_CALENDAR_COLOR,
|
||||
sortOrder: 999,
|
||||
isSubscribed: true,
|
||||
isVisible: true,
|
||||
isDefault: false,
|
||||
includeInAvailability: 'none',
|
||||
defaultAlertsWithTime: null,
|
||||
defaultAlertsWithoutTime: null,
|
||||
timeZone: null,
|
||||
shareWith: null,
|
||||
myRights: {
|
||||
mayReadFreeBusy: true,
|
||||
mayReadItems: true,
|
||||
mayWriteAll: false,
|
||||
mayWriteOwn: false,
|
||||
mayUpdatePrivate: false,
|
||||
mayRSVP: false,
|
||||
mayAdmin: false,
|
||||
mayDelete: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract month and day from an AnniversaryDate.
|
||||
* Returns null if the date cannot be parsed into month/day.
|
||||
*/
|
||||
function parseBirthdayDate(date: string | PartialDate | Timestamp): { month: number; day: number; year?: number } | null {
|
||||
if (typeof date === 'string') {
|
||||
// Could be ISO date string like "1990-05-15" or partial "--05-15"
|
||||
if (date.startsWith('--')) {
|
||||
// Partial date: --MM-DD
|
||||
const match = date.match(/^--(\d{2})-(\d{2})$/);
|
||||
if (match) {
|
||||
return { month: parseInt(match[1], 10), day: parseInt(match[2], 10) };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const parsed = parseISO(date);
|
||||
if (!isNaN(parsed.getTime())) {
|
||||
return {
|
||||
month: parsed.getMonth() + 1,
|
||||
day: parsed.getDate(),
|
||||
year: parsed.getFullYear(),
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if ('utc' in date && date['@type'] === 'Timestamp') {
|
||||
// Timestamp type
|
||||
try {
|
||||
const parsed = parseISO(date.utc);
|
||||
if (!isNaN(parsed.getTime())) {
|
||||
return {
|
||||
month: parsed.getMonth() + 1,
|
||||
day: parsed.getDate(),
|
||||
year: parsed.getFullYear(),
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// PartialDate type
|
||||
const partial = date as PartialDate;
|
||||
if (partial.month && partial.day) {
|
||||
return {
|
||||
month: partial.month,
|
||||
day: partial.day,
|
||||
year: partial.year || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate virtual CalendarEvent objects from contacts that have birthday anniversaries.
|
||||
* Events are generated for each year in the given date range.
|
||||
*/
|
||||
export function generateBirthdayEvents(
|
||||
contacts: ContactCard[],
|
||||
rangeStart: string,
|
||||
rangeEnd: string,
|
||||
): CalendarEvent[] {
|
||||
const events: CalendarEvent[] = [];
|
||||
const start = parseISO(rangeStart);
|
||||
const end = parseISO(rangeEnd);
|
||||
|
||||
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
|
||||
return events;
|
||||
}
|
||||
|
||||
const years = eachYearOfInterval({ start, end });
|
||||
// Also include the end date's year if not already covered
|
||||
const endYear = end.getFullYear();
|
||||
if (!years.some(y => y.getFullYear() === endYear)) {
|
||||
years.push(new Date(endYear, 0, 1));
|
||||
}
|
||||
|
||||
for (const contact of contacts) {
|
||||
if (!contact.anniversaries) continue;
|
||||
|
||||
for (const [key, anniversary] of Object.entries(contact.anniversaries)) {
|
||||
if (anniversary.kind !== 'birth') continue;
|
||||
|
||||
const parsed = parseBirthdayDate(anniversary.date);
|
||||
if (!parsed) continue;
|
||||
|
||||
const displayName = getContactDisplayName(contact);
|
||||
if (!displayName) continue;
|
||||
|
||||
for (const yearDate of years) {
|
||||
const year = yearDate.getFullYear();
|
||||
const monthStr = String(parsed.month).padStart(2, '0');
|
||||
const dayStr = String(parsed.day).padStart(2, '0');
|
||||
const eventStart = `${year}-${monthStr}-${dayStr}T00:00:00`;
|
||||
|
||||
// Check if this birthday occurrence falls within the date range
|
||||
const occurrenceDate = new Date(year, parsed.month - 1, parsed.day);
|
||||
if (occurrenceDate < start || occurrenceDate > end) continue;
|
||||
|
||||
const age = parsed.year ? year - parsed.year : undefined;
|
||||
const ageText = age && age > 0 ? ` (${age})` : '';
|
||||
|
||||
const event: CalendarEvent = {
|
||||
id: `birthday-${contact.id}-${key}-${year}`,
|
||||
calendarIds: { [BIRTHDAY_CALENDAR_ID]: true },
|
||||
isDraft: false,
|
||||
isOrigin: false,
|
||||
utcStart: null,
|
||||
utcEnd: null,
|
||||
'@type': 'Event',
|
||||
uid: `birthday-${contact.id}-${key}`,
|
||||
title: `🎂 ${displayName}${ageText}`,
|
||||
description: '',
|
||||
descriptionContentType: 'text/plain',
|
||||
created: null,
|
||||
updated: '',
|
||||
sequence: 0,
|
||||
start: eventStart,
|
||||
duration: 'P1D',
|
||||
timeZone: null,
|
||||
showWithoutTime: true,
|
||||
status: 'confirmed',
|
||||
freeBusyStatus: 'free',
|
||||
privacy: 'public',
|
||||
color: null,
|
||||
keywords: null,
|
||||
categories: null,
|
||||
locale: null,
|
||||
replyTo: null,
|
||||
organizerCalendarAddress: null,
|
||||
participants: null,
|
||||
mayInviteSelf: false,
|
||||
mayInviteOthers: false,
|
||||
hideAttendees: false,
|
||||
recurrenceId: null,
|
||||
recurrenceIdTimeZone: null,
|
||||
recurrenceRules: null,
|
||||
recurrenceOverrides: null,
|
||||
excludedRecurrenceRules: null,
|
||||
useDefaultAlerts: false,
|
||||
alerts: null,
|
||||
locations: null,
|
||||
virtualLocations: null,
|
||||
links: null,
|
||||
relatedTo: null,
|
||||
};
|
||||
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
@@ -1873,6 +1873,7 @@
|
||||
"back_to_email": "Zurück zu E-Mails",
|
||||
"back_to_month": "Zurück zur Monatsansicht",
|
||||
"my_calendars": "Meine Kalender",
|
||||
"birthday_calendar": "Geburtstage",
|
||||
"mini_calendar_change": "Klicken, um den Monat zu wechseln",
|
||||
"views": {
|
||||
"month": "Monat",
|
||||
@@ -2010,7 +2011,9 @@
|
||||
"hover_preview_delay_500ms": "0,5 Sekunden Verzögerung",
|
||||
"hover_preview_delay_1s": "1 Sekunde Verzögerung",
|
||||
"hover_preview_delay_2s": "2 Sekunden Verzögerung",
|
||||
"hover_preview_off": "Deaktiviert"
|
||||
"hover_preview_off": "Deaktiviert",
|
||||
"show_birthday_calendar": "Geburtstags-Kalender",
|
||||
"show_birthday_calendar_desc": "Zeigt einen virtuellen Kalender mit Geburtstagen aus deinen Kontakten"
|
||||
},
|
||||
"days": {
|
||||
"monday": "Montag",
|
||||
|
||||
@@ -1876,6 +1876,7 @@
|
||||
"back_to_email": "Back to email",
|
||||
"back_to_month": "Back to month",
|
||||
"my_calendars": "Calendars",
|
||||
"birthday_calendar": "Birthdays",
|
||||
"mini_calendar_change": "Click to change month",
|
||||
"views": {
|
||||
"month": "Month",
|
||||
@@ -2013,7 +2014,9 @@
|
||||
"hover_preview_delay_500ms": "0.5-second delay",
|
||||
"hover_preview_delay_1s": "1-second delay",
|
||||
"hover_preview_delay_2s": "2-second delay",
|
||||
"hover_preview_off": "Disabled"
|
||||
"hover_preview_off": "Disabled",
|
||||
"show_birthday_calendar": "Contact birthday calendar",
|
||||
"show_birthday_calendar_desc": "Show a virtual calendar with birthdays from your contacts"
|
||||
},
|
||||
"days": {
|
||||
"monday": "Monday",
|
||||
|
||||
@@ -1873,6 +1873,7 @@
|
||||
"back_to_email": "Volver al correo",
|
||||
"back_to_month": "Volver al mes",
|
||||
"my_calendars": "Mis calendarios",
|
||||
"birthday_calendar": "Cumpleaños",
|
||||
"mini_calendar_change": "Clic para cambiar de mes",
|
||||
"views": {
|
||||
"month": "Mes",
|
||||
@@ -2010,7 +2011,9 @@
|
||||
"hover_preview_delay_500ms": "Retraso de 0,5 segundos",
|
||||
"hover_preview_delay_1s": "Retraso de 1 segundo",
|
||||
"hover_preview_delay_2s": "Retraso de 2 segundos",
|
||||
"hover_preview_off": "Desactivado"
|
||||
"hover_preview_off": "Desactivado",
|
||||
"show_birthday_calendar": "Calendario de cumpleaños",
|
||||
"show_birthday_calendar_desc": "Mostrar un calendario virtual con los cumpleaños de tus contactos"
|
||||
},
|
||||
"days": {
|
||||
"monday": "Lunes",
|
||||
|
||||
@@ -1873,6 +1873,7 @@
|
||||
"back_to_email": "Retour aux e-mails",
|
||||
"back_to_month": "Retour au mois",
|
||||
"my_calendars": "Mes calendriers",
|
||||
"birthday_calendar": "Anniversaires",
|
||||
"mini_calendar_change": "Cliquer pour changer de mois",
|
||||
"views": {
|
||||
"month": "Mois",
|
||||
@@ -2010,7 +2011,9 @@
|
||||
"hover_preview_delay_500ms": "Délai de 0,5 seconde",
|
||||
"hover_preview_delay_1s": "Délai de 1 seconde",
|
||||
"hover_preview_delay_2s": "Délai de 2 secondes",
|
||||
"hover_preview_off": "Désactivé"
|
||||
"hover_preview_off": "Désactivé",
|
||||
"show_birthday_calendar": "Calendrier des anniversaires",
|
||||
"show_birthday_calendar_desc": "Afficher un calendrier virtuel avec les anniversaires de vos contacts"
|
||||
},
|
||||
"days": {
|
||||
"monday": "Lundi",
|
||||
|
||||
@@ -1873,6 +1873,7 @@
|
||||
"back_to_email": "Torna alla posta",
|
||||
"back_to_month": "Torna al mese",
|
||||
"my_calendars": "I miei calendari",
|
||||
"birthday_calendar": "Compleanni",
|
||||
"mini_calendar_change": "Clicca per cambiare mese",
|
||||
"views": {
|
||||
"month": "Mese",
|
||||
@@ -2010,7 +2011,9 @@
|
||||
"hover_preview_delay_500ms": "Ritardo di 0,5 secondi",
|
||||
"hover_preview_delay_1s": "Ritardo di 1 secondo",
|
||||
"hover_preview_delay_2s": "Ritardo di 2 secondi",
|
||||
"hover_preview_off": "Disabilitato"
|
||||
"hover_preview_off": "Disabilitato",
|
||||
"show_birthday_calendar": "Calendario compleanni",
|
||||
"show_birthday_calendar_desc": "Mostra un calendario virtuale con i compleanni dei tuoi contatti"
|
||||
},
|
||||
"days": {
|
||||
"monday": "Lunedì",
|
||||
|
||||
@@ -1873,6 +1873,7 @@
|
||||
"back_to_email": "メールに戻る",
|
||||
"back_to_month": "月表示に戻る",
|
||||
"my_calendars": "マイカレンダー",
|
||||
"birthday_calendar": "誕生日",
|
||||
"mini_calendar_change": "クリックで月を変更",
|
||||
"views": {
|
||||
"month": "月",
|
||||
@@ -2010,7 +2011,9 @@
|
||||
"hover_preview_delay_500ms": "0.5秒遅延",
|
||||
"hover_preview_delay_1s": "1秒遅延",
|
||||
"hover_preview_delay_2s": "2秒遅延",
|
||||
"hover_preview_off": "無効"
|
||||
"hover_preview_off": "無効",
|
||||
"show_birthday_calendar": "誕生日カレンダー",
|
||||
"show_birthday_calendar_desc": "連絡先の誕生日を表示する仮想カレンダーを表示します"
|
||||
},
|
||||
"days": {
|
||||
"monday": "月曜日",
|
||||
|
||||
@@ -1873,6 +1873,7 @@
|
||||
"back_to_email": "Terug naar e-mail",
|
||||
"back_to_month": "Terug naar maand",
|
||||
"my_calendars": "Mijn agenda's",
|
||||
"birthday_calendar": "Verjaardagen",
|
||||
"mini_calendar_change": "Klik om van maand te wisselen",
|
||||
"views": {
|
||||
"month": "Maand",
|
||||
@@ -2010,7 +2011,9 @@
|
||||
"hover_preview_delay_500ms": "0,5 seconde vertraging",
|
||||
"hover_preview_delay_1s": "1 seconde vertraging",
|
||||
"hover_preview_delay_2s": "2 seconden vertraging",
|
||||
"hover_preview_off": "Uitgeschakeld"
|
||||
"hover_preview_off": "Uitgeschakeld",
|
||||
"show_birthday_calendar": "Verjaardagskalender",
|
||||
"show_birthday_calendar_desc": "Toon een virtuele kalender met verjaardagen van je contacten"
|
||||
},
|
||||
"days": {
|
||||
"monday": "Maandag",
|
||||
|
||||
@@ -1873,6 +1873,7 @@
|
||||
"back_to_email": "Voltar ao e-mail",
|
||||
"back_to_month": "Voltar ao mês",
|
||||
"my_calendars": "Meus calendários",
|
||||
"birthday_calendar": "Aniversários",
|
||||
"mini_calendar_change": "Clique para mudar o mês",
|
||||
"views": {
|
||||
"month": "Mês",
|
||||
@@ -2010,7 +2011,9 @@
|
||||
"hover_preview_delay_500ms": "Atraso de 0,5 segundos",
|
||||
"hover_preview_delay_1s": "Atraso de 1 segundo",
|
||||
"hover_preview_delay_2s": "Atraso de 2 segundos",
|
||||
"hover_preview_off": "Desativado"
|
||||
"hover_preview_off": "Desativado",
|
||||
"show_birthday_calendar": "Calendário de aniversários",
|
||||
"show_birthday_calendar_desc": "Mostrar um calendário virtual com os aniversários dos seus contactos"
|
||||
},
|
||||
"days": {
|
||||
"monday": "Segunda-feira",
|
||||
|
||||
@@ -1873,6 +1873,7 @@
|
||||
"back_to_email": "Вернуться к почте",
|
||||
"back_to_month": "Вернуться к месяцу",
|
||||
"my_calendars": "Календари",
|
||||
"birthday_calendar": "Дни рождения",
|
||||
"mini_calendar_change": "Нажмите для смены месяца",
|
||||
"views": {
|
||||
"month": "Месяц",
|
||||
@@ -2010,7 +2011,9 @@
|
||||
"hover_preview_delay_500ms": "Задержка 0,5 секунды",
|
||||
"hover_preview_delay_1s": "Задержка 1 секунда",
|
||||
"hover_preview_delay_2s": "Задержка 2 секунды",
|
||||
"hover_preview_off": "Отключено"
|
||||
"hover_preview_off": "Отключено",
|
||||
"show_birthday_calendar": "Календарь дней рождения",
|
||||
"show_birthday_calendar_desc": "Показать виртуальный календарь с днями рождения из ваших контактов"
|
||||
},
|
||||
"days": {
|
||||
"monday": "Понедельник",
|
||||
|
||||
@@ -153,6 +153,10 @@ interface SettingsState {
|
||||
enableCalendarTasks: boolean;
|
||||
showTasksOnCalendar: boolean;
|
||||
|
||||
// Contact Birthday Calendar
|
||||
showBirthdayCalendar: boolean;
|
||||
birthdayCalendarColor: string;
|
||||
|
||||
// Email Notifications
|
||||
emailNotificationsEnabled: boolean;
|
||||
emailNotificationSound: boolean;
|
||||
@@ -278,6 +282,10 @@ const DEFAULT_SETTINGS = {
|
||||
enableCalendarTasks: false,
|
||||
showTasksOnCalendar: true,
|
||||
|
||||
// Contact Birthday Calendar
|
||||
showBirthdayCalendar: false,
|
||||
birthdayCalendarColor: '#eab308',
|
||||
|
||||
// Email Notifications
|
||||
emailNotificationsEnabled: true,
|
||||
emailNotificationSound: true,
|
||||
@@ -392,6 +400,8 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
calendarInvitationParsingEnabled: state.calendarInvitationParsingEnabled,
|
||||
enableCalendarTasks: state.enableCalendarTasks,
|
||||
showTasksOnCalendar: state.showTasksOnCalendar,
|
||||
showBirthdayCalendar: state.showBirthdayCalendar,
|
||||
birthdayCalendarColor: state.birthdayCalendarColor,
|
||||
expandedFilterView: state.expandedFilterView,
|
||||
showTimeInMonthView: state.showTimeInMonthView,
|
||||
showWeekNumbers: state.showWeekNumbers,
|
||||
|
||||
Reference in New Issue
Block a user