Mega push vol 3

This commit is contained in:
AlacrisDevs
2026-02-05 01:48:29 +02:00
parent 9af0ef5307
commit b517bb975c
6 changed files with 908 additions and 23 deletions

View File

@@ -13,6 +13,26 @@
position: number;
}
interface Comment {
id: string;
card_id: string;
user_id: string;
content: string;
created_at: string;
profiles?: { full_name: string | null; email: string };
}
interface Member {
id: string;
user_id: string;
profiles: {
id: string;
full_name: string | null;
email: string;
avatar_url: string | null;
};
}
interface Props {
card: KanbanCard | null;
isOpen: boolean;
@@ -23,6 +43,7 @@
columnId?: string;
userId?: string;
onCreate?: (card: KanbanCard) => void;
members?: Member[];
}
let {
@@ -35,6 +56,7 @@
columnId,
userId,
onCreate,
members = [],
}: Props = $props();
const supabase = getContext<SupabaseClient<Database>>("supabase");
@@ -42,20 +64,38 @@
let title = $state("");
let description = $state("");
let checklist = $state<ChecklistItem[]>([]);
let comments = $state<Comment[]>([]);
let newItemTitle = $state("");
let newComment = $state("");
let assigneeId = $state<string | null>(null);
let dueDate = $state<string>("");
let priority = $state<string>("medium");
let isLoading = $state(false);
let isSaving = $state(false);
let showAssigneePicker = $state(false);
$effect(() => {
if (isOpen) {
if (mode === "edit" && card) {
title = card.title;
description = card.description ?? "";
assigneeId = (card as any).assignee_id ?? null;
dueDate = (card as any).due_date
? new Date((card as any).due_date)
.toISOString()
.split("T")[0]
: "";
priority = (card as any).priority ?? "medium";
loadChecklist();
loadComments();
} else if (mode === "create") {
title = "";
description = "";
assigneeId = null;
dueDate = "";
priority = "medium";
checklist = [];
comments = [];
}
}
});
@@ -65,7 +105,7 @@
isLoading = true;
const { data } = await supabase
.from("checklist_items")
.from("kanban_checklist_items")
.select("*")
.eq("card_id", card.id)
.order("position");
@@ -74,6 +114,27 @@
isLoading = false;
}
async function loadComments() {
if (!card) return;
const { data } = await supabase
.from("kanban_comments")
.select(
`
id,
card_id,
user_id,
content,
created_at,
profiles:user_id (full_name, email)
`,
)
.eq("card_id", card.id)
.order("created_at", { ascending: true });
comments = (data ?? []) as Comment[];
}
async function handleSave() {
if (mode === "create") {
await handleCreate();
@@ -88,11 +149,21 @@
.update({
title,
description: description || null,
assignee_id: assigneeId,
due_date: dueDate || null,
priority,
})
.eq("id", card.id);
if (!error) {
onUpdate({ ...card, title, description: description || null });
onUpdate({
...card,
title,
description: description || null,
assignee_id: assigneeId,
due_date: dueDate || null,
priority,
} as KanbanCard);
}
isSaving = false;
}
@@ -133,7 +204,7 @@
const position = checklist.length;
const { data, error } = await supabase
.from("checklist_items")
.from("kanban_checklist_items")
.insert({
card_id: card.id,
title: newItemTitle,
@@ -149,9 +220,37 @@
}
}
async function handleAddComment() {
if (!card || !newComment.trim() || !userId) return;
const { data, error } = await supabase
.from("kanban_comments")
.insert({
card_id: card.id,
user_id: userId,
content: newComment,
})
.select(
`
id,
card_id,
user_id,
content,
created_at,
profiles:user_id (full_name, email)
`,
)
.single();
if (!error && data) {
comments = [...comments, data as Comment];
newComment = "";
}
}
async function toggleItem(item: ChecklistItem) {
const { error } = await supabase
.from("checklist_items")
.from("kanban_checklist_items")
.update({ completed: !item.completed })
.eq("id", item.id);
@@ -164,7 +263,7 @@
async function deleteItem(itemId: string) {
const { error } = await supabase
.from("checklist_items")
.from("kanban_checklist_items")
.delete()
.eq("id", itemId);
@@ -173,6 +272,20 @@
}
}
function formatDate(dateStr: string): string {
return new Date(dateStr).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}
function getAssignee(id: string | null) {
if (!id) return null;
return members.find((m) => m.user_id === id);
}
async function handleDelete() {
if (!card || !confirm("Delete this card?")) return;
@@ -207,6 +320,129 @@
rows={3}
/>
<!-- Assignee, Due Date, Priority Row -->
<div class="grid grid-cols-3 gap-4">
<!-- Assignee -->
<div class="relative">
<label class="block text-sm font-medium text-light mb-1"
>Assignee</label
>
<button
type="button"
class="w-full px-3 py-2 bg-dark border border-light/20 rounded-lg text-left text-sm flex items-center gap-2 hover:border-light/40 transition-colors"
onclick={() =>
(showAssigneePicker = !showAssigneePicker)}
>
{#if assigneeId && getAssignee(assigneeId)}
{@const assignee = getAssignee(assigneeId)}
<div
class="w-6 h-6 rounded-full bg-primary/20 flex items-center justify-center text-xs text-primary"
>
{(assignee?.profiles.full_name ||
assignee?.profiles.email ||
"?")[0].toUpperCase()}
</div>
<span class="text-light truncate"
>{assignee?.profiles.full_name ||
assignee?.profiles.email}</span
>
{:else}
<div
class="w-6 h-6 rounded-full bg-light/10 flex items-center justify-center"
>
<svg
class="w-3 h-3 text-light/40"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"
/>
<circle cx="12" cy="7" r="4" />
</svg>
</div>
<span class="text-light/40">Unassigned</span>
{/if}
</button>
{#if showAssigneePicker}
<div
class="absolute top-full left-0 right-0 mt-1 bg-dark border border-light/20 rounded-lg shadow-lg z-10 max-h-48 overflow-y-auto"
>
<button
class="w-full px-3 py-2 text-left text-sm text-light/60 hover:bg-light/5 flex items-center gap-2"
onclick={() => {
assigneeId = null;
showAssigneePicker = false;
}}
>
<div
class="w-6 h-6 rounded-full bg-light/10"
></div>
Unassigned
</button>
{#each members as member}
<button
class="w-full px-3 py-2 text-left text-sm hover:bg-light/5 flex items-center gap-2 {assigneeId ===
member.user_id
? 'bg-primary/10 text-primary'
: 'text-light'}"
onclick={() => {
assigneeId = member.user_id;
showAssigneePicker = false;
}}
>
<div
class="w-6 h-6 rounded-full bg-primary/20 flex items-center justify-center text-xs"
>
{(member.profiles.full_name ||
member.profiles.email ||
"?")[0].toUpperCase()}
</div>
{member.profiles.full_name ||
member.profiles.email}
</button>
{/each}
</div>
{/if}
</div>
<!-- Due Date -->
<div>
<label
for="due-date"
class="block text-sm font-medium text-light mb-1"
>Due Date</label
>
<input
id="due-date"
type="date"
bind:value={dueDate}
class="w-full px-3 py-2 bg-dark border border-light/20 rounded-lg text-sm text-light focus:outline-none focus:border-primary"
/>
</div>
<!-- Priority -->
<div>
<label
for="priority"
class="block text-sm font-medium text-light mb-1"
>Priority</label
>
<select
id="priority"
bind:value={priority}
class="w-full px-3 py-2 bg-dark border border-light/20 rounded-lg text-sm text-light focus:outline-none focus:border-primary"
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="urgent">Urgent</option>
</select>
</div>
</div>
<div>
<div class="flex items-center justify-between mb-3">
<label class="text-sm font-medium text-light"
@@ -302,6 +538,71 @@
{/if}
</div>
<!-- Comments Section -->
{#if mode === "edit"}
<div>
<label class="block text-sm font-medium text-light mb-3"
>Comments</label
>
<div class="space-y-3 mb-3 max-h-48 overflow-y-auto">
{#each comments as comment}
<div class="flex gap-3">
<div
class="w-8 h-8 rounded-full bg-primary/20 flex-shrink-0 flex items-center justify-center text-xs text-primary"
>
{((comment.profiles as any)?.full_name ||
(comment.profiles as any)?.email ||
"?")[0].toUpperCase()}
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<span
class="text-sm font-medium text-light"
>
{(comment.profiles as any)
?.full_name ||
(comment.profiles as any)
?.email ||
"Unknown"}
</span>
<span class="text-xs text-light/40"
>{formatDate(
comment.created_at,
)}</span
>
</div>
<p class="text-sm text-light/70 mt-0.5">
{comment.content}
</p>
</div>
</div>
{/each}
{#if comments.length === 0}
<p class="text-sm text-light/40 text-center py-2">
No comments yet
</p>
{/if}
</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 a comment..."
bind:value={newComment}
onkeydown={(e) =>
e.key === "Enter" && handleAddComment()}
/>
<Button
size="sm"
onclick={handleAddComment}
disabled={!newComment.trim()}
>
Send
</Button>
</div>
</div>
{/if}
<div
class="flex items-center justify-between pt-3 border-t border-light/10"
>