2c15d409bc
Co-authored-by: Kandev Agent <agent@kandev.dev>
91 lines
2.8 KiB
TypeScript
91 lines
2.8 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { IconExternalLink, IconTrash } from "@tabler/icons-react";
|
|
|
|
import { Button } from "@kandev/ui/button";
|
|
|
|
import { revokeShare, type Share } from "@/lib/api/domains/share-api";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
type Props = {
|
|
shares: Share[];
|
|
onRevoked: () => void;
|
|
};
|
|
|
|
/**
|
|
* Renders the list of active (non-revoked) shares with a per-row revoke
|
|
* button. Revoke is optimistic: the API call fires; on success we call
|
|
* onRevoked() so the parent can refresh from the source of truth.
|
|
*/
|
|
export function ShareList({ shares, onRevoked }: Props) {
|
|
const { t } = useTranslation();
|
|
const active = shares.filter((s) => !s.revoked_at);
|
|
if (active.length === 0) return null;
|
|
|
|
return (
|
|
<div className="flex flex-col gap-2 rounded border bg-muted/30 p-3">
|
|
<div className="text-xs font-medium text-muted-foreground">
|
|
{t("task:activeSharesForThisSession")}
|
|
</div>
|
|
<ul className="flex flex-col gap-1.5">
|
|
{active.map((share) => (
|
|
<ShareRow key={share.id} share={share} onRevoked={onRevoked} />
|
|
))}
|
|
</ul>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ShareRow({ share, onRevoked }: { share: Share; onRevoked: () => void }) {
|
|
const { t } = useTranslation();
|
|
const [busy, setBusy] = useState(false);
|
|
const [err, setErr] = useState<string | null>(null);
|
|
|
|
const handleRevoke = async () => {
|
|
setBusy(true);
|
|
setErr(null);
|
|
try {
|
|
await revokeShare(share.id);
|
|
onRevoked();
|
|
} catch (e) {
|
|
setErr(e instanceof Error ? e.message : t("task:failedToRevokeShare"));
|
|
} finally {
|
|
// Always clear busy — on success the row usually unmounts via the
|
|
// parent's refresh, but if it stays rendered (e.g. error path or a
|
|
// not-yet-reflected list) we don't want it stuck in "Revoking…".
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<li className="flex min-w-0 flex-col gap-1">
|
|
<div className="flex min-w-0 items-center justify-between gap-2 text-sm">
|
|
<a
|
|
href={share.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex min-w-0 flex-1 items-center gap-1 text-primary hover:underline cursor-pointer"
|
|
>
|
|
<IconExternalLink className="h-3 w-3 flex-shrink-0" />
|
|
<span className="min-w-0 flex-1 truncate" title={share.url}>
|
|
{share.url}
|
|
</span>
|
|
</a>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={handleRevoke}
|
|
disabled={busy}
|
|
className="flex-shrink-0 cursor-pointer"
|
|
aria-label={t("task:revokeShare")}
|
|
>
|
|
<IconTrash className="h-3 w-3" />
|
|
{busy ? t("task:revoking") : t("task:revoke")}
|
|
</Button>
|
|
</div>
|
|
{err && <span className="text-xs text-destructive">{err}</span>}
|
|
</li>
|
|
);
|
|
}
|