3b9c179643
\ - Add HabitGrid component with a responsive calendar-style layout\\ - Support variable habit durations with period-based cell rendering\ - Add skeleton loading states for better UX during data fetching - Add CounterHabitButton component for cells with increment/decrement interactions\ - Create HabitCellContextMenu for period-based actions (reset, set values, fail) - Add HabitCreationForm for creating new habits\ - Add HabitActionMenu component for additional operations on habits (archive, delete, edit)\ - Add SetValueDialog component for editing the value of a cell
232 lines
5.7 KiB
Svelte
232 lines
5.7 KiB
Svelte
<script lang="ts">
|
|
import { Button } from '$lib/components/ui/button';
|
|
import { Input } from '$lib/components/ui/input';
|
|
import * as Dialog from '$lib/components/ui/dialog';
|
|
import { Label } from '$lib/components/ui/label';
|
|
import { Checkbox } from '$lib/components/ui/checkbox';
|
|
import * as Collapsible from '$lib/components/ui/collapsible';
|
|
import { ChevronDown, ChevronUp } from '@lucide/svelte';
|
|
import { getHabitService } from '$lib/context/habits.svelte';
|
|
|
|
let { onHabitCreated }: { onHabitCreated?: () => Promise<void> | void } = $props();
|
|
|
|
const habitService = getHabitService();
|
|
|
|
let showAdvancedOptions = $state(false);
|
|
let isSubmitting = $state(false);
|
|
|
|
// Form fields
|
|
let name = $state('');
|
|
let target = $state(1);
|
|
let duration = $state(1);
|
|
let unit = $state('');
|
|
let increment = $state(1);
|
|
let startDate = $state(new Date().toISOString().split('T')[0]);
|
|
let active = $state(true);
|
|
|
|
|
|
async function handleSubmit() {
|
|
if (isSubmitting || !name.trim()) return;
|
|
|
|
try {
|
|
isSubmitting = true;
|
|
|
|
await habitService.createHabit({
|
|
name,
|
|
target,
|
|
duration,
|
|
increment,
|
|
unit,
|
|
startDate: (() => {
|
|
// Create date in local timezone to avoid UTC conversion issues
|
|
const [year, month, day] = startDate.split('-').map(Number);
|
|
return new Date(year, month - 1, day);
|
|
})(),
|
|
active
|
|
})
|
|
|
|
// Reset form
|
|
name = '';
|
|
target = 1;
|
|
duration = 1;
|
|
unit = '';
|
|
increment = 1;
|
|
startDate = new Date().toISOString().split('T')[0];
|
|
active = true;
|
|
showAdvancedOptions = false;
|
|
|
|
// Notify parent
|
|
onHabitCreated?.();
|
|
} catch (error) {
|
|
console.error('Failed to create habit:', error);
|
|
} finally {
|
|
isSubmitting = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<Dialog.Header>
|
|
<Dialog.Title>Create a new habit</Dialog.Title>
|
|
<Dialog.Description>
|
|
Add a new habit to track. Set your target and customize the tracking period.
|
|
</Dialog.Description>
|
|
</Dialog.Header>
|
|
|
|
<div class="space-y-6">
|
|
<!-- Habit Name -->
|
|
<div class="space-y-2">
|
|
<Label for="name" class="text-sm font-medium">
|
|
Habit Name <span class="text-destructive">*</span>
|
|
</Label>
|
|
<Input
|
|
id="name"
|
|
bind:value={name}
|
|
placeholder="e.g. Drink 8 glasses of water, Exercise for 30 minutes"
|
|
disabled={isSubmitting}
|
|
required
|
|
class="w-full"
|
|
/>
|
|
<p class="text-xs text-muted-foreground">
|
|
Give your habit a clear, specific name
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Target & Duration Row -->
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="space-y-2">
|
|
<Label for="target" class="text-sm font-medium">Target</Label>
|
|
<Input
|
|
id="target"
|
|
type="number"
|
|
bind:value={target}
|
|
min="1"
|
|
max="999"
|
|
disabled={isSubmitting}
|
|
placeholder="1"
|
|
/>
|
|
<p class="text-xs text-muted-foreground">
|
|
Daily goal to reach
|
|
</p>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<Label for="duration" class="text-sm font-medium">Period (days)</Label>
|
|
<Input
|
|
id="duration"
|
|
type="number"
|
|
bind:value={duration}
|
|
min="1"
|
|
max="30"
|
|
disabled={isSubmitting}
|
|
placeholder="1"
|
|
/>
|
|
<p class="text-xs text-muted-foreground">
|
|
How many days per period
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Advanced Options -->
|
|
<div class="space-y-4">
|
|
<Collapsible.Root bind:open={showAdvancedOptions}>
|
|
<Collapsible.Trigger class="flex items-center gap-2 py-2 text-sm font-medium hover:text-primary transition-colors">
|
|
{#if showAdvancedOptions}
|
|
<ChevronUp class="h-4 w-4" />
|
|
{:else}
|
|
<ChevronDown class="h-4 w-4" />
|
|
{/if}
|
|
Advanced Settings
|
|
</Collapsible.Trigger>
|
|
|
|
<Collapsible.Content class="space-y-4 pt-2">
|
|
<!-- Increment & Unit Row -->
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="space-y-2">
|
|
<Label for="increment" class="text-sm font-medium">Increment</Label>
|
|
<Input
|
|
id="increment"
|
|
type="number"
|
|
bind:value={increment}
|
|
min="0.1"
|
|
step="0.1"
|
|
max="100"
|
|
disabled={isSubmitting}
|
|
placeholder="1"
|
|
/>
|
|
<p class="text-xs text-muted-foreground">
|
|
Step size for tracking
|
|
</p>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<Label for="unit" class="text-sm font-medium">Unit</Label>
|
|
<Input
|
|
id="unit"
|
|
bind:value={unit}
|
|
placeholder="e.g. glasses, minutes, pages"
|
|
disabled={isSubmitting}
|
|
class="w-full"
|
|
/>
|
|
<p class="text-xs text-muted-foreground">
|
|
Unit of measurement
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Start Date & Active Row -->
|
|
<div class="grid grid-cols-2 gap-4 items-start">
|
|
<div class="space-y-2">
|
|
<Label for="startDate" class="text-sm font-medium">Start Date</Label>
|
|
<Input
|
|
id="startDate"
|
|
type="date"
|
|
bind:value={startDate}
|
|
disabled={isSubmitting}
|
|
class="w-full"
|
|
/>
|
|
<p class="text-xs text-muted-foreground">
|
|
When to begin tracking
|
|
</p>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<Label class="text-sm font-medium">Status</Label>
|
|
<div class="flex items-center space-x-2 h-10">
|
|
<Checkbox
|
|
id="active"
|
|
bind:checked={active}
|
|
disabled={isSubmitting}
|
|
/>
|
|
<Label for="active" class="text-sm font-normal cursor-pointer">
|
|
Start tracking immediately
|
|
</Label>
|
|
</div>
|
|
<p class="text-xs text-muted-foreground">
|
|
Enable habit tracking
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Collapsible.Content>
|
|
</Collapsible.Root>
|
|
</div>
|
|
</div>
|
|
|
|
<Dialog.Footer class="flex gap-2">
|
|
<Dialog.Close>
|
|
<Button variant="outline" disabled={isSubmitting}>
|
|
Cancel
|
|
</Button>
|
|
</Dialog.Close>
|
|
<Button
|
|
onclick={handleSubmit}
|
|
disabled={isSubmitting || !name.trim()}
|
|
class="gap-2"
|
|
>
|
|
{#if isSubmitting}
|
|
Creating...
|
|
{:else}
|
|
Create Habit
|
|
{/if}
|
|
</Button>
|
|
</Dialog.Footer>
|