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: add props and css variables to Slideshow #1758

Merged
merged 2 commits into from
Dec 17, 2024
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
8 changes: 6 additions & 2 deletions core/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Analytics } from '@vercel/analytics/react';
import { SpeedInsights } from '@vercel/speed-insights/next';
import { clsx } from 'clsx';
import type { Metadata } from 'next';
import { DM_Serif_Text, Inter, Roboto_Mono } from 'next/font/google';
import { NextIntlClientProvider } from 'next-intl';
Expand Down Expand Up @@ -108,8 +109,11 @@ export default async function RootLayout({ params, children }: Props) {
const messages = await getMessages();

return (
<html lang={locale}>
<body className={`${inter.variable} ${dm_serif_text.variable} ${roboto_mono.variable}`}>
<html
className={clsx(inter.variable, dm_serif_text.variable, roboto_mono.variable)}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: do we really need clsx here? 🙄

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idk we just never string interpolate classes like that anywhere so I changed it to be consistent

lang={locale}
>
<body>
<Notifications />
<NextIntlClientProvider locale={locale} messages={messages}>
<NuqsAdapter>
Expand Down
16 changes: 16 additions & 0 deletions core/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,20 @@

--accordion-title-font-family: var(--font-family-mono);
--accordion-content-font-family: var(--font-family-body);

/* Slideshow */

--slideshow-focus: hsl(var(--primary));
--slideshow-mask: hsl(var(--foreground) / 80%);
--slideshow-background: color-mix(in oklab, hsl(var(--primary)), black 75%);
--slideshow-title: hsl(var(--background));
--slideshow-title-font-family: var(--font-family-heading);
--slideshow-description: hsl(var(--background) / 80%);
--slideshow-description-font-family: var(--font-family-body);
--slideshow-pagination: hsl(var(--background));
--slideshow-play-border: hsl(var(--contrast-300) / 50%);
--slideshow-play-border-hover: hsl(var(--contrast-300) / 80%);
--slideshow-play-text: hsl(var(--background));
--slideshow-number: hsl(var(--background));
--slideshow-number-font-family: var(--font-family-mono);
}
118 changes: 71 additions & 47 deletions core/vibes/soul/sections/slideshow/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,31 @@ import Autoplay from 'embla-carousel-autoplay';
import Fade from 'embla-carousel-fade';
import useEmblaCarousel from 'embla-carousel-react';
import { Pause, Play } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { ComponentPropsWithoutRef, useCallback, useEffect, useState } from 'react';

import { ButtonLink } from '@/vibes/soul/primitives/button-link';
import { Image } from '~/components/image';

type ButtonLinkProps = ComponentPropsWithoutRef<typeof ButtonLink>;

interface Slide {
title: string;
description?: string;
showDescription?: boolean;
image?: { alt: string; blurDataUrl?: string; src: string };
cta?: { label: string; href: string };
cta?: {
label: string;
href: string;
variant?: ButtonLinkProps['variant'];
size?: ButtonLinkProps['size'];
shape?: ButtonLinkProps['shape'];
};
showCta?: boolean;
}

interface Props {
slides: Slide[];
playOnInit?: boolean;
interval?: number;
className?: string;
}
Expand Down Expand Up @@ -70,9 +81,9 @@ const useProgressButton = (
};
};

export function Slideshow({ slides, interval = 5000, className }: Props) {
export function Slideshow({ slides, playOnInit = true, interval = 5000, className }: Props) {
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true, duration: 20 }, [
Autoplay({ delay: interval }),
Autoplay({ delay: interval, playOnInit }),
Fade(),
]);
const { selectedIndex, scrollSnaps, onProgressButtonClick } = useProgressButton(emblaApi);
Expand Down Expand Up @@ -117,58 +128,71 @@ export function Slideshow({ slides, interval = 5000, className }: Props) {
}, [emblaApi, playCount]);

return (
<section className={clsx('relative h-[80vh] bg-primary-shadow @container', className)}>
<section
className={clsx('relative h-[80vh] bg-[var(--slideshow-background)] @container', className)}
>
<div className="h-full overflow-hidden" ref={emblaRef}>
<div className="flex h-full">
{slides.map(({ title, description, image, cta }, idx) => {
return (
<div className="relative h-full w-full min-w-0 shrink-0 grow-0 basis-full" key={idx}>
<div className="absolute inset-x-0 bottom-0 z-10 bg-gradient-to-t from-foreground/80 to-transparent">
<div className="mx-auto w-full max-w-screen-2xl text-balance px-4 pb-16 pt-12 text-background @xl:px-6 @xl:pb-20 @xl:pt-16 @4xl:px-8 @4xl:pt-20">
<h1 className="m-0 max-w-xl font-heading text-4xl font-medium leading-none @2xl:text-5xl @2xl:leading-[.9] @4xl:text-6xl">
{title}
</h1>
{description != null && description !== '' && (
<p className="mt-2 max-w-xl text-base leading-normal text-background/80 @xl:mt-3 @xl:text-lg">
{description}
</p>
)}
{cta != null && cta.href !== '' && cta.label !== '' && (
<ButtonLink className="mt-6 @xl:mt-8" href={cta.href} variant="tertiary">
{cta.label}
</ButtonLink>
)}
{slides.map(
({ title, description, showDescription = true, image, cta, showCta = true }, idx) => {
return (
<div
className="relative h-full w-full min-w-0 shrink-0 grow-0 basis-full"
key={idx}
>
<div className="absolute inset-x-0 bottom-0 z-10 bg-gradient-to-t from-[var(--slideshow-mask)] to-transparent">
<div className="mx-auto w-full max-w-screen-2xl text-balance px-4 pb-16 pt-12 @xl:px-6 @xl:pb-20 @xl:pt-16 @4xl:px-8 @4xl:pt-20">
<h1 className="m-0 max-w-xl font-[family-name:var(--slideshow-title-font-family)] text-4xl font-medium leading-none text-[var(--slideshow-title)] @2xl:text-5xl @2xl:leading-[.9] @4xl:text-6xl">
{title}
</h1>
{showDescription && (
<p className="max-w-x mt-2 font-[family-name:var(--slideshow-description-font-family)] text-base leading-normal text-[var(--slideshow-description)] @xl:mt-3 @xl:text-lg">
{description}
</p>
)}
{showCta && (
<ButtonLink
className="mt-6 @xl:mt-8"
href={cta?.href ?? '#'}
shape={cta?.shape ?? 'pill'}
size={cta?.size ?? 'large'}
variant={cta?.variant ?? 'tertiary'}
>
{cta?.label ?? 'Learn more'}
</ButtonLink>
)}
</div>
</div>
</div>

{image?.src != null && image.src !== '' && (
<Image
alt={image.alt}
blurDataURL={image.blurDataUrl}
className="block h-20 w-full object-cover"
fill
placeholder={
image.blurDataUrl != null && image.blurDataUrl !== '' ? 'blur' : 'empty'
}
priority
sizes="100vw"
src={image.src}
/>
)}
</div>
);
})}
{image?.src != null && image.src !== '' && (
<Image
alt={image.alt}
blurDataURL={image.blurDataUrl}
className="block h-20 w-full object-cover"
fill
placeholder={
image.blurDataUrl != null && image.blurDataUrl !== '' ? 'blur' : 'empty'
}
priority
sizes="100vw"
src={image.src}
/>
)}
</div>
);
},
)}
</div>
</div>

{/* Controls */}
<div className="absolute bottom-4 left-1/2 flex w-full max-w-screen-2xl -translate-x-1/2 flex-wrap items-center px-4 text-background @xl:bottom-6 @xl:px-6 @4xl:px-8">
<div className="absolute bottom-4 left-1/2 flex w-full max-w-screen-2xl -translate-x-1/2 flex-wrap items-center px-4 @xl:bottom-6 @xl:px-6 @4xl:px-8">
{/* Progress Buttons */}
{scrollSnaps.map((_: number, index: number) => {
return (
<button
aria-label={`View image number ${index + 1}`}
className="rounded-lg px-1.5 py-2 focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-primary"
className="rounded-lg px-1.5 py-2 focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-[var(--slideshow-focus)]"
key={index}
onClick={() => {
onProgressButtonClick(index);
Expand All @@ -179,7 +203,7 @@ export function Slideshow({ slides, interval = 5000, className }: Props) {
{/* White Bar - Current Index Indicator / Progress Bar */}
<div
className={clsx(
'absolute h-0.5 bg-background',
'absolute h-0.5 bg-[var(--slideshow-pagination)]',
'opacity-0 fill-mode-forwards',
isPlaying ? 'running' : 'paused',
index === selectedIndex
Expand All @@ -194,7 +218,7 @@ export function Slideshow({ slides, interval = 5000, className }: Props) {
/>
{/* Grey Bar BG */}
<div
className="h-0.5 bg-background opacity-30"
className="h-0.5 bg-[var(--slideshow-pagination)] opacity-30"
style={{ width: `${150 / slides.length}px` }}
/>
</div>
Expand All @@ -203,15 +227,15 @@ export function Slideshow({ slides, interval = 5000, className }: Props) {
})}

{/* Carousel Count - "01/03" */}
<span className="ml-auto mr-3 mt-px font-mono text-sm">
<span className="ml-auto mr-3 mt-px font-[family-name:var(--slideshow-number-font-family)] text-sm text-[var(--slideshow-number)]">
{selectedIndex + 1 < 10 ? `0${selectedIndex + 1}` : selectedIndex + 1}/
{slides.length < 10 ? `0${slides.length}` : slides.length}
</span>

{/* Stop / Start Button */}
<button
aria-label={isPlaying ? 'Pause' : 'Play'}
className="flex h-7 w-7 items-center justify-center rounded-lg border border-contrast-300/50 ring-primary transition-opacity duration-300 hover:border-contrast-300/80 focus-visible:outline-0 focus-visible:ring-2"
className="flex h-7 w-7 items-center justify-center rounded-lg border border-[var(--slideshow-play-border)] text-[var(--slideshow-play-text)] ring-[var(--slideshow-focus)] transition-opacity duration-300 hover:border-[var(--slideshow-play-border-hover)] focus-visible:outline-0 focus-visible:ring-2"
onClick={toggleAutoplay}
type="button"
>
Expand Down
Loading