Skip to content

Commit 128945a

Browse files
authored
Merge pull request #130 from Logannford/dashboard/roadmap-bento-box-component
feat(dashboard): start of progress (roadmap) bento grid item
2 parents 9beb66c + cd727bb commit 128945a

File tree

7 files changed

+261
-10
lines changed

7 files changed

+261
-10
lines changed

src/components/dashboard/dashboard-bento-grid.tsx

+3-8
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import AllQuestionsDashboardBentoBox from '../dashboard/all-questions-bento-box'
1515
import TodaysQuestionBentoBox from './todays-question-bento-box';
1616
import YesterdaysQuestionBentoBox from './yesterdays-question-bento-box';
1717
import { getYesterdaysQuestion } from '@/actions/questions/get-yesterdays-question';
18+
import ProgressBentoBox from './progression-bento-box';
1819

1920
export default async function DashboardBentoGrid() {
2021
const todaysQuestion = await getTodaysQuestion();
@@ -61,15 +62,9 @@ export default async function DashboardBentoGrid() {
6162
padded: true,
6263
},
6364
{
64-
title: 'Roadmap',
65-
description: (
66-
<div className="font-satoshi">
67-
View your progress and upcoming challenges, generated just for you!
68-
</div>
69-
),
70-
header: <Skeleton />,
65+
header: <ProgressBentoBox />,
7166
className: 'md:col-span-2 text-white',
72-
padded: true,
67+
padded: false,
7368
},
7469
{
7570
title: `${userStreak?.totalDailyStreak} day streak!`,

src/components/dashboard/progression-bento-box-card.tsx

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { ArrowRight, Lock } from 'lucide-react';
2+
import { Button } from '../ui/button';
3+
import { InfiniteMovingCards } from '../ui/infinite-moving-cards';
4+
import { Separator } from '../ui/separator';
5+
import { useUser } from '@/hooks/useUser';
6+
import { Grid } from '../ui/grid';
7+
import { useUserServer } from '@/hooks/useUserServer';
8+
import {
9+
Tooltip,
10+
TooltipContent,
11+
TooltipProvider,
12+
TooltipTrigger,
13+
} from '../ui/tooltip';
14+
import Chip from '../global/chip';
15+
16+
const items: {
17+
name: string;
18+
title: string;
19+
}[] = [
20+
{
21+
name: 'JavaScript',
22+
title: 'Object',
23+
},
24+
{
25+
name: 'JavaScript',
26+
title: 'Arrays',
27+
},
28+
{
29+
name: 'JavaScript',
30+
title: 'Functions',
31+
},
32+
{
33+
name: 'JavaScript',
34+
title: 'Scope',
35+
},
36+
{
37+
name: 'JavaScript',
38+
title: 'Asynchronous programming',
39+
},
40+
{
41+
name: 'JavaScript',
42+
title: 'Promises',
43+
},
44+
{
45+
name: 'JavaScript',
46+
title: 'Callbacks',
47+
},
48+
{
49+
name: 'JavaScript',
50+
title: 'Closures',
51+
},
52+
];
53+
54+
export default async function ProgressBentoBox() {
55+
const user = await useUserServer();
56+
57+
return (
58+
<div className="h-full flex flex-col p-4 relative group overflow-hidden">
59+
<div className="absolute">
60+
<Chip color="accent" text="Roadmap" />
61+
</div>
62+
<Grid size={20} position="top-right" />
63+
<div className="h-full flex items-center justify-center relative">
64+
<InfiniteMovingCards items={items} speed="slow" />
65+
<Separator className="absolute top-1/2 -translate-y-1/2 z-50 bg-black-50" />
66+
</div>
67+
<div className="flex w-full justify-between">
68+
<div className="space-y-1">
69+
<h6 className="text-xl">Progression</h6>
70+
<p className="font-satoshi text-xs">
71+
Your very own, personalised progression framework to help you grow
72+
as a developer.
73+
</p>
74+
</div>
75+
{user?.userLevel !== 'FREE' && user?.userLevel !== 'STANDARD' && (
76+
<Button
77+
href="/progression"
78+
variant="accent"
79+
className="font-ubuntu font-medium"
80+
>
81+
View yours now{' '}
82+
<ArrowRight className="size-3 ml-1 group-hover:ml-2 duration-300" />
83+
</Button>
84+
)}
85+
</div>
86+
{user?.userLevel === 'FREE' ||
87+
(user?.userLevel === 'STANDARD' && (
88+
<TooltipProvider>
89+
<Tooltip>
90+
<TooltipTrigger className="absolute">
91+
<div className="flex items-center bg-accent p-2 rounded-md">
92+
<Lock className="size-5" />
93+
</div>
94+
</TooltipTrigger>
95+
<TooltipContent className="font-satoshi">
96+
You need to be a premium member to access this feature.
97+
</TooltipContent>
98+
</Tooltip>
99+
</TooltipProvider>
100+
))}
101+
</div>
102+
);
103+
}

src/components/global/navigation/sidebar.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,10 @@ export function AppSidebar() {
8686
{user?.userLevel === 'ADMIN' ||
8787
(user?.userLevel === 'PREMIUM' &&
8888
!pathname.startsWith('/settings')) ? (
89-
<p>Roadmap</p>
89+
<p>Progression</p>
9090
) : (
9191
<div className="flex items-center gap-3">
92-
<p>Roadmap</p>
92+
<p>Progression</p>
9393
<LockIcon className="size-4" />
9494
</div>
9595
)}
+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
'use client';
2+
3+
import { cn } from '@/utils/cn';
4+
import React, { useEffect, useState } from 'react';
5+
6+
export const InfiniteMovingCards = ({
7+
items,
8+
direction = 'left',
9+
speed = 'fast',
10+
pauseOnHover = true,
11+
className,
12+
}: {
13+
items: {
14+
name: string;
15+
title: string;
16+
}[];
17+
direction?: 'left' | 'right';
18+
speed?: 'fast' | 'normal' | 'slow';
19+
pauseOnHover?: boolean;
20+
className?: string;
21+
}) => {
22+
const containerRef = React.useRef<HTMLDivElement>(null);
23+
const scrollerRef = React.useRef<HTMLUListElement>(null);
24+
25+
useEffect(() => {
26+
addAnimation();
27+
}, []);
28+
29+
const [start, setStart] = useState(false);
30+
31+
function addAnimation() {
32+
if (containerRef.current && scrollerRef.current) {
33+
const scrollerContent = Array.from(scrollerRef.current.children);
34+
35+
scrollerContent.forEach((item) => {
36+
const duplicatedItem = item.cloneNode(true);
37+
if (scrollerRef.current) {
38+
scrollerRef.current.appendChild(duplicatedItem);
39+
}
40+
});
41+
42+
getDirection();
43+
getSpeed();
44+
setStart(true);
45+
}
46+
}
47+
48+
const getDirection = () => {
49+
if (containerRef.current) {
50+
if (direction === 'left') {
51+
containerRef.current.style.setProperty(
52+
'--animation-direction',
53+
'forwards'
54+
);
55+
} else {
56+
containerRef.current.style.setProperty(
57+
'--animation-direction',
58+
'reverse'
59+
);
60+
}
61+
}
62+
};
63+
64+
const getSpeed = () => {
65+
if (containerRef.current) {
66+
if (speed === 'fast') {
67+
containerRef.current.style.setProperty('--animation-duration', '20s');
68+
} else if (speed === 'normal') {
69+
containerRef.current.style.setProperty('--animation-duration', '40s');
70+
} else {
71+
containerRef.current.style.setProperty('--animation-duration', '80s');
72+
}
73+
}
74+
};
75+
76+
return (
77+
<div
78+
ref={containerRef}
79+
className={cn(
80+
'scroller relative z-20 max-w-7xl overflow-hidden [mask-image:linear-gradient(to_right,transparent,white_20%,white_80%,transparent)]',
81+
className
82+
)}
83+
>
84+
<ul
85+
ref={scrollerRef}
86+
className={cn(
87+
'flex min-w-full shrink-0 gap-4 py-16 w-max flex-nowrap',
88+
start && 'animate-scroll-right',
89+
pauseOnHover && 'hover:[animation-play-state:paused]'
90+
)}
91+
>
92+
{items.map((item, idx) => (
93+
<li
94+
className={cn(
95+
'bg-black-100 w-[150px] relative rounded-2xl border flex-shrink-0 border-black-50 p-4 md:w-[200px] transition-all',
96+
idx % 2 === 0 ? '-translate-y-16' : 'translate-y-16'
97+
)}
98+
key={item.name}
99+
>
100+
{/* Roadmap marker line */}
101+
<div
102+
className={cn(
103+
'absolute left-1/2 w-px bg-black-50',
104+
idx % 2 === 0
105+
? 'bottom-0 h-[25px] translate-y-full'
106+
: 'top-0 h-[25px] -translate-y-full'
107+
)}
108+
>
109+
{/* Dot at the connection point */}
110+
<div
111+
className={cn(
112+
'absolute left-1/2 size-1.5 rounded-full bg-black-50 -translate-x-1/2',
113+
idx % 2 !== 0 ? 'top-0' : 'bottom-0'
114+
)}
115+
/>
116+
</div>
117+
118+
<div
119+
aria-hidden="true"
120+
className="user-select-none -z-1 pointer-events-none absolute -left-0.5 -top-0.5 h-[calc(100%_+_4px)] w-[calc(100%_+_4px)]"
121+
></div>
122+
<div className="relative z-20 flex size-full justify-center items-center">
123+
<span className="text-center leading-[1.6] text-white font-ubuntu font-normal">
124+
{item.title}
125+
</span>
126+
</div>
127+
</li>
128+
))}
129+
</ul>
130+
</div>
131+
);
132+
};

src/hooks/useUserServer.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { getUserFromDb, getUserFromSession } from '@/actions/user/get-user';
2+
3+
export const useUserServer = async () => {
4+
const userSession = await getUserFromSession();
5+
if (!userSession?.data?.user?.id) return null;
6+
7+
const userData = await getUserFromDb(userSession?.data?.user?.id);
8+
if (!userData) {
9+
console.error('No user data found');
10+
return null;
11+
}
12+
13+
return userData;
14+
};

tailwind.config.ts

+7
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,18 @@ const config: Config = {
124124
transform: 'translateY(calc(-72px * var(--question-count)))',
125125
},
126126
},
127+
'scroll-right': {
128+
to: {
129+
transform: 'translate(calc(-50% - 0.5rem))',
130+
},
131+
},
127132
},
128133
animation: {
129134
'accordion-down': 'accordion-down 0.2s ease-out',
130135
'accordion-up': 'accordion-up 0.2s ease-out',
131136
scroll: 'scroll 30s linear infinite',
137+
'scroll-right':
138+
'scroll-right var(--animation-duration, 40s) var(--animation-direction, forwards) linear infinite',
132139
},
133140
},
134141
},

0 commit comments

Comments
 (0)