feat(portal): add SSR operations and exclusive groups
This commit is contained in:
@@ -3,15 +3,17 @@ import { asc, eq } from "drizzle-orm";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { db } from "@/lib/database";
|
||||
import { addGroupMember, removeGroupMember, setGroupAccess } from "../actions";
|
||||
import { addGroupMember, assignDefaultGroup, deleteGroup, removeGroupMember, setGroupAccess } from "../actions";
|
||||
|
||||
const savedMessages: Record<string, string> = {
|
||||
created: "Group created with access disabled.",
|
||||
access: "Group access policy updated.",
|
||||
"member-added": "User added to the group.",
|
||||
"member-removed": "User removed from the group.",
|
||||
"member-added": "User assigned to the group.",
|
||||
"member-removed": "User returned to the default group.",
|
||||
};
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function GroupPage({
|
||||
params,
|
||||
searchParams,
|
||||
@@ -32,10 +34,16 @@ export default async function GroupPage({
|
||||
discordGlobalName: users.discordGlobalName,
|
||||
discordUserId: users.discordUserId,
|
||||
}).from(users).orderBy(asc(users.discordUsername)),
|
||||
db.select({ userId: userGroupMemberships.userId }).from(userGroupMemberships)
|
||||
.where(eq(userGroupMemberships.groupId, group.id)),
|
||||
db.select({
|
||||
userId: userGroupMemberships.userId,
|
||||
groupId: userGroupMemberships.groupId,
|
||||
groupName: groups.name,
|
||||
}).from(userGroupMemberships).innerJoin(groups, eq(groups.id, userGroupMemberships.groupId)),
|
||||
]);
|
||||
const memberIds = new Set(memberships.map((membership) => membership.userId));
|
||||
const assignmentByUser = new Map(memberships.map((membership) => [membership.userId, membership]));
|
||||
const memberCount = group.isDefault
|
||||
? allUsers.length - assignmentByUser.size
|
||||
: memberships.filter((membership) => membership.groupId === group.id).length;
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-6xl px-6 py-12">
|
||||
@@ -55,30 +63,38 @@ export default async function GroupPage({
|
||||
</form>
|
||||
</header>
|
||||
|
||||
{query.saved && <p className="mt-7 border-l-2 border-signal bg-panel px-5 py-4 font-mono text-xs font-bold uppercase tracking-wider">{savedMessages[query.saved] ?? "Group updated."}</p>}
|
||||
{query.saved && <p className="mt-7 border-l-2 border-signal bg-panel px-5 py-4 font-mono text-xs font-bold uppercase tracking-wider" role="status">{savedMessages[query.saved] ?? "Group updated."}</p>}
|
||||
|
||||
<section className="mt-10">
|
||||
<div className="flex items-end justify-between border-b border-line pb-4">
|
||||
<div><p className="font-mono text-[9px] font-bold uppercase tracking-widest text-muted">Membership</p><h2 className="mt-2 font-display text-3xl font-black uppercase">Registered users</h2></div>
|
||||
<span className="font-mono text-xs text-muted">{group.isDefault ? allUsers.length : memberIds.size} members</span>
|
||||
<span className="font-mono text-xs text-muted">{memberCount} members</span>
|
||||
</div>
|
||||
{group.isDefault && <p className="border-b border-line bg-panel px-5 py-4 text-sm text-muted">Membership in <strong className="text-ink">everyone</strong> is automatic and cannot be removed.</p>}
|
||||
{group.isDefault && <p className="border-b border-line bg-panel px-5 py-4 text-sm text-muted">Users belong to <strong className="text-ink">everyone</strong> only while they have no explicit group assignment.</p>}
|
||||
<div className="divide-y divide-line">
|
||||
{allUsers.map((user) => {
|
||||
const isMember = group.isDefault || memberIds.has(user.id);
|
||||
const assignment = assignmentByUser.get(user.id);
|
||||
const isMember = group.isDefault ? !assignment : assignment?.groupId === group.id;
|
||||
return (
|
||||
<article className="grid gap-4 py-5 sm:grid-cols-[1fr_auto] sm:items-center" key={user.id}>
|
||||
<div>
|
||||
<Link className="font-mono text-sm font-bold underline decoration-line underline-offset-4 hover:decoration-accent" href={`/admin/users/${user.id}`}>{user.firstName ?? user.discordGlobalName ?? user.discordUsername}</Link>
|
||||
<p className="mt-1 font-mono text-[10px] text-muted">@{user.discordUsername} · {user.discordUserId}</p>
|
||||
{!isMember && assignment && <p className="mt-1 text-xs text-muted">Currently assigned to {assignment.groupName}</p>}
|
||||
</div>
|
||||
{group.isDefault ? (
|
||||
<span className="font-mono text-[9px] font-bold uppercase text-muted">Automatic member</span>
|
||||
{isMember ? (
|
||||
group.isDefault ? <span className="font-mono text-[9px] font-bold uppercase text-muted">Default assignment</span> : (
|
||||
<form action={removeGroupMember}>
|
||||
<input name="groupId" type="hidden" value={group.id} />
|
||||
<input name="userId" type="hidden" value={user.id} />
|
||||
<button className="font-mono text-[9px] font-bold uppercase text-accent underline underline-offset-4" type="submit">Return to everyone</button>
|
||||
</form>
|
||||
)
|
||||
) : (
|
||||
<form action={isMember ? removeGroupMember : addGroupMember}>
|
||||
<form action={group.isDefault ? assignDefaultGroup : addGroupMember}>
|
||||
<input name="groupId" type="hidden" value={group.id} />
|
||||
<input name="userId" type="hidden" value={user.id} />
|
||||
<button className={`font-mono text-[9px] font-bold uppercase underline underline-offset-4 ${isMember ? "text-accent" : "text-ink"}`} type="submit">{isMember ? "Remove from group" : "Add to group"}</button>
|
||||
<button className="font-mono text-[9px] font-bold uppercase text-ink underline underline-offset-4" type="submit">Move to {group.name}</button>
|
||||
</form>
|
||||
)}
|
||||
</article>
|
||||
@@ -87,6 +103,23 @@ export default async function GroupPage({
|
||||
{!allUsers.length && <p className="py-8 text-sm text-muted">No registered users yet.</p>}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{!group.isDefault && (
|
||||
<section className="mt-12 border border-accent bg-panel p-6">
|
||||
<p className="font-mono text-[10px] font-bold uppercase tracking-widest text-accent">Danger zone</p>
|
||||
<h2 className="mt-3 font-display text-2xl font-black uppercase">Delete {group.name}</h2>
|
||||
<p className="mt-3 max-w-2xl text-sm leading-6 text-muted">Deleting this group returns its {memberCount} {memberCount === 1 ? "member" : "members"} to the protected default group. This cannot be undone.</p>
|
||||
<details className="mt-5">
|
||||
<summary className="w-fit cursor-pointer font-mono text-[10px] font-bold uppercase text-accent underline underline-offset-4">Review deletion</summary>
|
||||
<form action={deleteGroup} className="mt-4 flex flex-wrap items-center gap-4">
|
||||
<input name="groupId" type="hidden" value={group.id} />
|
||||
<input name="confirmDelete" type="hidden" value="yes" />
|
||||
<button className="bg-accent px-5 py-3 font-mono text-[10px] font-bold uppercase tracking-wider text-canvas" type="submit">Delete group permanently</button>
|
||||
<span className="text-xs text-muted">Members will use everyone immediately.</span>
|
||||
</form>
|
||||
</details>
|
||||
</section>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user