commit pre-existing group settings content edits
This commit is contained in:
parent
828bb301d6
commit
1b7e4b94b5
@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user