Mega push vol 4
This commit is contained in:
@@ -1,17 +1,24 @@
|
||||
<script lang="ts">
|
||||
import type { KanbanCard as KanbanCardType } from "$lib/supabase/types";
|
||||
import { Badge } from "$lib/components/ui";
|
||||
import { Avatar } from "$lib/components/ui";
|
||||
|
||||
// Extended card type with optional new fields from migration
|
||||
interface ExtendedCard extends KanbanCardType {
|
||||
priority?: "low" | "medium" | "high" | "urgent" | null;
|
||||
assignee_id?: string | null;
|
||||
interface Tag {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
card: ExtendedCard;
|
||||
card: KanbanCardType & {
|
||||
tags?: Tag[];
|
||||
checklist_done?: number;
|
||||
checklist_total?: number;
|
||||
assignee_name?: string | null;
|
||||
assignee_avatar?: string | null;
|
||||
};
|
||||
isDragging?: boolean;
|
||||
onclick?: () => void;
|
||||
ondelete?: (cardId: string) => void;
|
||||
draggable?: boolean;
|
||||
ondragstart?: (e: DragEvent) => void;
|
||||
}
|
||||
@@ -20,114 +27,125 @@
|
||||
card,
|
||||
isDragging = false,
|
||||
onclick,
|
||||
ondelete,
|
||||
draggable = true,
|
||||
ondragstart,
|
||||
}: Props = $props();
|
||||
|
||||
function handleDelete(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
if (confirm("Are you sure you want to delete this card?")) {
|
||||
ondelete?.(card.id);
|
||||
}
|
||||
}
|
||||
|
||||
function formatDueDate(dateStr: string | null): string {
|
||||
if (!dateStr) return "";
|
||||
const date = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diff = date.getTime() - now.getTime();
|
||||
const days = Math.ceil(diff / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (days < 0) return "Overdue";
|
||||
if (days === 0) return "Today";
|
||||
if (days === 1) return "Tomorrow";
|
||||
return date.toLocaleDateString();
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
function getDueDateVariant(
|
||||
dateStr: string | null,
|
||||
): "error" | "warning" | "default" {
|
||||
if (!dateStr) return "default";
|
||||
const date = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diff = date.getTime() - now.getTime();
|
||||
const days = Math.ceil(diff / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (days < 0) return "error";
|
||||
if (days <= 2) return "warning";
|
||||
return "default";
|
||||
}
|
||||
|
||||
function getPriorityColor(priority: string | null): string {
|
||||
switch (priority) {
|
||||
case "urgent":
|
||||
return "#E03D00";
|
||||
case "high":
|
||||
return "#FFAB00";
|
||||
case "medium":
|
||||
return "#00A3E0";
|
||||
case "low":
|
||||
return "#33E000";
|
||||
default:
|
||||
return "#E5E6F0";
|
||||
}
|
||||
}
|
||||
const hasFooter = $derived(
|
||||
!!card.due_date ||
|
||||
(card.checklist_total ?? 0) > 0 ||
|
||||
!!card.assignee_id,
|
||||
);
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="bg-night rounded-[16px] p-3 cursor-pointer hover:ring-1 hover:ring-primary/30 transition-all group"
|
||||
<button
|
||||
type="button"
|
||||
class="bg-night rounded-[16px] p-2 cursor-pointer hover:ring-1 hover:ring-primary/30 transition-all group w-full text-left overflow-clip flex flex-col gap-2 relative"
|
||||
class:opacity-50={isDragging}
|
||||
{draggable}
|
||||
{ondragstart}
|
||||
{onclick}
|
||||
onkeydown={(e) => e.key === "Enter" && onclick?.()}
|
||||
role="listitem"
|
||||
tabindex="0"
|
||||
>
|
||||
<!-- Priority indicator -->
|
||||
{#if card.priority}
|
||||
<div
|
||||
class="w-full h-1 rounded-full mb-2"
|
||||
style="background-color: {getPriorityColor(card.priority)}"
|
||||
></div>
|
||||
{:else if card.color}
|
||||
<div
|
||||
class="w-full h-1 rounded-full mb-2"
|
||||
style="background-color: {card.color}"
|
||||
></div>
|
||||
<!-- Delete button (top-right, visible on hover) -->
|
||||
{#if ondelete}
|
||||
<button
|
||||
type="button"
|
||||
class="absolute top-1 right-1 p-1 rounded-lg opacity-0 group-hover:opacity-100 hover:bg-error/20 transition-all z-10"
|
||||
onclick={handleDelete}
|
||||
aria-label="Delete card"
|
||||
>
|
||||
<span
|
||||
class="material-symbols-rounded text-light/40 hover:text-error"
|
||||
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
|
||||
>
|
||||
delete
|
||||
</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- Tags / Chips -->
|
||||
{#if card.tags && card.tags.length > 0}
|
||||
<div class="flex gap-[10px] items-start flex-wrap">
|
||||
{#each card.tags as tag}
|
||||
<span
|
||||
class="rounded-[4px] px-1 py-[4px] font-body font-bold text-[14px] text-night leading-none overflow-clip"
|
||||
style="background-color: {tag.color || '#00A3E0'}"
|
||||
>
|
||||
{tag.name}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Title -->
|
||||
<p class="text-sm font-medium text-light">{card.title}</p>
|
||||
<p class="font-body text-body text-white w-full leading-none">
|
||||
{card.title}
|
||||
</p>
|
||||
|
||||
<!-- Description -->
|
||||
{#if card.description}
|
||||
<p class="text-xs text-light/50 mt-1 line-clamp-2">
|
||||
{card.description}
|
||||
</p>
|
||||
{/if}
|
||||
<!-- Bottom row: details + avatar -->
|
||||
{#if hasFooter}
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div class="flex gap-1 items-center">
|
||||
<!-- Due date -->
|
||||
{#if card.due_date}
|
||||
<div class="flex items-center">
|
||||
<span
|
||||
class="material-symbols-rounded text-light p-1"
|
||||
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||
>
|
||||
calendar_today
|
||||
</span>
|
||||
<span
|
||||
class="font-body text-[12px] text-light leading-none"
|
||||
>
|
||||
{formatDueDate(card.due_date)}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Footer with metadata -->
|
||||
<div class="mt-3 flex items-center justify-between gap-2">
|
||||
<!-- Due date -->
|
||||
{#if card.due_date}
|
||||
<Badge size="sm" variant={getDueDateVariant(card.due_date)}>
|
||||
<svg
|
||||
class="w-3 h-3 mr-1"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" />
|
||||
<line x1="16" y1="2" x2="16" y2="6" />
|
||||
<line x1="8" y1="2" x2="8" y2="6" />
|
||||
<line x1="3" y1="10" x2="21" y2="10" />
|
||||
</svg>
|
||||
{formatDueDate(card.due_date)}
|
||||
</Badge>
|
||||
{/if}
|
||||
|
||||
<!-- Assignee placeholder -->
|
||||
{#if card.assignee_id}
|
||||
<div
|
||||
class="w-6 h-6 rounded-full bg-primary/20 flex items-center justify-center text-primary text-xs font-medium"
|
||||
>
|
||||
A
|
||||
<!-- Checklist -->
|
||||
{#if (card.checklist_total ?? 0) > 0}
|
||||
<div class="flex items-center">
|
||||
<span
|
||||
class="material-symbols-rounded text-light p-1"
|
||||
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||
>
|
||||
check_box
|
||||
</span>
|
||||
<span
|
||||
class="font-body text-[12px] text-light leading-none"
|
||||
>
|
||||
{card.checklist_done ?? 0}/{card.checklist_total}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Assignee avatar -->
|
||||
{#if card.assignee_id}
|
||||
<Avatar
|
||||
name={card.assignee_name || "?"}
|
||||
src={card.assignee_avatar}
|
||||
size="sm"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user