Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions src/layouts/AdminLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react'
import { useState } from 'react'

import { Outlet } from 'react-router-dom'

Expand Down Expand Up @@ -125,13 +125,9 @@ const AdminLayout = () => {
<div className={`transition-all duration-300 ${collapsed ? 'ml-[72px]' : 'ml-64'} `}>
<Topbar
title="Moderation Queue"
showSearch
searchPlaceholder="Search reports..."
actionsSlot={<AvatarMenu name="Admin Mod" />}
searchSlot={
<input
className="border-border-secondary w-full rounded-md border-2 bg-transparent px-3 py-2 text-sm outline-none"
placeholder="Search reports..."
/>
}
/>

<main className="p-6">
Expand Down
42 changes: 16 additions & 26 deletions src/shared/composites/AvatarMenu/AvatarMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ export function AvatarMenu({

<div className="text-text-secondary text-xs">Moderator account</div>
</div>

{/* Items */}
<Button
onClick={onEditProfile}
Expand All @@ -97,7 +96,6 @@ export function AvatarMenu({
>
Security settings
</Button>

<div className="bg-border-tertiary my-1 h-px" />
<Button
onClick={onPreferences}
Expand All @@ -108,39 +106,31 @@ export function AvatarMenu({
>
Preferences
</Button>
<Button
className="flex items-center justify-between px-3 py-2"
variant={'ghost'}
rightIcon={
<Switch
className="ms-6"
checked={themeName === 'dark'}
onChange={(checked: boolean) => {
const newTheme = checked ? 'dark' : 'light'

setTheme(newTheme)

toast.info(`Theme changed to ${newTheme}`)
}}
/>
}
>
<div className="hover:bg-bg-tertiary flex items-center justify-between rounded-md px-3 py-2 transition">
<div className="text-text-primary flex items-center gap-3 text-sm">
{themeName === 'dark' ? (
<SunIcon className="size-4" />
) : (
<MoonIcon className="size-4" />
)}

{themeName === 'dark' ? (
<div className="text-text-secondary text-sm">Light mode</div>
) : (
<div className="text-text-secondary text-sm">Dark mode</div>
)}
<span className="text-text-secondary">
{themeName === 'dark' ? 'Light mode' : 'Dark mode'}
</span>
</div>
</Button>
<div className="bg-border-tertiary my-1 h-px" />

<Switch
checked={themeName === 'dark'}
onChange={(checked: boolean) => {
const newTheme = checked ? 'dark' : 'light'

setTheme(newTheme)

toast.info(`Theme changed to ${newTheme}`)
}}
/>
</div>{' '}
<div className="bg-border-tertiary my-1 h-px" />
<Button
onClick={onLogout}
className="contents-center text-text-danger hover:bg-bg-danger flex w-full cursor-pointer flex-row items-center gap-2 rounded-md px-3 py-2 text-left text-sm transition"
Expand Down
87 changes: 87 additions & 0 deletions src/shared/composites/MetricCard/MetricCard.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type { Meta, StoryObj } from '@storybook/react-vite'

import { MetricCard } from './MetricCard'

const meta: Meta<typeof MetricCard> = {
title: 'Shared/Composites/MetricCard',

component: MetricCard,
}

export default meta

type Story = StoryObj<typeof MetricCard>

export const Default: Story = {
args: {
label: 'Escalation Rate',

value: '34%',

subLabel: 'of all reports',
},
}

export const Danger: Story = {
args: {
label: 'Escalated',

value: 1,

subLabel: 'needs review',

valueColor: 'danger',
},
}

export const Warning: Story = {
args: {
label: 'Pending',

value: 3,

subLabel: 'awaiting action',

valueColor: 'warning',
},
}

export const Success: Story = {
args: {
label: 'AI Auto-resolved',

value: 8,

subLabel: 'this week',

valueColor: 'success',
},
}

export const WithoutSubLabel: Story = {
args: {
label: 'Total Users',

value: 1284,
},
}

export const GridPreview: Story = {
args: {
label: 'Preview',

value: 0,
},

render: () => (
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
<MetricCard label="Pending" value={3} subLabel="awaiting action" valueColor="warning" />

<MetricCard label="Escalated" value={1} subLabel="needs review" valueColor="danger" />

<MetricCard label="AI Auto-resolved" value={8} subLabel="this week" valueColor="success" />

<MetricCard label="Escalation Rate" value="34%" subLabel="of all reports" />
</div>
),
}
74 changes: 74 additions & 0 deletions src/shared/composites/MetricCard/MetricCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { BaseCard } from '@/shared/primitives/BaseCard'

import { cva } from 'class-variance-authority'

const valueStyles = cva(
`
text-[22px]
leading-none
font-medium
`,
{
variants: {
valueColor: {
default: 'text-text-primary',

danger: 'text-text-danger',

warning: 'text-text-warning',

success: 'text-text-success',
},
},

defaultVariants: {
valueColor: 'default',
},
}
)

export interface MetricCardProps {
label: string

value: string | number

subLabel?: string

valueColor?: 'default' | 'danger' | 'warning' | 'success'

className?: string
}

export function MetricCard({
label,

value,

subLabel,

valueColor = 'default',

className = '',
}: MetricCardProps) {
return (
<BaseCard padding="md" className={className}>
<div className="flex min-h-[72px] flex-col justify-between">
<div className="text-text-secondary text-[10px] tracking-wider uppercase">{label}</div>

<div className="">
<div
className={valueStyles({
valueColor,
})}
>
{value}
</div>

{subLabel && <div className="text-text-secondary mt-1 text-[10px]">{subLabel}</div>}
</div>
</div>
</BaseCard>
)
}

export default MetricCard
3 changes: 3 additions & 0 deletions src/shared/composites/MetricCard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './MetricCard'

export { default } from './MetricCard'
26 changes: 21 additions & 5 deletions src/shared/composites/Topbar/Topbar.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import React from 'react'

import { Bars3Icon } from '@heroicons/react/24/outline'
import { MagnifyingGlassIcon } from '@heroicons/react/24/solid'

export interface TopbarProps {
title: string

showSearch?: boolean

searchPlaceholder?: string

searchSlot?: React.ReactNode

actionsSlot?: React.ReactNode
Expand All @@ -18,11 +23,11 @@ export function Topbar({
title,

searchSlot,

showSearch,
actionsSlot,

onMenuToggle,

searchPlaceholder,
className = '',
}: TopbarProps) {
return (
Expand All @@ -45,9 +50,20 @@ export function Topbar({
</div>

{/* Desktop Search */}
{searchSlot && (
<div className="hidden flex-1 px-6 md:flex md:justify-center">
<div className="w-full max-w-xl">{searchSlot}</div>
{(showSearch || searchSlot) && (
<div className="hidden flex-1 items-center px-6 md:flex md:justify-center">
<div className="w-full max-w-xl">
{searchSlot || (
<div className="border-border-secondary bg-bg-secondary flex items-center gap-2 rounded-md border px-3 py-2">
<MagnifyingGlassIcon className="text-text-tertiary size-4 shrink-0" />

<input
placeholder={searchPlaceholder || 'Search...'}
className="w-full bg-transparent text-sm outline-none"
/>
</div>
)}
</div>
</div>
)}

Expand Down
52 changes: 49 additions & 3 deletions src/views/LoginPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import LoginCard from '@/LoginCard'
// import LoginCard from '@/LoginCard'
import MetricCard from '@/shared/composites/MetricCard'

// import { SidebarNav } from '@/shared/composites/SidebarNav'

Expand All @@ -13,9 +14,54 @@ import LoginCard from '@/LoginCard'
// import { useState } from 'react'

const LoginPage = () => {
const metricCardData = [
{
idx: 1,
label: 'Pending',
value: 3,
subLabel: 'awaiting action',
valueColor: 'warning',
},
{
idx: 2,
label: 'Escalated',
value: 1,
subLabel: 'needs review',
valueColor: 'danger',
},
{
idx: 3,
label: 'AI Auto-resolved',
value: 8,
subLabel: 'this week',
valueColor: 'success',
},
{
idx: 4,
label: 'Escalation Rate',
value: '34%',
subLabel: 'of all reports',
valueColor: 'default',
},
] as const

return (
<div className="flex min-h-screen items-center justify-center">
<LoginCard />
<div className="flex min-h-screen flex-col gap-6 sm:p-6">
{' '}
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
{metricCardData.map((item) => {
return (
<MetricCard
key={item.idx}
label={item.label}
value={item.value}
subLabel={item.subLabel}
valueColor={item.valueColor}
/>
)
})}
</div>
{/* <LoginCard /> */}
</div>
)
}
Expand Down
Loading