add client/admin pages, show info and created admin api and server actions

This commit is contained in:
2024-04-28 19:32:31 +03:00
parent db66161d81
commit d6b259d71c
33 changed files with 458 additions and 91 deletions

View File

@@ -26,7 +26,7 @@ export const CardWrapper = ({
}: Props) => {
return (
<Card
className="shadow-2xl max-w-[430px] w-full sm:min-w-[430px]">
className={`shadow-2xl max-w-[430px] w-full sm:min-w-[430px]`}>
<CardHeader>
<Header label={headerLabel} title={headerTitle}/>
</CardHeader>
@@ -34,7 +34,7 @@ export const CardWrapper = ({
{children}
</CardContent>
{showSocial && <CardFooter className="flex-wrap">
<div className="relative flex-none w-[100%] mb-4" style={{ background: 'block' }}>
<div className="relative flex-none w-[100%] mb-4">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t"></span>
</div>

View File

@@ -10,7 +10,7 @@ const LogoutButton = ({ children }: LogoutButtonProps) => {
const onClick = () => logout()
return (
<span onClick={onClick} className="cursor-pointer">
<span onClick={onClick}>
{children}
</span>
)

View File

@@ -0,0 +1,23 @@
'use client'
import { UserRole } from '@prisma/client'
import { useCurrentRole } from '@/hooks/useCurrentRole'
import FormError from '@/components/form-error'
interface RoleGateProps {
children: React.ReactNode
allowedRole: UserRole
}
export const RoleGate = ({
children,
allowedRole,
}: RoleGateProps) => {
const role = useCurrentRole()
if (role !== allowedRole) {
return <FormError message="You do not have permission to view this content!"/>
}
return <>{children}</>
}

View File

@@ -3,26 +3,29 @@
import { useCurrentUser } from '@/hooks/useCurrentUser'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { FaUser } from 'react-icons/fa6'
import { IoExitOutline } from 'react-icons/io5'
import LogoutButton from '@/components/auth/logout-button'
const fallbackInitials = (name?: string | null | undefined): string => {
return (name ?? '').split(' ').map((w: string) => w[0]).join('').toUpperCase()
}
const UserButton = () => {
const user = useCurrentUser()
return (
<DropdownMenu>
<DropdownMenuTrigger>
<DropdownMenuTrigger className="outline-0">
<Avatar>
<AvatarImage src={user?.image || ''} alt="User Avatar"/>
<AvatarFallback className="bg-sky-400">
<FaUser className="text-muted"/>
<AvatarFallback className="bg-sky-600 text-muted text-lg font-light">
{fallbackInitials(user?.name)}
</AvatarFallback>
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-40" align="end">
<DropdownMenuContent className="w-28 p-0" align="end">
<LogoutButton>
<DropdownMenuItem className="cursor-pointer">
<DropdownMenuItem className="cursor-pointer p-2">
<IoExitOutline className="w-4 h-4 mr-2"/>
Logout
</DropdownMenuItem>

View File

@@ -0,0 +1,44 @@
import { type ExtendedUser } from '@/config/auth'
import { Card, CardContent, CardHeader } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
interface UserInfoProps {
user?: ExtendedUser
label: string
}
export const UserInfo = ({ user, label }: UserInfoProps) => {
return (
<Card className={`max-w-[430px] w-full sm:min-w-[430px] shadow-md`}>
<CardHeader>
<p className="text-2xl font-semibold text-center">
{label}
</p>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-row justify-between items-center rounded-lg border p-3 shadow-sm">
<span className="text-sm font-medium">ID</span>
<span className="trancate text-xs max-w-[180px] font-mono p-1 bg-slate-100 rounded-md">{user?.id}</span>
</div>
<div className="flex flex-row justify-between items-center rounded-lg border p-3 shadow-sm">
<span className="text-sm font-medium">Name</span>
<span className="trancate text-xs max-w-[180px] font-mono p-1 bg-slate-100 rounded-md">{user?.name}</span>
</div>
<div className="flex flex-row justify-between items-center rounded-lg border p-3 shadow-sm">
<span className="text-sm font-medium">Email</span>
<span className="trancate text-xs max-w-[180px] font-mono p-1 bg-slate-100 rounded-md">{user?.email}</span>
</div>
<div className="flex flex-row justify-between items-center rounded-lg border p-3 shadow-sm">
<span className="text-sm font-medium">Role</span>
<span className="trancate text-xs max-w-[180px] font-mono p-1 bg-slate-100 rounded-md">{user?.role}</span>
</div>
<div className="flex flex-row justify-between items-center rounded-lg border p-3 shadow-sm">
<span className="text-sm font-medium">Two factor authentication</span>
<Badge variant={user?.isTwoFactorEnabled ? 'success' : 'destructive'}>
{user?.isTwoFactorEnabled ? 'ON' : 'OFF'}
</Badge>
</div>
</CardContent>
</Card>
)
}

View File

@@ -1,15 +1,14 @@
import { TriangleAlert } from 'lucide-react'
type Props = {
type FormErrorProps = {
message?: string
}
const FormError = ({ message }: Props) => {
const FormError = ({ message }: FormErrorProps) => {
if (!message) return null
return (
<div
className="bg-destructive/15 p-3 rounded-md flex items-center gap-x-2 text-sm text-destructive">
<div className="bg-destructive/15 p-3 rounded-md flex items-center gap-x-2 text-sm text-destructive">
<TriangleAlert className="w-4 h-4"/>
<p>{message}</p>
</div>

View File

@@ -1,15 +1,14 @@
import { CircleCheck } from 'lucide-react'
type Props = {
type FormSuccessProps = {
message?: string
}
const FormSuccess = ({ message }: Props) => {
const FormSuccess = ({ message }: FormSuccessProps) => {
if (!message) return null
return (
<div
className="bg-lime-500/15 p-3 rounded-md flex items-center gap-x-2 text-sm text-lime-800">
<div className="bg-lime-500/15 p-3 rounded-md flex items-center gap-x-2 text-sm text-lime-800">
<CircleCheck className="w-4 h-4"/>
<p>{message}</p>
</div>

View File

@@ -7,15 +7,18 @@ import { ChangeEvent } from 'react'
export default function LocaleSwitcher () {
const changeLocale = useChangeLocale()
const locale = useCurrentLocale()
const selectHandler = (e: ChangeEvent<HTMLSelectElement>) => changeLocale(e.target.value as loc)
const selectHandler = (e: ChangeEvent<HTMLSelectElement>) =>
changeLocale(e.target.value as loc)
// {cn(styles['yo-locale-switcher'], 'pr-4')}
return (//@ts-ignore
<select className="appearance-none bg-transparent block text-center text-xs text-sky-600 py-1 px-2 my-2 mx-0 outline-0"
aria-label="Switch locale"
defaultValue={locale} onChange={selectHandler}
<form><select
className="appearance-none bg-transparent block text-center text-xs text-sky-600 py-1 px-2 my-2 mx-0 outline-0"
aria-label="Switch locale"
defaultValue={locale} onChange={selectHandler}
>
{LC.map(item => (<option key={item.iso} value={item.code} className="pr-4">
{item.iso.toUpperCase()}
</option>))}
</select>)
</select></form>)
}

38
components/ui/badge.tsx Normal file
View File

@@ -0,0 +1,38 @@
import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const badgeVariants = cva(
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default:
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive:
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
outline: 'text-foreground',
success:
'border-transparent bg-emerald-500 text-primary-foreground hover:bg-emerald-500/80',
},
},
defaultVariants: {
variant: 'default',
},
},
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge ({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

31
components/ui/sonner.tsx Normal file
View File

@@ -0,0 +1,31 @@
"use client"
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
)
}
export { Toaster }