feat: add Discord profile dropdown menu with role display and integrate with admin and form layouts via AdminShellServer.

This commit is contained in:
2026-07-03 23:02:15 +07:00 Unverified
parent 6cb28421e6
commit ac6c35598c
12 changed files with 413 additions and 83 deletions
+8 -26
View File
@@ -27,43 +27,25 @@ import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { DropdownMenuAvatar, DiscordMemberProfile } from "@/components/dropdown-menu-avatar";
export function FormLayoutClient({
userName,
userImage,
profile,
}: {
userName: string;
userImage: string;
profile?: DiscordMemberProfile | null;
}) {
const [imgError, setImgError] = useState(false);
return (
<div className="flex items-center gap-3">
{userImage && !imgError && (
<Image
src={userImage}
alt={userName}
width={28}
height={28}
className="rounded-full object-cover"
onError={() => setImgError(true)}
unoptimized
/>
)}
<span className="text-sm text-muted-foreground">{userName}</span>
<Button
variant="ghost"
size="sm"
onClick={() => signOut()}
>
<LogOut className="w-4 h-4" />
</Button>
</div>
<DropdownMenuAvatar
userName={userName}
userImage={userImage}
profile={profile || null}
/>
);
}
+8
View File
@@ -4,6 +4,7 @@ import { FormLayoutClient } from "./client";
import LoginDialog from "@/components/login-dialog";
import { Metadata } from "next";
import { ThemeToggle } from "@/components/theme-toggle";
import { getDiscordMemberProfile } from "@/app/actions/discord";
import Link from 'next/link';
const siteUrl = process.env.BASE_URL;
@@ -35,6 +36,12 @@ export default async function FormLayout({
children: React.ReactNode;
}) {
const session = await getServerSession(authOptions);
const discordId = (session?.user as any)?.discordId;
let profile = null;
if (discordId) {
profile = await getDiscordMemberProfile(discordId);
}
return (
<div className="flex min-h-screen flex-col flex-1 w-full bg-background text-foreground">
@@ -49,6 +56,7 @@ export default async function FormLayout({
<FormLayoutClient
userName={session.user.name ?? ""}
userImage={session.user.image ?? ""}
profile={profile}
/>
)}
<ThemeToggle />
+39 -2
View File
@@ -7,6 +7,9 @@ import {
CardDescription,
} from "@/components/ui/card";
import { HtmlDisplay } from "@/components/html-display";
import { getServerSession } from "next-auth";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { getGuildMemberRoles } from "@/app/actions/discord";
export const dynamic = "force-dynamic";
@@ -16,14 +19,48 @@ export default async function FormsListingPage() {
orderBy: (f, { desc }) => [desc(f.createdAt)],
});
const session = await getServerSession(authOptions);
const discordId = (session?.user as { discordId?: string })?.discordId;
let userRoles: string[] = [];
if (discordId) {
try {
userRoles = await getGuildMemberRoles(discordId);
} catch (e) {
console.error(e);
}
}
const visibleForms = formsList.filter((form) => {
const allowedRoles = form.allowedRoles || [];
const deniedRoles = form.deniedRoles || [];
if (allowedRoles.length === 0 && deniedRoles.length === 0) return true;
// If user is not logged in but there are role restrictions, hide it.
if (!discordId) return false;
let isAllowed = allowedRoles.length === 0;
if (allowedRoles.length > 0) {
isAllowed = userRoles.some(r => allowedRoles.includes(r));
}
let isDenied = false;
if (deniedRoles.length > 0) {
isDenied = userRoles.some(r => deniedRoles.includes(r));
}
return isAllowed && !isDenied;
});
return (
<div className="max-w-4xl mx-auto p-4 md:p-6 space-y-6">
<h1 className="text-3xl font-bold tracking-tight">Form ทั้งหมด</h1>
{formsList.length === 0 ? (
{visibleForms.length === 0 ? (
<p className="text-muted-foreground">ตอนนี้ยังไม่เปิดรับสัก Form</p>
) : (
<div className="flex flex-col gap-4">
{formsList.map((form) => (
{visibleForms.map((form) => (
<Link key={form.id} href={`/form/${form.id}`}>
<Card className="hover:bg-muted/50 transition-colors h-full flex flex-col">
<CardHeader>