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 { sanitizeOutgoingCalendarEventData } from "@/lib/calendar-event-normalization";
|
||||||
import { getEventStartDate } from "@/lib/calendar-utils";
|
import { getEventStartDate } from "@/lib/calendar-utils";
|
||||||
import { useTaskStore } from "@/stores/task-store";
|
import { useTaskStore } from "@/stores/task-store";
|
||||||
|
import { useContactStore } from "@/stores/contact-store";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { CalendarEvent, CalendarParticipant } from "@/lib/jmap/types";
|
import type { CalendarEvent, CalendarParticipant } from "@/lib/jmap/types";
|
||||||
import { getUserParticipantId } from "@/lib/calendar-participants";
|
import { getUserParticipantId } from "@/lib/calendar-participants";
|
||||||
|
import { generateBirthdayEvents, createBirthdayCalendar, BIRTHDAY_CALENDAR_ID } from "@/lib/birthday-calendar";
|
||||||
import { debug } from "@/lib/debug";
|
import { debug } from "@/lib/debug";
|
||||||
|
|
||||||
type PendingScopeAction =
|
type PendingScopeAction =
|
||||||
@@ -69,10 +71,11 @@ export default function CalendarPage() {
|
|||||||
setSelectedDate, setViewMode, toggleCalendarVisibility, updateCalendar,
|
setSelectedDate, setViewMode, toggleCalendarVisibility, updateCalendar,
|
||||||
refreshAllSubscriptions, icalSubscriptions,
|
refreshAllSubscriptions, icalSubscriptions,
|
||||||
} = useCalendarStore();
|
} = useCalendarStore();
|
||||||
const { firstDayOfWeek, timeFormat, showWeekNumbers, enableCalendarTasks, showTasksOnCalendar, calendarHoverPreview } = useSettingsStore();
|
const { firstDayOfWeek, timeFormat, showWeekNumbers, enableCalendarTasks, showTasksOnCalendar, calendarHoverPreview, showBirthdayCalendar, birthdayCalendarColor, updateSetting } = useSettingsStore();
|
||||||
const taskStore = useTaskStore();
|
const taskStore = useTaskStore();
|
||||||
const fetchTasksFn = useTaskStore(state => state.fetchTasks);
|
const fetchTasksFn = useTaskStore(state => state.fetchTasks);
|
||||||
const { identities } = useIdentityStore();
|
const { identities } = useIdentityStore();
|
||||||
|
const contacts = useContactStore((s) => s.contacts);
|
||||||
const normalizedViewMode = isCalendarViewMode(viewMode) ? viewMode : "month";
|
const normalizedViewMode = isCalendarViewMode(viewMode) ? viewMode : "month";
|
||||||
|
|
||||||
const currentUserEmails = useMemo(() =>
|
const currentUserEmails = useMemo(() =>
|
||||||
@@ -148,6 +151,13 @@ export default function CalendarPage() {
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [client, refreshAllSubscriptions]);
|
}, [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 dateRange = useMemo(() => {
|
||||||
const d = selectedDate;
|
const d = selectedDate;
|
||||||
switch (normalizedViewMode) {
|
switch (normalizedViewMode) {
|
||||||
@@ -743,14 +753,31 @@ export default function CalendarPage() {
|
|||||||
return () => window.removeEventListener("keydown", handleKey);
|
return () => window.removeEventListener("keydown", handleKey);
|
||||||
}, [navigatePrev, navigateNext, goToToday, setViewMode, openCreateModal, showEventModal, detailEvent, enableCalendarTasks]);
|
}, [navigatePrev, navigateNext, goToToday, setViewMode, openCreateModal, showEventModal, detailEvent, enableCalendarTasks]);
|
||||||
|
|
||||||
const visibleEvents = useMemo(() =>
|
const birthdayEvents = useMemo(() => {
|
||||||
events.filter((e) => {
|
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;
|
if (!e.start || !e.calendarIds) return false;
|
||||||
const calIds = Object.keys(e.calendarIds);
|
const calIds = Object.keys(e.calendarIds);
|
||||||
return calIds.some((id) => selectedCalendarIds.includes(id));
|
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(() => {
|
useEffect(() => {
|
||||||
const hiddenEvents = events.filter((event) => {
|
const hiddenEvents = events.filter((event) => {
|
||||||
@@ -799,7 +826,7 @@ export default function CalendarPage() {
|
|||||||
<CalendarMonthView
|
<CalendarMonthView
|
||||||
selectedDate={selectedDate}
|
selectedDate={selectedDate}
|
||||||
events={visibleEvents}
|
events={visibleEvents}
|
||||||
calendars={calendars}
|
calendars={allCalendars}
|
||||||
onSelectDate={handleSelectDate}
|
onSelectDate={handleSelectDate}
|
||||||
onSelectEvent={handleSelectEvent}
|
onSelectEvent={handleSelectEvent}
|
||||||
onHoverEvent={handleHoverEvent}
|
onHoverEvent={handleHoverEvent}
|
||||||
@@ -815,7 +842,7 @@ export default function CalendarPage() {
|
|||||||
<CalendarWeekView
|
<CalendarWeekView
|
||||||
selectedDate={selectedDate}
|
selectedDate={selectedDate}
|
||||||
events={visibleEvents}
|
events={visibleEvents}
|
||||||
calendars={calendars}
|
calendars={allCalendars}
|
||||||
onSelectDate={handleSelectDate}
|
onSelectDate={handleSelectDate}
|
||||||
onSelectEvent={handleSelectEvent}
|
onSelectEvent={handleSelectEvent}
|
||||||
onHoverEvent={handleHoverEvent}
|
onHoverEvent={handleHoverEvent}
|
||||||
@@ -834,7 +861,7 @@ export default function CalendarPage() {
|
|||||||
<CalendarDayView
|
<CalendarDayView
|
||||||
selectedDate={selectedDate}
|
selectedDate={selectedDate}
|
||||||
events={visibleEvents}
|
events={visibleEvents}
|
||||||
calendars={calendars}
|
calendars={allCalendars}
|
||||||
onSelectEvent={handleSelectEvent}
|
onSelectEvent={handleSelectEvent}
|
||||||
onHoverEvent={handleHoverEvent}
|
onHoverEvent={handleHoverEvent}
|
||||||
onHoverLeave={handleHoverLeave}
|
onHoverLeave={handleHoverLeave}
|
||||||
@@ -851,7 +878,7 @@ export default function CalendarPage() {
|
|||||||
<CalendarAgendaView
|
<CalendarAgendaView
|
||||||
selectedDate={selectedDate}
|
selectedDate={selectedDate}
|
||||||
events={visibleEvents}
|
events={visibleEvents}
|
||||||
calendars={calendars}
|
calendars={allCalendars}
|
||||||
onSelectEvent={handleSelectEvent}
|
onSelectEvent={handleSelectEvent}
|
||||||
onHoverEvent={handleHoverEvent}
|
onHoverEvent={handleHoverEvent}
|
||||||
onHoverLeave={handleHoverLeave}
|
onHoverLeave={handleHoverLeave}
|
||||||
@@ -942,10 +969,14 @@ export default function CalendarPage() {
|
|||||||
showWeekNumbers={showWeekNumbers}
|
showWeekNumbers={showWeekNumbers}
|
||||||
/>
|
/>
|
||||||
<CalendarSidebarPanel
|
<CalendarSidebarPanel
|
||||||
calendars={calendars}
|
calendars={allCalendars}
|
||||||
selectedCalendarIds={selectedCalendarIds}
|
selectedCalendarIds={selectedCalendarIds}
|
||||||
onToggleVisibility={toggleCalendarVisibility}
|
onToggleVisibility={toggleCalendarVisibility}
|
||||||
onColorChange={client ? (calendarId, color) => {
|
onColorChange={client ? (calendarId, color) => {
|
||||||
|
if (calendarId === BIRTHDAY_CALENDAR_ID) {
|
||||||
|
updateSetting('birthdayCalendarColor', color);
|
||||||
|
return;
|
||||||
|
}
|
||||||
updateCalendar(client, calendarId, { color });
|
updateCalendar(client, calendarId, { color });
|
||||||
} : undefined}
|
} : undefined}
|
||||||
onSubscribe={() => setShowSubscriptionModal(true)}
|
onSubscribe={() => setShowSubscriptionModal(true)}
|
||||||
|
|||||||
@@ -2,13 +2,14 @@
|
|||||||
|
|
||||||
import { useState, useRef, useEffect, useMemo } from "react";
|
import { useState, useRef, useEffect, useMemo } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
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 { cn, formatDateTime } from "@/lib/utils";
|
||||||
import type { Calendar } from "@/lib/jmap/types";
|
import type { Calendar } from "@/lib/jmap/types";
|
||||||
import { CalendarColorPicker } from "@/components/settings/calendar-management-settings";
|
import { CalendarColorPicker } from "@/components/settings/calendar-management-settings";
|
||||||
import { useCalendarStore } from "@/stores/calendar-store";
|
import { useCalendarStore } from "@/stores/calendar-store";
|
||||||
import { useSettingsStore } from "@/stores/settings-store";
|
import { useSettingsStore } from "@/stores/settings-store";
|
||||||
import { useTaskStore } from "@/stores/task-store";
|
import { useTaskStore } from "@/stores/task-store";
|
||||||
|
import { BIRTHDAY_CALENDAR_ID } from "@/lib/birthday-calendar";
|
||||||
import { toast } from "@/stores/toast-store";
|
import { toast } from "@/stores/toast-store";
|
||||||
import type { IJMAPClient } from '@/lib/jmap/client-interface';
|
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>
|
</button>
|
||||||
|
|
||||||
{/* Subscription context menu on right-click */}
|
{/* Subscription context menu on right-click */}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export function CalendarSettings() {
|
|||||||
showWeekNumbers,
|
showWeekNumbers,
|
||||||
enableCalendarTasks,
|
enableCalendarTasks,
|
||||||
showTasksOnCalendar,
|
showTasksOnCalendar,
|
||||||
|
showBirthdayCalendar,
|
||||||
calendarHoverPreview,
|
calendarHoverPreview,
|
||||||
updateSetting,
|
updateSetting,
|
||||||
} = useSettingsStore();
|
} = useSettingsStore();
|
||||||
@@ -98,6 +99,16 @@ export function CalendarSettings() {
|
|||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|
||||||
|
<SettingItem
|
||||||
|
label={t('show_birthday_calendar')}
|
||||||
|
description={t('show_birthday_calendar_desc')}
|
||||||
|
>
|
||||||
|
<ToggleSwitch
|
||||||
|
checked={showBirthdayCalendar}
|
||||||
|
onChange={(checked) => updateSetting('showBirthdayCalendar', checked)}
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
{isFeatureEnabled('calendarTasksEnabled') && (
|
{isFeatureEnabled('calendarTasksEnabled') && (
|
||||||
<>
|
<>
|
||||||
<SettingItem
|
<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_email": "Zurück zu E-Mails",
|
||||||
"back_to_month": "Zurück zur Monatsansicht",
|
"back_to_month": "Zurück zur Monatsansicht",
|
||||||
"my_calendars": "Meine Kalender",
|
"my_calendars": "Meine Kalender",
|
||||||
|
"birthday_calendar": "Geburtstage",
|
||||||
"mini_calendar_change": "Klicken, um den Monat zu wechseln",
|
"mini_calendar_change": "Klicken, um den Monat zu wechseln",
|
||||||
"views": {
|
"views": {
|
||||||
"month": "Monat",
|
"month": "Monat",
|
||||||
@@ -2010,7 +2011,9 @@
|
|||||||
"hover_preview_delay_500ms": "0,5 Sekunden Verzögerung",
|
"hover_preview_delay_500ms": "0,5 Sekunden Verzögerung",
|
||||||
"hover_preview_delay_1s": "1 Sekunde Verzögerung",
|
"hover_preview_delay_1s": "1 Sekunde Verzögerung",
|
||||||
"hover_preview_delay_2s": "2 Sekunden 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": {
|
"days": {
|
||||||
"monday": "Montag",
|
"monday": "Montag",
|
||||||
|
|||||||
@@ -1876,6 +1876,7 @@
|
|||||||
"back_to_email": "Back to email",
|
"back_to_email": "Back to email",
|
||||||
"back_to_month": "Back to month",
|
"back_to_month": "Back to month",
|
||||||
"my_calendars": "Calendars",
|
"my_calendars": "Calendars",
|
||||||
|
"birthday_calendar": "Birthdays",
|
||||||
"mini_calendar_change": "Click to change month",
|
"mini_calendar_change": "Click to change month",
|
||||||
"views": {
|
"views": {
|
||||||
"month": "Month",
|
"month": "Month",
|
||||||
@@ -2013,7 +2014,9 @@
|
|||||||
"hover_preview_delay_500ms": "0.5-second delay",
|
"hover_preview_delay_500ms": "0.5-second delay",
|
||||||
"hover_preview_delay_1s": "1-second delay",
|
"hover_preview_delay_1s": "1-second delay",
|
||||||
"hover_preview_delay_2s": "2-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": {
|
"days": {
|
||||||
"monday": "Monday",
|
"monday": "Monday",
|
||||||
|
|||||||
@@ -1873,6 +1873,7 @@
|
|||||||
"back_to_email": "Volver al correo",
|
"back_to_email": "Volver al correo",
|
||||||
"back_to_month": "Volver al mes",
|
"back_to_month": "Volver al mes",
|
||||||
"my_calendars": "Mis calendarios",
|
"my_calendars": "Mis calendarios",
|
||||||
|
"birthday_calendar": "Cumpleaños",
|
||||||
"mini_calendar_change": "Clic para cambiar de mes",
|
"mini_calendar_change": "Clic para cambiar de mes",
|
||||||
"views": {
|
"views": {
|
||||||
"month": "Mes",
|
"month": "Mes",
|
||||||
@@ -2010,7 +2011,9 @@
|
|||||||
"hover_preview_delay_500ms": "Retraso de 0,5 segundos",
|
"hover_preview_delay_500ms": "Retraso de 0,5 segundos",
|
||||||
"hover_preview_delay_1s": "Retraso de 1 segundo",
|
"hover_preview_delay_1s": "Retraso de 1 segundo",
|
||||||
"hover_preview_delay_2s": "Retraso de 2 segundos",
|
"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": {
|
"days": {
|
||||||
"monday": "Lunes",
|
"monday": "Lunes",
|
||||||
|
|||||||
@@ -1873,6 +1873,7 @@
|
|||||||
"back_to_email": "Retour aux e-mails",
|
"back_to_email": "Retour aux e-mails",
|
||||||
"back_to_month": "Retour au mois",
|
"back_to_month": "Retour au mois",
|
||||||
"my_calendars": "Mes calendriers",
|
"my_calendars": "Mes calendriers",
|
||||||
|
"birthday_calendar": "Anniversaires",
|
||||||
"mini_calendar_change": "Cliquer pour changer de mois",
|
"mini_calendar_change": "Cliquer pour changer de mois",
|
||||||
"views": {
|
"views": {
|
||||||
"month": "Mois",
|
"month": "Mois",
|
||||||
@@ -2010,7 +2011,9 @@
|
|||||||
"hover_preview_delay_500ms": "Délai de 0,5 seconde",
|
"hover_preview_delay_500ms": "Délai de 0,5 seconde",
|
||||||
"hover_preview_delay_1s": "Délai de 1 seconde",
|
"hover_preview_delay_1s": "Délai de 1 seconde",
|
||||||
"hover_preview_delay_2s": "Délai de 2 secondes",
|
"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": {
|
"days": {
|
||||||
"monday": "Lundi",
|
"monday": "Lundi",
|
||||||
|
|||||||
@@ -1873,6 +1873,7 @@
|
|||||||
"back_to_email": "Torna alla posta",
|
"back_to_email": "Torna alla posta",
|
||||||
"back_to_month": "Torna al mese",
|
"back_to_month": "Torna al mese",
|
||||||
"my_calendars": "I miei calendari",
|
"my_calendars": "I miei calendari",
|
||||||
|
"birthday_calendar": "Compleanni",
|
||||||
"mini_calendar_change": "Clicca per cambiare mese",
|
"mini_calendar_change": "Clicca per cambiare mese",
|
||||||
"views": {
|
"views": {
|
||||||
"month": "Mese",
|
"month": "Mese",
|
||||||
@@ -2010,7 +2011,9 @@
|
|||||||
"hover_preview_delay_500ms": "Ritardo di 0,5 secondi",
|
"hover_preview_delay_500ms": "Ritardo di 0,5 secondi",
|
||||||
"hover_preview_delay_1s": "Ritardo di 1 secondo",
|
"hover_preview_delay_1s": "Ritardo di 1 secondo",
|
||||||
"hover_preview_delay_2s": "Ritardo di 2 secondi",
|
"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": {
|
"days": {
|
||||||
"monday": "Lunedì",
|
"monday": "Lunedì",
|
||||||
|
|||||||
@@ -1873,6 +1873,7 @@
|
|||||||
"back_to_email": "メールに戻る",
|
"back_to_email": "メールに戻る",
|
||||||
"back_to_month": "月表示に戻る",
|
"back_to_month": "月表示に戻る",
|
||||||
"my_calendars": "マイカレンダー",
|
"my_calendars": "マイカレンダー",
|
||||||
|
"birthday_calendar": "誕生日",
|
||||||
"mini_calendar_change": "クリックで月を変更",
|
"mini_calendar_change": "クリックで月を変更",
|
||||||
"views": {
|
"views": {
|
||||||
"month": "月",
|
"month": "月",
|
||||||
@@ -2010,7 +2011,9 @@
|
|||||||
"hover_preview_delay_500ms": "0.5秒遅延",
|
"hover_preview_delay_500ms": "0.5秒遅延",
|
||||||
"hover_preview_delay_1s": "1秒遅延",
|
"hover_preview_delay_1s": "1秒遅延",
|
||||||
"hover_preview_delay_2s": "2秒遅延",
|
"hover_preview_delay_2s": "2秒遅延",
|
||||||
"hover_preview_off": "無効"
|
"hover_preview_off": "無効",
|
||||||
|
"show_birthday_calendar": "誕生日カレンダー",
|
||||||
|
"show_birthday_calendar_desc": "連絡先の誕生日を表示する仮想カレンダーを表示します"
|
||||||
},
|
},
|
||||||
"days": {
|
"days": {
|
||||||
"monday": "月曜日",
|
"monday": "月曜日",
|
||||||
|
|||||||
@@ -1873,6 +1873,7 @@
|
|||||||
"back_to_email": "Terug naar e-mail",
|
"back_to_email": "Terug naar e-mail",
|
||||||
"back_to_month": "Terug naar maand",
|
"back_to_month": "Terug naar maand",
|
||||||
"my_calendars": "Mijn agenda's",
|
"my_calendars": "Mijn agenda's",
|
||||||
|
"birthday_calendar": "Verjaardagen",
|
||||||
"mini_calendar_change": "Klik om van maand te wisselen",
|
"mini_calendar_change": "Klik om van maand te wisselen",
|
||||||
"views": {
|
"views": {
|
||||||
"month": "Maand",
|
"month": "Maand",
|
||||||
@@ -2010,7 +2011,9 @@
|
|||||||
"hover_preview_delay_500ms": "0,5 seconde vertraging",
|
"hover_preview_delay_500ms": "0,5 seconde vertraging",
|
||||||
"hover_preview_delay_1s": "1 seconde vertraging",
|
"hover_preview_delay_1s": "1 seconde vertraging",
|
||||||
"hover_preview_delay_2s": "2 seconden 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": {
|
"days": {
|
||||||
"monday": "Maandag",
|
"monday": "Maandag",
|
||||||
|
|||||||
@@ -1873,6 +1873,7 @@
|
|||||||
"back_to_email": "Voltar ao e-mail",
|
"back_to_email": "Voltar ao e-mail",
|
||||||
"back_to_month": "Voltar ao mês",
|
"back_to_month": "Voltar ao mês",
|
||||||
"my_calendars": "Meus calendários",
|
"my_calendars": "Meus calendários",
|
||||||
|
"birthday_calendar": "Aniversários",
|
||||||
"mini_calendar_change": "Clique para mudar o mês",
|
"mini_calendar_change": "Clique para mudar o mês",
|
||||||
"views": {
|
"views": {
|
||||||
"month": "Mês",
|
"month": "Mês",
|
||||||
@@ -2010,7 +2011,9 @@
|
|||||||
"hover_preview_delay_500ms": "Atraso de 0,5 segundos",
|
"hover_preview_delay_500ms": "Atraso de 0,5 segundos",
|
||||||
"hover_preview_delay_1s": "Atraso de 1 segundo",
|
"hover_preview_delay_1s": "Atraso de 1 segundo",
|
||||||
"hover_preview_delay_2s": "Atraso de 2 segundos",
|
"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": {
|
"days": {
|
||||||
"monday": "Segunda-feira",
|
"monday": "Segunda-feira",
|
||||||
|
|||||||
@@ -1873,6 +1873,7 @@
|
|||||||
"back_to_email": "Вернуться к почте",
|
"back_to_email": "Вернуться к почте",
|
||||||
"back_to_month": "Вернуться к месяцу",
|
"back_to_month": "Вернуться к месяцу",
|
||||||
"my_calendars": "Календари",
|
"my_calendars": "Календари",
|
||||||
|
"birthday_calendar": "Дни рождения",
|
||||||
"mini_calendar_change": "Нажмите для смены месяца",
|
"mini_calendar_change": "Нажмите для смены месяца",
|
||||||
"views": {
|
"views": {
|
||||||
"month": "Месяц",
|
"month": "Месяц",
|
||||||
@@ -2010,7 +2011,9 @@
|
|||||||
"hover_preview_delay_500ms": "Задержка 0,5 секунды",
|
"hover_preview_delay_500ms": "Задержка 0,5 секунды",
|
||||||
"hover_preview_delay_1s": "Задержка 1 секунда",
|
"hover_preview_delay_1s": "Задержка 1 секунда",
|
||||||
"hover_preview_delay_2s": "Задержка 2 секунды",
|
"hover_preview_delay_2s": "Задержка 2 секунды",
|
||||||
"hover_preview_off": "Отключено"
|
"hover_preview_off": "Отключено",
|
||||||
|
"show_birthday_calendar": "Календарь дней рождения",
|
||||||
|
"show_birthday_calendar_desc": "Показать виртуальный календарь с днями рождения из ваших контактов"
|
||||||
},
|
},
|
||||||
"days": {
|
"days": {
|
||||||
"monday": "Понедельник",
|
"monday": "Понедельник",
|
||||||
|
|||||||
@@ -153,6 +153,10 @@ interface SettingsState {
|
|||||||
enableCalendarTasks: boolean;
|
enableCalendarTasks: boolean;
|
||||||
showTasksOnCalendar: boolean;
|
showTasksOnCalendar: boolean;
|
||||||
|
|
||||||
|
// Contact Birthday Calendar
|
||||||
|
showBirthdayCalendar: boolean;
|
||||||
|
birthdayCalendarColor: string;
|
||||||
|
|
||||||
// Email Notifications
|
// Email Notifications
|
||||||
emailNotificationsEnabled: boolean;
|
emailNotificationsEnabled: boolean;
|
||||||
emailNotificationSound: boolean;
|
emailNotificationSound: boolean;
|
||||||
@@ -278,6 +282,10 @@ const DEFAULT_SETTINGS = {
|
|||||||
enableCalendarTasks: false,
|
enableCalendarTasks: false,
|
||||||
showTasksOnCalendar: true,
|
showTasksOnCalendar: true,
|
||||||
|
|
||||||
|
// Contact Birthday Calendar
|
||||||
|
showBirthdayCalendar: false,
|
||||||
|
birthdayCalendarColor: '#eab308',
|
||||||
|
|
||||||
// Email Notifications
|
// Email Notifications
|
||||||
emailNotificationsEnabled: true,
|
emailNotificationsEnabled: true,
|
||||||
emailNotificationSound: true,
|
emailNotificationSound: true,
|
||||||
@@ -392,6 +400,8 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
calendarInvitationParsingEnabled: state.calendarInvitationParsingEnabled,
|
calendarInvitationParsingEnabled: state.calendarInvitationParsingEnabled,
|
||||||
enableCalendarTasks: state.enableCalendarTasks,
|
enableCalendarTasks: state.enableCalendarTasks,
|
||||||
showTasksOnCalendar: state.showTasksOnCalendar,
|
showTasksOnCalendar: state.showTasksOnCalendar,
|
||||||
|
showBirthdayCalendar: state.showBirthdayCalendar,
|
||||||
|
birthdayCalendarColor: state.birthdayCalendarColor,
|
||||||
expandedFilterView: state.expandedFilterView,
|
expandedFilterView: state.expandedFilterView,
|
||||||
showTimeInMonthView: state.showTimeInMonthView,
|
showTimeInMonthView: state.showTimeInMonthView,
|
||||||
showWeekNumbers: state.showWeekNumbers,
|
showWeekNumbers: state.showWeekNumbers,
|
||||||
|
|||||||
Reference in New Issue
Block a user