Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dashboard): create right panel with user/creator modes and activ… #122

Merged
merged 1 commit into from
Feb 3, 2025
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
22 changes: 22 additions & 0 deletions apps/web/app/(routes)/dashboard/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { RightPanel } from '~/components/sections/user/right-panel'

export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="min-h-screen flex flex-col">
<div className="flex-1 flex flex-col-reverse md:flex-row">
<main className="flex-1 w-full md:w-[calc(100%-340px)]">
{children}
</main>
<aside className="w-full md:w-[340px] md:flex-shrink-0">
<div className="h-full sticky top-0">
<RightPanel />
</div>
</aside>
</div>
</div>
)
}
47 changes: 47 additions & 0 deletions apps/web/components/base/scroll-area.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use client'

import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
import * as React from 'react'
import { cn } from '~/lib/utils'

const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn('relative overflow-hidden', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName

const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = 'vertical', ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
'flex touch-none select-none transition-colors',
orientation === 'vertical' &&
'h-full w-2.5 border-l border-l-transparent p-[1px]',
orientation === 'horizontal' &&
'h-2.5 border-t border-t-transparent p-[1px]',
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName

export { ScrollArea, ScrollBar }
71 changes: 71 additions & 0 deletions apps/web/components/sections/user/right-panel/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
'use client'

import { Suspense, useState } from 'react'
import { Card } from '~/components/base/card'
import { ScrollArea } from '~/components/base/scroll-area'
import { Tabs, TabsList, TabsTrigger } from '~/components/base/tabs'
import type { DashboardMode } from '~/lib/types/right-side-panel'
import {
ActivitySkeleton,
LatestUpdates,
NavigationMenu,
NavigationSkeleton,
RecentActivity,
UpdatesSkeleton,
} from './lazy-components'

export function RightPanel() {
const [mode, setMode] = useState<DashboardMode>('user')

return (
<Card
className="w-full h-full bg-background p-2 rounded-none md:rounded-lg"
role="complementary"
aria-label="Dashboard side panel"
>
<div className="flex flex-col h-auto md:h-full pt-2">
<div className="px-2 pb-2">
<Tabs
value={mode}
onValueChange={(value: string) => setMode(value as DashboardMode)}
className="w-full"
aria-label="Dashboard mode selection"
>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger
value="user"
aria-label="Switch to user mode"
className="data-[state=active]:bg-primary/10 data-[state=active]:text-primary"
>
User
</TabsTrigger>
<TabsTrigger
value="creator"
aria-label="Switch to creator mode"
className="data-[state=active]:bg-primary/10 data-[state=active]:text-primary"
>
Creator
</TabsTrigger>
</TabsList>
</Tabs>
</div>

<ScrollArea className="flex-1 px-2" aria-label="Dashboard content">
<div className="space-y-6 py-2" aria-label="Dashboard sections">
<Suspense fallback={<NavigationSkeleton />}>
<NavigationMenu mode={mode} />
</Suspense>

<Suspense fallback={<UpdatesSkeleton />}>
<LatestUpdates />
</Suspense>

<Suspense fallback={<ActivitySkeleton />}>
<RecentActivity />
</Suspense>
</div>
</ScrollArea>
</div>
</Card>
)
}
60 changes: 60 additions & 0 deletions apps/web/components/sections/user/right-panel/latest-updates.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
'use client'

import { Book } from 'lucide-react'
import { Button } from '~/components/base/button'
import { ScrollArea } from '~/components/base/scroll-area'
import { mockUpdates } from '~/lib/constants/mock-data/right-panel-mocks'

export function LatestUpdates() {
return (
<section className="space-y-3" aria-labelledby="latest-updates-title">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Book className="w-5 h-5 text-muted-foreground" aria-hidden="true" />
<h3 id="latest-updates-title" className="text-sm font-medium">
Latest Updates
</h3>
</div>
<Button
variant="link"
className="text-sm text-primary p-0 h-auto"
aria-label="View all updates"
>
View All
</Button>
</div>

<ScrollArea
className="h-[180px] xs:h-[200px] w-full rounded-md border"
aria-label="Latest project updates"
>
<div className="p-3 space-y-3" role="feed" aria-busy="false">
{mockUpdates.map((update) => (
<article
key={update.id}
className="space-y-1 border-b border-border pb-3 last:border-0"
>
<div className="flex items-center justify-between gap-4">
<h4 className="text-sm font-medium line-clamp-1">
{update.title}
</h4>
<time
className="text-xs text-muted-foreground whitespace-nowrap"
dateTime={update.timestamp}
>
{update.timestamp}
</time>
</div>
<p
className="text-xs text-muted-foreground line-clamp-2"
aria-label={`Update description: ${update.description}`}
>
{update.description}
</p>
</article>
))}
</div>
</ScrollArea>
</section>
)
}
65 changes: 65 additions & 0 deletions apps/web/components/sections/user/right-panel/lazy-components.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// apps/web/components/sections/user/right-panel/lazy-components.tsx
import dynamic from 'next/dynamic'
import { Skeleton } from '~/components/base/skeleton'

export function NavigationSkeleton() {
return (
<div className="space-y-2">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-12 w-full" />
))}
</div>
)
}

export function UpdatesSkeleton() {
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<Skeleton className="h-6 w-32" />
<Skeleton className="h-6 w-20" />
</div>
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-24 w-full" />
))}
</div>
)
}

export function ActivitySkeleton() {
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<Skeleton className="h-6 w-32" />
<Skeleton className="h-6 w-20" />
</div>
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-16 w-full" />
))}
</div>
)
}

export const NavigationMenu = dynamic(
() => import('./navigation-menu').then((mod) => mod.NavigationMenu),
{
loading: () => <NavigationSkeleton />,
ssr: true,
},
)

export const LatestUpdates = dynamic(
() => import('./latest-updates').then((mod) => mod.LatestUpdates),
{
loading: () => <UpdatesSkeleton />,
ssr: true,
},
)

export const RecentActivity = dynamic(
() => import('./recent-activity').then((mod) => mod.RecentActivity),
{
loading: () => <ActivitySkeleton />,
ssr: true,
},
)
84 changes: 84 additions & 0 deletions apps/web/components/sections/user/right-panel/navigation-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
'use client'

import { ChevronRight, Heart, Settings, Wallet } from 'lucide-react'
import Link from 'next/link'
import type {
DashboardMode,
NavigationItem,
} from '~/lib/types/right-side-panel'

const userNavigation: NavigationItem[] = [
{
icon: Wallet,
label: 'My Wallet',
href: '/wallet',
description: 'Access your wallet and transactions',
},
{
icon: Heart,
label: 'Supported Projects',
href: '/supported-projects',
description: 'View your supported projects',
},
{
icon: Settings,
label: 'Settings',
href: '/settings',
description: 'Manage your account settings',
},
]

const creatorNavigation: NavigationItem[] = [
{
icon: Wallet,
label: 'Creator Dashboard',
href: '/creator-dashboard',
description: 'View your creator dashboard',
},
{
icon: Heart,
label: 'My Projects',
href: '/my-projects',
description: 'Manage your created projects',
},
{
icon: Settings,
label: 'Creator Settings',
href: '/creator-settings',
description: 'Manage creator settings',
},
]

interface NavigationMenuProps {
mode: DashboardMode
}

export function NavigationMenu({ mode }: NavigationMenuProps) {
const items = mode === 'user' ? userNavigation : creatorNavigation
const menuLabel = mode === 'user' ? 'User navigation' : 'Creator navigation'

return (
<nav className="space-y-1" aria-label={menuLabel}>
{items.map((item) => (
<Link
key={item.href}
href={item.href}
className="flex items-center justify-between p-3 rounded-lg hover:bg-muted transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
aria-label={item.description}
>
<div className="flex items-center gap-3">
<item.icon
className="w-5 h-5 text-muted-foreground"
aria-hidden="true"
/>
<span className="text-sm font-medium">{item.label}</span>
</div>
<ChevronRight
className="w-4 h-4 text-muted-foreground"
aria-hidden="true"
/>
</Link>
))}
</nav>
)
}
Loading
Loading