mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-18 00:49:40 +02:00
feat(shared-stores,shared-ui): add shared reminder system
Add notificationService (Browser Notification API wrapper), createReminderScheduler (30s poller with source pattern for checking due reminders), and ReminderPicker UI component. Todo module gets todoReminderSource (checks task dueDate - minutesBefore) and ReminderSelector now delegates to shared ReminderPicker. Scheduler supports multiple sources (todo, calendar, planta, etc.), tag-based dedup, graceful error handling, and runtime source addition. 22 new tests (8 notification + 8 scheduler + 6 ReminderPicker). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b995d52146
commit
4fa096147c
11 changed files with 624 additions and 26 deletions
|
|
@ -15,6 +15,7 @@ export {
|
|||
COLORS_16,
|
||||
DEFAULT_COLOR,
|
||||
getRandomColor,
|
||||
ReminderPicker,
|
||||
} from './molecules';
|
||||
export type { SelectOption, FilterDropdownOption } from './molecules';
|
||||
|
||||
|
|
|
|||
68
packages/shared-ui/src/molecules/ReminderPicker.svelte
Normal file
68
packages/shared-ui/src/molecules/ReminderPicker.svelte
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<script lang="ts">
|
||||
import { Bell, BellSlash } from '@manacore/shared-icons';
|
||||
|
||||
/**
|
||||
* Reusable reminder time picker dropdown.
|
||||
* Lets user select "X minutes before" for reminders on tasks, events, etc.
|
||||
*/
|
||||
|
||||
interface ReminderOption {
|
||||
value: number | null;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
/** Selected value in minutes (null = no reminder) */
|
||||
value: number | null;
|
||||
/** Called when selection changes */
|
||||
onChange: (minutes: number | null) => void;
|
||||
/** Custom options (defaults to standard set) */
|
||||
options?: ReminderOption[];
|
||||
/** Disable the picker */
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: ReminderOption[] = [
|
||||
{ value: null, label: 'Keine Erinnerung' },
|
||||
{ value: 5, label: '5 Minuten vorher' },
|
||||
{ value: 15, label: '15 Minuten vorher' },
|
||||
{ value: 30, label: '30 Minuten vorher' },
|
||||
{ value: 60, label: '1 Stunde vorher' },
|
||||
{ value: 1440, label: '1 Tag vorher' },
|
||||
];
|
||||
|
||||
let { value, onChange, options = DEFAULT_OPTIONS, disabled = false }: Props = $props();
|
||||
|
||||
function handleChange(e: Event) {
|
||||
const target = e.target as HTMLSelectElement;
|
||||
const raw = target.value;
|
||||
onChange(raw === '' ? null : parseInt(raw, 10));
|
||||
}
|
||||
|
||||
const displayLabel = $derived(
|
||||
options.find((o) => o.value === value)?.label ?? 'Keine Erinnerung'
|
||||
);
|
||||
|
||||
const hasReminder = $derived(value !== null);
|
||||
</script>
|
||||
|
||||
<div class="inline-flex items-center gap-1.5">
|
||||
{#if hasReminder}
|
||||
<Bell size={14} weight="fill" class="text-primary flex-shrink-0" />
|
||||
{:else}
|
||||
<BellSlash size={14} class="text-muted-foreground flex-shrink-0" />
|
||||
{/if}
|
||||
<select
|
||||
class="appearance-none bg-transparent text-xs cursor-pointer
|
||||
{hasReminder ? 'text-primary font-medium' : 'text-muted-foreground'}
|
||||
focus:outline-none"
|
||||
{disabled}
|
||||
onchange={handleChange}
|
||||
>
|
||||
{#each options as option}
|
||||
<option value={option.value ?? ''} selected={option.value === value}>
|
||||
{option.label}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
57
packages/shared-ui/src/molecules/ReminderPicker.test.ts
Normal file
57
packages/shared-ui/src/molecules/ReminderPicker.test.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, fireEvent } from '@testing-library/svelte';
|
||||
import ReminderPicker from './ReminderPicker.svelte';
|
||||
|
||||
describe('ReminderPicker', () => {
|
||||
it('renders a select element', () => {
|
||||
const { container } = render(ReminderPicker, {
|
||||
props: { value: null, onChange: vi.fn() },
|
||||
});
|
||||
expect(container.querySelector('select')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders default options', () => {
|
||||
const { container } = render(ReminderPicker, {
|
||||
props: { value: null, onChange: vi.fn() },
|
||||
});
|
||||
const options = container.querySelectorAll('option');
|
||||
expect(options.length).toBeGreaterThanOrEqual(5);
|
||||
});
|
||||
|
||||
it('shows bell icon when reminder is set', () => {
|
||||
const { container } = render(ReminderPicker, {
|
||||
props: { value: 15, onChange: vi.fn() },
|
||||
});
|
||||
// Bell icon should be present (not BellSlash)
|
||||
const svgs = container.querySelectorAll('svg');
|
||||
expect(svgs.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('calls onChange when selection changes', async () => {
|
||||
const onChange = vi.fn();
|
||||
const { container } = render(ReminderPicker, {
|
||||
props: { value: null, onChange },
|
||||
});
|
||||
const select = container.querySelector('select')!;
|
||||
await fireEvent.change(select, { target: { value: '30' } });
|
||||
expect(onChange).toHaveBeenCalledWith(30);
|
||||
});
|
||||
|
||||
it('calls onChange with null for "no reminder"', async () => {
|
||||
const onChange = vi.fn();
|
||||
const { container } = render(ReminderPicker, {
|
||||
props: { value: 15, onChange },
|
||||
});
|
||||
const select = container.querySelector('select')!;
|
||||
await fireEvent.change(select, { target: { value: '' } });
|
||||
expect(onChange).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it('supports disabled state', () => {
|
||||
const { container } = render(ReminderPicker, {
|
||||
props: { value: null, onChange: vi.fn(), disabled: true },
|
||||
});
|
||||
const select = container.querySelector('select')!;
|
||||
expect(select.disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -7,6 +7,7 @@ export { default as FilterDropdown } from './FilterDropdown.svelte';
|
|||
export { default as FavoriteButton } from './FavoriteButton.svelte';
|
||||
export { default as ColorPicker } from './ColorPicker.svelte';
|
||||
export { COLORS_12, COLORS_16, DEFAULT_COLOR, getRandomColor } from './ColorPicker.constants';
|
||||
export { default as ReminderPicker } from './ReminderPicker.svelte';
|
||||
export type { SelectOption } from './Select.types';
|
||||
export type { FilterDropdownOption } from './FilterDropdown.types';
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue