Files
bullseye-app/src/routes/(app)/habits/HabitCreationForm.svelte
T
hiperman 3b9c179643 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
2026-02-25 01:15:58 -05:00

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>