First commit

This commit is contained in:
AlacrisDevs
2026-02-04 23:01:44 +02:00
commit cfec43f7ef
78 changed files with 9509 additions and 0 deletions

View File

@@ -0,0 +1,231 @@
<script lang="ts">
import { getContext } from 'svelte';
import { Modal, Button, Input, Textarea } from '$lib/components/ui';
import type { KanbanCard } from '$lib/supabase/types';
import type { SupabaseClient } from '@supabase/supabase-js';
import type { Database } from '$lib/supabase/types';
interface ChecklistItem {
id: string;
card_id: string;
title: string;
completed: boolean;
position: number;
}
interface Props {
card: KanbanCard | null;
isOpen: boolean;
onClose: () => void;
onUpdate: (card: KanbanCard) => void;
onDelete: (cardId: string) => void;
}
let { card, isOpen, onClose, onUpdate, onDelete }: Props = $props();
const supabase = getContext<SupabaseClient<Database>>('supabase');
let title = $state('');
let description = $state('');
let checklist = $state<ChecklistItem[]>([]);
let newItemTitle = $state('');
let isLoading = $state(false);
let isSaving = $state(false);
$effect(() => {
if (card && isOpen) {
title = card.title;
description = card.description ?? '';
loadChecklist();
}
});
async function loadChecklist() {
if (!card) return;
isLoading = true;
const { data } = await supabase
.from('checklist_items')
.select('*')
.eq('card_id', card.id)
.order('position');
checklist = (data ?? []) as ChecklistItem[];
isLoading = false;
}
async function handleSave() {
if (!card) return;
isSaving = true;
const { error } = await supabase
.from('kanban_cards')
.update({
title,
description: description || null
})
.eq('id', card.id);
if (!error) {
onUpdate({ ...card, title, description: description || null });
}
isSaving = false;
}
async function handleAddItem() {
if (!card || !newItemTitle.trim()) return;
const position = checklist.length;
const { data, error } = await supabase
.from('checklist_items')
.insert({
card_id: card.id,
title: newItemTitle,
position,
completed: false
})
.select()
.single();
if (!error && data) {
checklist = [...checklist, data as ChecklistItem];
newItemTitle = '';
}
}
async function toggleItem(item: ChecklistItem) {
const { error } = await supabase
.from('checklist_items')
.update({ completed: !item.completed })
.eq('id', item.id);
if (!error) {
checklist = checklist.map(i =>
i.id === item.id ? { ...i, completed: !i.completed } : i
);
}
}
async function deleteItem(itemId: string) {
const { error } = await supabase
.from('checklist_items')
.delete()
.eq('id', itemId);
if (!error) {
checklist = checklist.filter(i => i.id !== itemId);
}
}
async function handleDelete() {
if (!card || !confirm('Delete this card?')) return;
await supabase
.from('kanban_cards')
.delete()
.eq('id', card.id);
onDelete(card.id);
onClose();
}
const completedCount = $derived(checklist.filter(i => i.completed).length);
const progress = $derived(checklist.length > 0 ? (completedCount / checklist.length) * 100 : 0);
</script>
<Modal {isOpen} {onClose} title="Card Details" size="lg">
{#if card}
<div class="space-y-5">
<Input
label="Title"
bind:value={title}
placeholder="Card title"
/>
<Textarea
label="Description"
bind:value={description}
placeholder="Add a more detailed description..."
rows={3}
/>
<div>
<div class="flex items-center justify-between mb-3">
<label class="text-sm font-medium text-light">Checklist</label>
{#if checklist.length > 0}
<span class="text-xs text-light/50">{completedCount}/{checklist.length}</span>
{/if}
</div>
{#if checklist.length > 0}
<div class="mb-3 h-1.5 bg-light/10 rounded-full overflow-hidden">
<div
class="h-full bg-success transition-all duration-300"
style="width: {progress}%"
></div>
</div>
{/if}
{#if isLoading}
<div class="text-light/50 text-sm py-2">Loading...</div>
{:else}
<div class="space-y-2 mb-3">
{#each checklist as item}
<div class="flex items-center gap-3 group">
<button
class="w-5 h-5 rounded border flex items-center justify-center transition-colors
{item.completed ? 'bg-success border-success' : 'border-light/30 hover:border-light/50'}"
onclick={() => toggleItem(item)}
>
{#if item.completed}
<svg class="w-3 h-3 text-white" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
<polyline points="20,6 9,17 4,12" />
</svg>
{/if}
</button>
<span class="flex-1 text-sm {item.completed ? 'line-through text-light/40' : 'text-light'}">
{item.title}
</span>
<button
class="opacity-0 group-hover:opacity-100 p-1 text-light/40 hover:text-error transition-all"
onclick={() => deleteItem(item.id)}
aria-label="Delete item"
>
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
{/each}
</div>
<div class="flex gap-2">
<input
type="text"
class="flex-1 px-3 py-2 bg-dark border border-light/20 rounded-lg text-sm text-light placeholder:text-light/40 focus:outline-none focus:border-primary"
placeholder="Add an item..."
bind:value={newItemTitle}
onkeydown={(e) => e.key === 'Enter' && handleAddItem()}
/>
<Button size="sm" onclick={handleAddItem} disabled={!newItemTitle.trim()}>
Add
</Button>
</div>
{/if}
</div>
<div class="flex items-center justify-between pt-3 border-t border-light/10">
<Button variant="danger" onclick={handleDelete}>
Delete Card
</Button>
<div class="flex gap-2">
<Button variant="ghost" onclick={onClose}>Cancel</Button>
<Button onclick={handleSave} loading={isSaving}>
Save Changes
</Button>
</div>
</div>
</div>
{/if}
</Modal>