Skip to content

Commit 340b7d0

Browse files
authored
Added carrousel with tutorial videos (#389)
1 parent 6cf5552 commit 340b7d0

File tree

14 files changed

+350
-0
lines changed

14 files changed

+350
-0
lines changed

src/components/VirtualLab/labs-listing/no-vlabs.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { motion } from 'framer-motion';
44

55
import { NewWindowAdd, UserCircle } from '@/components/icons/EditorIcons';
66
import { ActionCard, HeroSection } from '@/components/VirtualLab/labs-listing/elements';
7+
import { TutorialsCarrousel } from '@/components/tutorials-carrousel';
78

89
type Props = {
910
showCreateSubscription?: boolean;
@@ -43,6 +44,7 @@ export default function VirtualSplashScreen({ showCreateSubscription = true }: P
4344
</motion.div>
4445
)}
4546
</div>
47+
<TutorialsCarrousel />
4648
</div>
4749
);
4850
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
*[_type=="tutorial"][]
2+
{
3+
title,
4+
description,
5+
"url": videoUrl,
6+
"imageURL": thumbnail.asset->url,
7+
"imageWidth": thumbnail.asset->metadata.dimensions.width,
8+
"imageHeight": thumbnail.asset->metadata.dimensions.height
9+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import query from './hooks.groq';
2+
import { assertType, TypeDef } from '@/util/type-guards';
3+
import { useSanity } from '@/services/sanity';
4+
import { logError } from '@/util/logger';
5+
6+
export function useSanityContentForTutorialsList() {
7+
return useSanity(query, isContentForTutorialsList) ?? [];
8+
}
9+
10+
export interface ContentForTutorialItem {
11+
url: string;
12+
title: string;
13+
description: string;
14+
imageURL: string;
15+
imageWidth: number;
16+
imageHeight: number;
17+
}
18+
19+
function isContentForTutorialsList(data: unknown): data is ContentForTutorialItem[] {
20+
const typeStringOrNull: TypeDef = ['|', 'string', 'null'];
21+
try {
22+
assertType(
23+
data,
24+
[
25+
'array',
26+
{
27+
url: 'string',
28+
title: typeStringOrNull,
29+
description: typeStringOrNull,
30+
imageURL: 'string',
31+
imageWidth: 'number',
32+
imageHeight: 'number',
33+
},
34+
],
35+
'ContentForTutorialsList'
36+
);
37+
return true;
38+
} catch (ex) {
39+
logError(ex);
40+
return false;
41+
}
42+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './tutorials-carrousel';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './navigation';
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
.navigation {
2+
flex: 0 0 auto;
3+
display: flex;
4+
flex-direction: row;
5+
justify-content: flex-start;
6+
align-items: center;
7+
flex-wrap: nowrap;
8+
gap: 8px;
9+
}
10+
11+
.navigation > button.page {
12+
content: '';
13+
width: 24px;
14+
height: 6px;
15+
border-radius: 99vmax;
16+
background-color: #096dd9;
17+
}
18+
19+
.navigation > button.page.selected {
20+
background-color: #fff;
21+
}
22+
23+
.navigation > button.arrow {
24+
width: 3em;
25+
display: grid;
26+
place-items: center;
27+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React from 'react';
2+
3+
import { classNames } from '@/util/utils';
4+
import { IconChevronLeft } from '@/components/LandingPage/icons/IconChevronLeft';
5+
import { IconChevronRight } from '@/components/LandingPage/icons/IconChevronRight';
6+
7+
import styles from './navigation.module.css';
8+
9+
export interface NavigationProps {
10+
className?: string;
11+
count: number;
12+
value: number;
13+
onChange(value: number): void;
14+
}
15+
16+
export function Navigation({ className, count, value, onChange }: NavigationProps) {
17+
const handleMoveLeft = () => {
18+
onChange((value + count - 1) % count);
19+
};
20+
const handleMoveRight = () => {
21+
onChange((value + 1) % count);
22+
};
23+
24+
return (
25+
<nav className={classNames(className, styles.navigation)}>
26+
<button
27+
className={styles.arrow}
28+
type="button"
29+
onClick={handleMoveLeft}
30+
aria-label="Previous card"
31+
>
32+
<IconChevronLeft />
33+
</button>
34+
{new Array(count).fill(0).map((_, index) => (
35+
<button
36+
className={classNames(styles.page, index === value && styles.selected)}
37+
// eslint-disable-next-line react/no-array-index-key
38+
key={index}
39+
onClick={() => onChange(index)}
40+
type="button"
41+
aria-label={`Go to card #${index + 1}`}
42+
/>
43+
))}
44+
<button
45+
className={styles.arrow}
46+
type="button"
47+
onClick={handleMoveRight}
48+
aria-label="Next card"
49+
>
50+
<IconChevronRight />
51+
</button>
52+
</nav>
53+
);
54+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export function scrollCardIntoView(container: HTMLDivElement, cardIndex: number) {
2+
const node = container.querySelectorAll('a').item(cardIndex);
3+
if (node) {
4+
node.scrollIntoView({
5+
behavior: 'smooth',
6+
});
7+
}
8+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './tutorial-card';
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export function PlayIcon() {
2+
return (
3+
<svg
4+
xmlns="http://www.w3.org/2000/svg"
5+
viewBox="-150 -150 300 300"
6+
preserveAspectRatio="xMidYMid meet"
7+
>
8+
<g fill="currentcolor" stroke="currentcolor" strokeWidth="10" strokeLinejoin="round">
9+
<circle fill="none" cx="0" cy="0" r="60" />
10+
<path d="M30,0L-20,25,-20,-25Z" />
11+
</g>
12+
</svg>
13+
);
14+
}

0 commit comments

Comments
 (0)