Mega push vol1

This commit is contained in:
AlacrisDevs
2026-02-05 01:09:55 +02:00
parent 2cfbd8531a
commit 1534e1f0af
24 changed files with 1953 additions and 617 deletions

View File

@@ -1,9 +1,9 @@
<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';
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;
@@ -19,52 +19,77 @@
onClose: () => void;
onUpdate: (card: KanbanCard) => void;
onDelete: (cardId: string) => void;
mode?: "edit" | "create";
columnId?: string;
userId?: string;
onCreate?: (card: KanbanCard) => void;
}
let { card, isOpen, onClose, onUpdate, onDelete }: Props = $props();
let {
card,
isOpen,
onClose,
onUpdate,
onDelete,
mode = "edit",
columnId,
userId,
onCreate,
}: Props = $props();
const supabase = getContext<SupabaseClient<Database>>('supabase');
const supabase = getContext<SupabaseClient<Database>>("supabase");
let title = $state('');
let description = $state('');
let title = $state("");
let description = $state("");
let checklist = $state<ChecklistItem[]>([]);
let newItemTitle = $state('');
let newItemTitle = $state("");
let isLoading = $state(false);
let isSaving = $state(false);
$effect(() => {
if (card && isOpen) {
title = card.title;
description = card.description ?? '';
loadChecklist();
if (isOpen) {
if (mode === "edit" && card) {
title = card.title;
description = card.description ?? "";
loadChecklist();
} else if (mode === "create") {
title = "";
description = "";
checklist = [];
}
}
});
async function loadChecklist() {
if (!card) return;
isLoading = true;
const { data } = await supabase
.from('checklist_items')
.select('*')
.eq('card_id', card.id)
.order('position');
.from("checklist_items")
.select("*")
.eq("card_id", card.id)
.order("position");
checklist = (data ?? []) as ChecklistItem[];
isLoading = false;
}
async function handleSave() {
if (mode === "create") {
await handleCreate();
return;
}
if (!card) return;
isSaving = true;
const { error } = await supabase
.from('kanban_cards')
.update({
title,
description: description || null
.from("kanban_cards")
.update({
title,
description: description || null,
})
.eq('id', card.id);
.eq("id", card.id);
if (!error) {
onUpdate({ ...card, title, description: description || null });
@@ -72,75 +97,108 @@
isSaving = false;
}
async function handleCreate() {
if (!title.trim() || !columnId || !userId) return;
isSaving = true;
const { data: column } = await supabase
.from("kanban_columns")
.select("cards:kanban_cards(count)")
.eq("id", columnId)
.single();
const position = (column as any)?.cards?.[0]?.count ?? 0;
const { data: newCard, error } = await supabase
.from("kanban_cards")
.insert({
column_id: columnId,
title,
description: description || null,
position,
created_by: userId,
})
.select()
.single();
if (!error && newCard) {
onCreate?.(newCard as KanbanCard);
onClose();
}
isSaving = false;
}
async function handleAddItem() {
if (!card || !newItemTitle.trim()) return;
const position = checklist.length;
const { data, error } = await supabase
.from('checklist_items')
.from("checklist_items")
.insert({
card_id: card.id,
title: newItemTitle,
position,
completed: false
completed: false,
})
.select()
.single();
if (!error && data) {
checklist = [...checklist, data as ChecklistItem];
newItemTitle = '';
newItemTitle = "";
}
}
async function toggleItem(item: ChecklistItem) {
const { error } = await supabase
.from('checklist_items')
.from("checklist_items")
.update({ completed: !item.completed })
.eq('id', item.id);
.eq("id", item.id);
if (!error) {
checklist = checklist.map(i =>
i.id === item.id ? { ...i, completed: !i.completed } : i
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')
.from("checklist_items")
.delete()
.eq('id', itemId);
.eq("id", itemId);
if (!error) {
checklist = checklist.filter(i => i.id !== itemId);
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);
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);
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}
<Modal
{isOpen}
{onClose}
title={mode === "create" ? "Add Card" : "Card Details"}
size="lg"
>
{#if mode === "create" || card}
<div class="space-y-5">
<Input
label="Title"
bind:value={title}
placeholder="Card title"
/>
<Input label="Title" bind:value={title} placeholder="Card title" />
<Textarea
label="Description"
@@ -151,15 +209,21 @@
<div>
<div class="flex items-center justify-between mb-3">
<label class="text-sm font-medium text-light">Checklist</label>
<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>
<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
<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>
@@ -174,16 +238,28 @@
<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'}"
{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">
<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'}">
<span
class="flex-1 text-sm {item.completed
? 'line-through text-light/40'
: 'text-light'}"
>
{item.title}
</span>
<button
@@ -191,7 +267,13 @@
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">
<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>
@@ -206,23 +288,38 @@
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()}
onkeydown={(e) =>
e.key === "Enter" && handleAddItem()}
/>
<Button size="sm" onclick={handleAddItem} disabled={!newItemTitle.trim()}>
<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 items-center justify-between pt-3 border-t border-light/10"
>
{#if mode === "edit"}
<Button variant="danger" onclick={handleDelete}>
Delete Card
</Button>
{:else}
<div></div>
{/if}
<div class="flex gap-2">
<Button variant="ghost" onclick={onClose}>Cancel</Button>
<Button onclick={handleSave} loading={isSaving}>
Save Changes
<Button
onclick={handleSave}
loading={isSaving}
disabled={!title.trim()}
>
{mode === "create" ? "Add Card" : "Save Changes"}
</Button>
</div>
</div>