commit pre-existing group settings content edits

This commit is contained in:
Nico 2026-02-14 00:52:11 -08:00
parent 828bb301d6
commit 1b7e4b94b5

View File

@ -2,16 +2,18 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import useTags from "@/hooks/use-tags";
import useGroupSettings from "@/hooks/use-group-settings";
import useGroupMembers from "@/hooks/use-group-members";
import useGroupInvites from "@/hooks/use-group-invites";
import useGroupAudit from "@/hooks/use-group-audit";
import useTags from "@/features/tags/hooks/use-tags";
import useGroupSettings from "@/features/groups/hooks/use-group-settings";
import useGroupMembers from "@/features/groups/hooks/use-group-members";
import useGroupInvites from "@/features/groups/hooks/use-group-invites";
import useGroupAudit from "@/features/groups/hooks/use-group-audit";
import { useGroupsContext } from "@/hooks/groups-context";
import TagInput from "@/components/tag-input";
import { useNotificationsContext } from "@/hooks/notifications-context";
import ConfirmSlideModal from "@/components/confirm-slide-modal";
import ConfirmRetypeModal from "@/components/confirm-retype-modal";
import { groupsDelete, groupsRename } from "@/lib/client/groups";
import ToggleButtonGroup from "@/components/toggle-button-group";
export default function GroupSettingsContent({ groupId }: { groupId: number }) {
const router = useRouter();
@ -267,6 +269,24 @@ export default function GroupSettingsContent({ groupId }: { groupId: number }) {
return { userId: top, count: topCount, name, searchValue };
})();
const mostActiveCount = mostActiveUser?.count ?? 0;
const renameDirty = Boolean(group && renameValue.trim() !== group.name);
const memberCount = members.length;
const showLeaveGroup = role !== "GROUP_OWNER" && !isLastAdmin;
useEffect(() => {
if (!renameModalOpen && !tagModalOpen) return;
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") {
if (tagModalOpen) setTagModalOpen(false);
if (renameModalOpen) handleCloseRenameModal();
}
if (event.key === "Enter" && !event.shiftKey && !event.defaultPrevented) {
if (renameModalOpen && renameDirty && isAdmin && renameValue.trim()) setConfirmRenameOpen(true);
}
}
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [renameModalOpen, tagModalOpen, renameDirty, isAdmin, renameValue, handleCloseRenameModal]);
async function handleDeleteGroup() {
const result = await groupsDelete();
@ -288,30 +308,9 @@ export default function GroupSettingsContent({ groupId }: { groupId: number }) {
);
}
const renameDirty = renameValue.trim() !== group.name;
const visibleTags = showAllTags ? tags : tags.slice(0, 5);
const hasMoreTags = tags.length > 5;
const tagsScrollable = showAllTags && tags.length > 15;
const memberCount = members.length;
const showLeaveGroup = role !== "GROUP_OWNER" && !isLastAdmin;
useEffect(() => {
if (!renameModalOpen && !tagModalOpen && !confirmDeleteGroupOpen) return;
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") {
if (confirmDeleteGroupOpen) setConfirmDeleteGroupOpen(false);
if (tagModalOpen) setTagModalOpen(false);
if (renameModalOpen) handleCloseRenameModal();
}
if (event.key === "Enter" && !event.shiftKey && !event.defaultPrevented) {
if (renameModalOpen && renameDirty && isAdmin && renameValue.trim()) setConfirmRenameOpen(true);
// if (tagModalOpen && pendingTags.length && canManageTags) handleSaveTags();
if (confirmDeleteGroupOpen && deleteConfirmText.trim().toUpperCase() === "DELETE") handleDeleteGroup();
}
}
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [renameModalOpen, tagModalOpen, confirmDeleteGroupOpen, renameDirty, isAdmin, renameValue, pendingTags.length, canManageTags, deleteConfirmText, handleDeleteGroup, handleSaveTags, handleCloseRenameModal]);
return (
<>
@ -425,23 +424,21 @@ export default function GroupSettingsContent({ groupId }: { groupId: number }) {
<div className="space-y-3">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="text-sm font-semibold">Join policy</div>
<div className="flex flex-wrap gap-2">
{[
<ToggleButtonGroup
value={localJoinPolicy}
onChange={policy => handleJoinPolicyChange(policy)}
ariaLabel="Join policy"
className="flex flex-wrap gap-2"
buttonBaseClassName="rounded-lg border"
sizeClassName="px-3 py-1.5 text-xs font-semibold transition"
activeClassName="border-accent bg-accent-soft"
inactiveClassName="border-accent-weak bg-panel hover:border-accent"
options={[
{ value: "NOT_ACCEPTING", label: "Disabled" },
{ value: "AUTO_ACCEPT", label: "Auto" },
{ value: "APPROVAL_REQUIRED", label: "Manual" }
].map(option => (
<button
key={option.value}
type="button"
className={`rounded-lg border px-3 py-1.5 text-xs font-semibold transition ${localJoinPolicy === option.value ? "border-accent bg-accent-soft" : "border-accent-weak bg-panel hover:border-accent"}`}
onClick={() => handleJoinPolicyChange(option.value as "NOT_ACCEPTING" | "AUTO_ACCEPT" | "APPROVAL_REQUIRED")}
aria-pressed={localJoinPolicy === option.value}
>
{option.label}
</button>
))}
</div>
]}
/>
</div>
</div>
<div className="divider" />
@ -518,42 +515,55 @@ export default function GroupSettingsContent({ groupId }: { groupId: number }) {
className={`rounded-lg border px-3 py-2 text-sm ${link.revokedAt || isInviteExpired(link.expiresAt) || (link.singleUse && link.usedAt) ? "border-red-400/60 bg-red-500/5" : "border-accent-weak bg-panel"}`}
>
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="text-xs text-soft">Token: {link.token.slice(0, 6)}{link.token.slice(-4)}</div>
<div className="flex items-center gap-2">
<button
type="button"
className="rounded-lg btn-outline-accent px-2 py-1 text-xs"
onClick={() => handleCopyInvite(link.token)}
disabled={localJoinPolicy === "NOT_ACCEPTING"}
>
Copy link
</button>
{(link.revokedAt || isInviteExpired(link.expiresAt) || (link.singleUse && link.usedAt)) ? (
<button
type="button"
className="rounded-lg btn-outline-accent px-2 py-1 text-xs"
onClick={() => reviveInvite(link.id, inviteTtlDays)}
disabled={localJoinPolicy === "NOT_ACCEPTING"}
>
Revive
</button>
) : (!link.revokedAt && !(link.singleUse && link.usedAt) && !isInviteExpired(link.expiresAt)) ? (
<button
type="button"
className="rounded-lg border border-red-400/60 bg-red-500/10 px-2 py-1 text-xs text-red-200"
onClick={() => revokeInvite(link.id)}
>
Revoke
</button>
) : null}
<button
type="button"
className="rounded-lg border border-red-400/60 bg-red-500/10 px-2 py-1 text-xs text-red-200"
onClick={() => setConfirmDeleteInvite({ id: link.id, token: link.token })}
>
Delete
</button>
</div>
<div className="text-xs text-soft">Token: {link.token.slice(0, 6)}...{link.token.slice(-4)}</div>
{(() => {
const showRevive = link.revokedAt || isInviteExpired(link.expiresAt) || (link.singleUse && link.usedAt);
const showRevoke = !showRevive && !link.revokedAt && !(link.singleUse && link.usedAt) && !isInviteExpired(link.expiresAt);
const options = [
{
value: "COPY",
label: "Copy link",
className: "btn-outline-accent",
disabled: localJoinPolicy === "NOT_ACCEPTING",
onClick: () => handleCopyInvite(link.token)
},
...(showRevive
? [{
value: "REVIVE",
label: "Revive",
className: "btn-outline-accent",
disabled: localJoinPolicy === "NOT_ACCEPTING",
onClick: () => reviveInvite(link.id, inviteTtlDays)
}]
: showRevoke
? [{
value: "REVOKE",
label: "Revoke",
className: "border border-red-400/60 bg-red-500/10 text-red-200",
onClick: () => revokeInvite(link.id)
}]
: []),
{
value: "DELETE",
label: "Delete",
className: "border border-red-400/60 bg-red-500/10 text-red-200",
onClick: () => setConfirmDeleteInvite({ id: link.id, token: link.token })
}
];
return (
<ToggleButtonGroup
value={null}
ariaLabel="Invite actions"
className="flex items-center gap-2"
buttonBaseClassName="rounded-lg"
sizeClassName="px-2 py-1 text-xs"
activeClassName=""
inactiveClassName=""
options={options}
/>
);
})()}
</div>
<div className="mt-2 flex flex-wrap gap-3 text-[11px] text-soft">
<span>Expires {formatInviteExpiry(link.expiresAt)}</span>
@ -577,10 +587,10 @@ export default function GroupSettingsContent({ groupId }: { groupId: number }) {
{members.map(member => {
const isSelf = member.userId === currentUserId;
const privilegeLabel = member.role === "GROUP_OWNER"
? "👑 Owner · Full control"
? "Owner - Full control"
: member.role === "GROUP_ADMIN"
? "🛡️ Admin · Manage members"
: "👤 Member · Entries only";
? "Admin - Manage members"
: "Member - Entries only";
return (
<div
key={member.userId}
@ -806,7 +816,7 @@ export default function GroupSettingsContent({ groupId }: { groupId: number }) {
onClick={handleCloseRenameModal}
aria-label="Close"
>
x
</button>
<div className="text-lg font-semibold">Change group name</div>
<input
@ -867,9 +877,7 @@ export default function GroupSettingsContent({ groupId }: { groupId: number }) {
>
<div className="flex items-center justify-between">
<div className="text-lg font-semibold">Edit tags</div>
<button type="button" className="rounded-lg btn-outline-accent px-2 py-1 text-sm" onClick={() => setTagModalOpen(false)} aria-label="Close">
</button>
<button type="button" className="rounded-lg btn-outline-accent px-2 py-1 text-sm" onClick={() => setTagModalOpen(false)} aria-label="Close">x</button>
</div>
<div className="mt-4 space-y-3">
<TagInput
@ -938,7 +946,7 @@ export default function GroupSettingsContent({ groupId }: { groupId: number }) {
<ConfirmSlideModal
isOpen={Boolean(confirmDeleteInvite)}
title="Delete invite link"
description={confirmDeleteInvite ? `Delete invite ${confirmDeleteInvite.token.slice(0, 6)}${confirmDeleteInvite.token.slice(-4)}?` : ""}
description={confirmDeleteInvite ? `Delete invite ${confirmDeleteInvite.token.slice(0, 6)}...${confirmDeleteInvite.token.slice(-4)}?` : ""}
confirmLabel="Delete link"
onClose={() => setConfirmDeleteInvite(null)}
onConfirm={async () => {
@ -1000,46 +1008,17 @@ export default function GroupSettingsContent({ groupId }: { groupId: number }) {
if (ok) notify({ title: "Ownership transferred", message: target.name });
}}
/>
{confirmDeleteGroupOpen ? (
<div className="fixed inset-0 z-[70] flex items-center justify-center bg-black/60 p-4 !mt-0" onClick={() => setConfirmDeleteGroupOpen(false)}>
<div
className="w-full max-w-sm rounded-2xl border border-accent-weak bg-panel p-5"
onClick={event => event.stopPropagation()}
onKeyDown={event => {
if (event.key === "Escape") setConfirmDeleteGroupOpen(false);
if (event.key === "Enter" && deleteConfirmText.trim().toUpperCase() === "DELETE") handleDeleteGroup();
}}
role="dialog"
tabIndex={-1}
>
<div className="text-lg font-semibold text-red-200">Delete group</div>
<p className="mt-2 text-sm text-muted">Type DELETE to confirm. This cannot be undone.</p>
<input
className={`mt-4 w-full input-base px-3 py-2 text-sm ${deleteConfirmText.trim().toUpperCase() === "DELETE" ? "" : "border-red-400/70"}`}
<ConfirmRetypeModal
isOpen={confirmDeleteGroupOpen}
title="Delete group"
description="Type DELETE to confirm. This cannot be undone."
expectedText="DELETE"
value={deleteConfirmText}
onChange={e => setDeleteConfirmText(e.target.value)}
placeholder="DELETE"
onChange={setDeleteConfirmText}
confirmLabel="Delete"
onClose={() => setConfirmDeleteGroupOpen(false)}
onConfirm={handleDeleteGroup}
/>
<div className="mt-4 flex items-center gap-2">
<button
type="button"
className="flex-1 rounded-lg btn-outline-accent px-3 py-2 text-sm font-semibold"
onClick={() => setConfirmDeleteGroupOpen(false)}
>
Cancel
</button>
<button
type="button"
className="flex-1 rounded-lg border border-red-400/60 bg-red-500/10 px-3 py-2 text-sm font-semibold text-red-200 disabled:opacity-40"
disabled={deleteConfirmText.trim().toUpperCase() !== "DELETE"}
onClick={handleDeleteGroup}
>
Delete
</button>
</div>
</div>
</div>
) : null}
</div>
</>
);