add habit tracking page with grid layout\
\ - 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
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user