Mega push vol1
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user