feat: bang edit and delete
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { useState, useTransition, useRef, useCallback } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import confetti from "canvas-confetti";
|
||||
import { Pencil, Trash2 } from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -11,8 +12,9 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import type { BangStatsDto } from "../server/queries";
|
||||
import { addBang } from "../server/actions";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { BangStatsDto, RecentBangDto } from "../server/queries";
|
||||
import { addBang, deleteBang, updateBang } from "../server/actions";
|
||||
|
||||
// ─── Celebration engine ───────────────────────────────────────────────────────
|
||||
|
||||
@@ -301,14 +303,7 @@ export function BangWidget({ stats, maxRecentBangs }: Props) {
|
||||
Last {Math.min(stats.recent.length, maxRecentBangs)}
|
||||
</p>
|
||||
{stats.recent.map((bang) => (
|
||||
<div key={bang.id} className="flex items-center justify-between gap-2 py-0.5">
|
||||
<span className="text-sm">{formatBangDate(bang.occurredOn)}</span>
|
||||
{bang.recordedByName && (
|
||||
<span className="max-w-[40%] truncate text-xs text-[var(--ink-mute)]">
|
||||
{bang.recordedByName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<RecentBangRow key={bang.id} bang={bang} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -320,6 +315,142 @@ export function BangWidget({ stats, maxRecentBangs }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
function RecentBangRow({ bang }: { bang: RecentBangDto }) {
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const [dateValue, setDateValue] = useState(bang.occurredOn);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
function handleEditOpenChange(open: boolean) {
|
||||
setEditOpen(open);
|
||||
if (open) {
|
||||
setDateValue(bang.occurredOn);
|
||||
setError(null);
|
||||
}
|
||||
}
|
||||
|
||||
function handleEditSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await updateBang({ id: bang.id, occurredOn: dateValue });
|
||||
setEditOpen(false);
|
||||
} catch {
|
||||
setError("Something went wrong. Try again.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
setError(null);
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await deleteBang({ id: bang.id });
|
||||
setDeleteOpen(false);
|
||||
} catch {
|
||||
setError("Something went wrong. Try again.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group/row flex items-center justify-between gap-2 py-0.5">
|
||||
<span className="text-sm">{formatBangDate(bang.occurredOn)}</span>
|
||||
<div className="flex min-w-0 items-center gap-1">
|
||||
{bang.recordedByName && (
|
||||
<span className="max-w-[5rem] truncate text-xs text-[var(--ink-mute)] sm:max-w-[6rem]">
|
||||
{bang.recordedByName}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
aria-label={`Edit bang on ${bang.occurredOn}`}
|
||||
onClick={() => setEditOpen(true)}
|
||||
>
|
||||
<Pencil className="size-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
aria-label={`Delete bang on ${bang.occurredOn}`}
|
||||
onClick={() => setDeleteOpen(true)}
|
||||
>
|
||||
<Trash2 className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Dialog open={editOpen} onOpenChange={handleEditOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit bang</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form
|
||||
id={`edit-bang-${bang.id}`}
|
||||
onSubmit={handleEditSubmit}
|
||||
className="flex flex-col gap-3 pt-1"
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor={`edit-bang-date-${bang.id}`} className="text-sm font-medium">
|
||||
Date
|
||||
</label>
|
||||
<input
|
||||
id={`edit-bang-date-${bang.id}`}
|
||||
type="date"
|
||||
value={dateValue}
|
||||
onChange={(e) => setDateValue(e.target.value)}
|
||||
className="input input-sm"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{error && editOpen && <p className="text-sm text-red-500">{error}</p>}
|
||||
</form>
|
||||
<DialogFooter showCloseButton>
|
||||
<button
|
||||
type="submit"
|
||||
form={`edit-bang-${bang.id}`}
|
||||
disabled={isPending}
|
||||
className="btn btn-primary btn-sm"
|
||||
>
|
||||
{isPending ? "Saving…" : "Save"}
|
||||
</button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
open={deleteOpen}
|
||||
onOpenChange={(open) => {
|
||||
setDeleteOpen(open);
|
||||
if (!open) setError(null);
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete bang?</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p className="text-sm text-[var(--ink-mute)]">
|
||||
Remove the bang from {formatBangDate(bang.occurredOn)}? This cannot be undone.
|
||||
</p>
|
||||
{error && deleteOpen && <p className="text-sm text-red-500">{error}</p>}
|
||||
<DialogFooter showCloseButton>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isPending}
|
||||
onClick={handleDelete}
|
||||
className="btn btn-sm bg-red-600 text-white hover:bg-red-700"
|
||||
>
|
||||
{isPending ? "Deleting…" : "Delete"}
|
||||
</button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function todayValue(): string {
|
||||
const d = new Date();
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||||
|
||||
Reference in New Issue
Block a user