Refine workspaces and event publishing; harden uploads and email delivery

This commit is contained in:
2026-09-09 15:44:00 -04:00
parent f5702caaea
commit 574f29a68e
93 changed files with 2885 additions and 535 deletions
+10 -33
View File
@@ -1,39 +1,16 @@
import { redirect } from "next/navigation";
import { headers } from "next/headers";
import Link from "next/link";
import { auth } from "@/server/auth";
import { getPlatformRole } from "@/server/roles";
import { Button } from "@/components/ui/button";
import { createServerCaller } from "@/trpc/server";
import { BackendShell } from "@/components/backend-shell";
import { DashboardTabBar } from "@/components/dashboard-tab-bar";
export default async function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) redirect("/sign-in?callbackURL=/admin");
const role = await getPlatformRole(session.user.id);
if (!role) redirect("/dashboard");
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
const caller = await createServerCaller();
const viewer = await caller.viewer.me();
if (!viewer.session) redirect("/sign-in?callbackURL=/admin");
if (!viewer.platformRole) redirect("/dashboard");
return (
<>
<main className="page-pad mx-auto w-full max-w-6xl py-6 pb-24 sm:py-10 sm:pb-10">
<div className="mb-6 hidden items-center gap-1 sm:flex">
<Button asChild variant="ghost" size="sm">
<Link href="/admin">Overview</Link>
</Button>
<Button asChild variant="ghost" size="sm">
<Link href="/admin/settings">Settings</Link>
</Button>
<Button asChild variant="ghost" size="sm">
<Link href="/dashboard">Dashboard</Link>
</Button>
</div>
<div className="reveal">{children}</div>
</main>
<DashboardTabBar showAdmin area="admin" />
</>
<BackendShell area="platform" groups={viewer.groups} activeGroupId={viewer.activeGroupId} showAdmin>
<div className="reveal">{children}</div>
</BackendShell>
);
}
+5 -3
View File
@@ -1,5 +1,7 @@
"use client";
import { NativeSelect } from "@/components/ui/native-select";
import { useState } from "react";
import { toast } from "sonner";
import { api } from "@/trpc/react";
@@ -78,8 +80,8 @@ export function PlatformCodes({
) : null}
{groups.length > 0 ? (
<div className="flex flex-wrap items-center gap-2">
<select
className="rounded-md border bg-background px-2 py-1.5 text-sm"
<NativeSelect
value={groupId}
onChange={(event) => setGroupId(event.target.value)}
>
@@ -88,7 +90,7 @@ export function PlatformCodes({
{group.name}
</option>
))}
</select>
</NativeSelect>
<Button
variant="secondary"
disabled={!groupId}
+13 -9
View File
@@ -1,5 +1,7 @@
"use client";
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useState } from "react";
import { toast } from "sonner";
import type { PlatformRole } from "@album/contracts";
@@ -55,25 +57,27 @@ export function PlatformUsers() {
</div>
<div className="flex items-center gap-2">
{row.platformRole ? (
<Badge variant="secondary">{row.platformRole}</Badge>
<Badge variant="secondary" className="capitalize">{row.platformRole.replaceAll("_", " ")}</Badge>
) : null}
<select
className="rounded-md border bg-background px-2 py-1.5 text-sm"
<Select
value={row.platformRole ?? "none"}
onChange={(event) => {
const value = event.target.value as PlatformRole | "none";
onValueChange={(selected) => {
const value = selected as PlatformRole | "none";
setRole.mutate({
userId: row.id,
role: value === "none" ? null : value,
});
}}
>
<SelectTrigger aria-label={`Platform role for ${row.name}`}><SelectValue /></SelectTrigger>
<SelectContent><SelectGroup>
{roles.map((role) => (
<option key={role} value={role}>
{role}
</option>
<SelectItem key={role} value={role} className="capitalize">
{role.replaceAll("_", " ").replace(/\b\w/g, (letter) => letter.toUpperCase())}
</SelectItem>
))}
</select>
</SelectGroup></SelectContent>
</Select>
</div>
</li>
))}
@@ -1,5 +1,7 @@
"use client";
import { NativeSelect } from "@/components/ui/native-select";
import { useState } from "react";
import { toast } from "sonner";
import type { EventCreatePolicy } from "@album/contracts";
@@ -61,9 +63,9 @@ export function DeploymentSettingsForm({
</Field>
<Field>
<FieldLabel htmlFor="policy">Event creation</FieldLabel>
<select
<NativeSelect
id="policy"
className="rounded-md border bg-background px-2 py-2"
value={policy}
onChange={(event) =>
setPolicy(event.target.value as EventCreatePolicy)
@@ -72,7 +74,7 @@ export function DeploymentSettingsForm({
<option value="open">Open (quota still applies)</option>
<option value="invite">Invite code required</option>
<option value="admin_only">Administrators only</option>
</select>
</NativeSelect>
</Field>
<Field>
<FieldLabel htmlFor="limit">Default event limit for new groups</FieldLabel>