
An access request queue where admins approve or deny permission requests, with an approval length setting.
@esuiss
components/blocks/roles-requests.tsx
"use client"
import * as React from "react"
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
Empty,
EmptyContent,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInset,
SidebarMenu,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarRail,
SidebarTrigger,
useSidebar,
} from "@/components/ui/sidebar"
type NavItem = {
value: string
label: string
icon: React.ReactNode
badge?: React.ReactNode
group?: string
}
function NavSidebar({
items,
value: valueProp,
defaultValue,
onValueChange,
header,
footer,
}: {
items: NavItem[]
value?: string
defaultValue?: string
onValueChange?: (value: string) => void
header?: React.ReactNode
footer?: React.ReactNode
}) {
const { isMobile, setOpenMobile } = useSidebar()
const [uncontrolledValue, setUncontrolledValue] = React.useState(
defaultValue ?? items[0]?.value
)
const value = valueProp ?? uncontrolledValue
const groups = [...new Set(items.map((item) => item.group ?? ""))]
return (
<Sidebar collapsible="icon">
{header && (
<SidebarHeader className="group-data-[collapsible=icon]:hidden">
{header}
</SidebarHeader>
)}
<SidebarContent>
{groups.map((group) => (
<SidebarGroup key={group}>
{group && <SidebarGroupLabel>{group}</SidebarGroupLabel>}
<SidebarGroupContent>
<SidebarMenu>
{items
.filter((item) => (item.group ?? "") === group)
.map((item) => (
<SidebarMenuItem key={item.value}>
<SidebarMenuButton
isActive={item.value === value}
tooltip={item.label}
aria-current={item.value === value ? "page" : undefined}
onClick={() => {
if (valueProp === undefined) {
setUncontrolledValue(item.value)
}
onValueChange?.(item.value)
if (isMobile) setOpenMobile(false)
}}
>
{item.icon}
<span>{item.label}</span>
</SidebarMenuButton>
{item.badge !== undefined && (
<SidebarMenuBadge>{item.badge}</SidebarMenuBadge>
)}
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
))}
</SidebarContent>
{footer && (
<SidebarFooter className="group-data-[collapsible=icon]:hidden">
{footer}
</SidebarFooter>
)}
<SidebarRail />
</Sidebar>
)
}
const nav: NavItem[] = [
{
value: "users",
label: "Users",
icon: (
<IconPlaceholder
lucide="UsersIcon"
tabler="IconUsers"
hugeicons="UserGroupIcon"
phosphor="UsersIcon"
remixicon="RiGroupLine"
/>
),
},
{
value: "roles",
label: "Roles",
icon: (
<IconPlaceholder
lucide="ShieldIcon"
tabler="IconShield"
hugeicons="Shield01Icon"
phosphor="ShieldIcon"
remixicon="RiShieldLine"
/>
),
},
{
value: "log",
label: "Activity",
icon: (
<IconPlaceholder
lucide="ChartColumnIcon"
tabler="IconChartBar"
hugeicons="ChartColumnIcon"
phosphor="ChartBarIcon"
remixicon="RiBarChartLine"
/>
),
},
{
value: "settings",
label: "Settings",
icon: (
<IconPlaceholder
lucide="SettingsIcon"
tabler="IconSettings"
hugeicons="Settings01Icon"
phosphor="GearIcon"
remixicon="RiSettingsLine"
/>
),
},
]
type Decision = "approved" | "denied" | null
const initialRequests = [
{
name: "Morgan Lee",
permission: "Change prices",
reason: "I need to update the campaign prices.",
time: "10 min ago",
hue: 30,
},
{
name: "Riley Chen",
permission: "Export customers",
reason: "I need the customer list for the monthly report.",
time: "1 hr ago",
hue: 150,
},
{
name: "Jordan Park",
permission: "Issue refunds",
reason: "So I can refund support tickets myself.",
time: "Yesterday",
hue: 100,
},
{
name: "Taylor Kim",
permission: "Publish",
reason: "I'd like to publish blog posts.",
time: "2 days ago",
hue: 200,
},
]
export function RolesRequests() {
const idPrefix = React.useId()
const [requests, setRequests] = React.useState(() =>
initialRequests.map((request) => ({
...request,
decision: null as Decision,
}))
)
const [duration, setDuration] = React.useState("Permanent")
const open = requests.filter((request) => !request.decision).length
const decide = (name: string, decision: Decision) => {
setRequests((current) =>
current.map((request) =>
request.name === name ? { ...request, decision } : request
)
)
window.setTimeout(
() =>
setRequests((current) =>
current.filter((request) => request.name !== name)
),
900
)
}
return (
<SidebarProvider>
<NavSidebar
items={nav}
defaultValue="roles"
header={
<p className="flex items-center gap-2 px-2 pt-1 text-sm font-semibold">
<span className="font-bold">suiss</span> Admin
</p>
}
/>
<SidebarInset>
<div className="flex flex-col gap-5 px-5 py-5.5 md:px-7">
<div className="flex flex-wrap items-start gap-3">
<SidebarTrigger className="-ms-2 mt-0.5" />
<div className="flex flex-1 flex-col">
<h1 className="text-3xl font-semibold tracking-tight">
Access requests
</h1>
<p className="text-sm text-muted-foreground" aria-live="polite">
{open} pending {open === 1 ? "request" : "requests"}
</p>
</div>
<div className="flex items-center gap-2">
<span
id={`${idPrefix}-duration`}
className="text-sm text-muted-foreground"
>
Approval length
</span>
<Select
value={duration}
onValueChange={(value) => setDuration(value as string)}
>
<SelectTrigger
size="sm"
aria-labelledby={`${idPrefix}-duration`}
className="h-7.5"
>
<SelectValue />
</SelectTrigger>
<SelectContent align="end">
{["24 hours", "7 days", "Permanent"].map((item) => (
<SelectItem key={item} value={item}>
{item}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{requests.length > 0 ? (
<ul className="overflow-hidden rounded-2xl bg-muted">
{requests.map((request) => (
<li
key={request.name}
data-closing={request.decision ? "" : undefined}
className="grid grid-rows-[1fr] transition-[grid-template-rows,opacity] delay-500 duration-400 ease-[cubic-bezier(0.32,0.72,0,1)] not-data-closing:delay-0 data-closing:grid-rows-[0fr] data-closing:opacity-0 motion-reduce:transition-none"
>
<div className="flex min-h-0 items-start gap-3 overflow-hidden ps-4">
<Avatar className="mt-3 size-8.5">
<AvatarFallback
style={{
background: `linear-gradient(160deg, oklch(0.78 0.1 ${request.hue}), oklch(0.6 0.14 ${request.hue + 30}))`,
}}
className="text-white"
>
{request.name
.split(" ")
.map((part) => part[0])
.join("")}
</AvatarFallback>
</Avatar>
<div className="flex min-w-0 flex-1 flex-col gap-2.5 border-border py-3 pe-4 sm:flex-row sm:items-start sm:gap-3 [li+li_&]:border-t">
<div className="flex min-w-0 flex-1 flex-col gap-0.75">
<span className="flex items-center gap-2">
<span className="text-sm font-semibold">
{request.name}
</span>
<span className="text-xs text-muted-foreground">
{request.time}
</span>
</span>
<span className="text-sm">
Requests {request.permission}
</span>
<span className="text-sm text-muted-foreground">
{request.reason}
</span>
</div>
{request.decision ? (
<span
role="status"
data-decision={request.decision}
className="pt-0.5 text-sm font-semibold text-[color-mix(in_oklab,var(--color-green-500),var(--foreground)_45%)] data-[decision=denied]:text-destructive dark:text-green-500 dark:data-[decision=denied]:text-destructive"
>
{request.decision === "approved"
? "Approved"
: "Denied"}
</span>
) : (
<div className="flex flex-wrap gap-2 sm:justify-end">
<Button
size="sm"
variant="secondary"
onClick={() => decide(request.name, "denied")}
>
Deny
</Button>
<Button
size="sm"
onClick={() => decide(request.name, "approved")}
>
Approve
</Button>
</div>
)}
</div>
</div>
</li>
))}
</ul>
) : (
<Empty className="gap-3 p-10 transition-[opacity,translate] duration-800 starting:translate-y-2 starting:opacity-0 motion-reduce:transition-none">
<EmptyHeader>
<EmptyMedia className="mb-0 text-[color-mix(in_oklab,var(--color-green-500),var(--foreground)_35%)] dark:text-green-500">
<IconPlaceholder
lucide="CheckIcon"
tabler="IconCheck"
hugeicons="Tick02Icon"
phosphor="CheckIcon"
remixicon="RiCheckLine"
className="size-10"
/>
</EmptyMedia>
<EmptyTitle className="text-xl">
No pending requests.
</EmptyTitle>
</EmptyHeader>
<EmptyContent>
<button
type="button"
onClick={() =>
setRequests(
initialRequests.map((request) => ({
...request,
decision: null,
}))
)
}
className="rounded-xs text-sm text-primary outline-none hover:underline focus-visible:ring-3 focus-visible:ring-ring/50"
>
Reset the sample ›
</button>
</EmptyContent>
</Empty>
)}
</div>
</SidebarInset>
</SidebarProvider>
)
}


























