Code
app/page.tsx
1"use client"
2
3import ShowcaseCard from "@/components/showcase-card";
4import {
5 Carousel,
6 CarouselContent,
7 CarouselItem,
8 CarouselNext,
9 CarouselPrevious
10} from "@/components/ui/carousel";
11import Autoplay from "embla-carousel-autoplay";
12
13const ShowcasePage = () => {
14 return (
15 <section className=" w-full py-20 flex items-center justify-center bg-black h-screen">
16 <div className=" w-full relative group">
17 <div className="absolute top-0 bottom-0 left-0 w-[2%] md:w-[20%] z-[1] bg-gradient-to-r from-black to-transparent" />
18 <div className="absolute top-0 bottom-0 right-0 w-[2%] md:w-[20%] z-[1] bg-gradient-to-l from-black to-transparent" />
19
20 <Carousel
21 plugins={[
22 Autoplay({
23 delay: 5000,
24 }),
25 ]}
26 opts={{
27 align: "center",
28 loop: true,
29 }}
30 className="w-full"
31 >
32 <CarouselContent className="px-3 md:px-0">
33 <CarouselItem className=" basis-[90%] md:basis-1/4">
34 <ShowcaseCard color="#FB3F00" icon="/assets/showcase-icon-1.png" image="/assets/showcase-1.png" video="/videos/showcase-1.mp4" title="Lovi" link="www.lovi.com" />
35 </CarouselItem>
36 <CarouselItem className=" basis-[90%] md:basis-1/4">
37 <ShowcaseCard color="#1500FF" icon="/assets/showcase-icon-2.png" image="/assets/showcase-2.png" video="/videos/showcase-2.mp4" title="Lovi" link="www.lovi.com" />
38 </CarouselItem>
39 <CarouselItem className=" basis-[90%] md:basis-1/4">
40 <ShowcaseCard color="#85C25D" icon="/assets/showcase-icon-3.png" image="/assets/showcase-3.png" video="/videos/showcase-3.mp4" title="Lovi" link="www.lovi.com" />
41 </CarouselItem>
42 <CarouselItem className=" basis-[90%] md:basis-1/4">
43 <ShowcaseCard color="#FF8E1C" icon="/assets/showcase-icon-2.png" image="/assets/showcase-4.png" title="Lovi" link="www.lovi.com" />
44 </CarouselItem>
45 <CarouselItem className=" basis-[90%] md:basis-1/4">
46 <ShowcaseCard color="#4A4A2F" icon="/assets/showcase-icon-1.png" image="/assets/showcase-5.png" title="Lovi" link="www.lovi.com" />
47 </CarouselItem>
48 </CarouselContent>
49 <div className=" opacity-0 group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity duration-200 ease-in pointer-events-none">
50 <CarouselPrevious />
51
52 <CarouselNext />
53 </div>
54 </Carousel>
55 </div>
56 </section>
57 );
58}
59
60export default ShowcasePage;
Installation
Initialized a project and installed the necessary dependencies to use this component.
1. Initialize a new Next.js project:
npx create-next-app@latest my-app
2. Install Shadcn UI
npx shadcn@latest init
3. Install Shadcn Components
npx shadcn@latest add carousel button
4. Install Embla Carousel Autoplay
npm install embla-carousel-autoplay --save
Add Showcase Card Component
src/components/showcase-card.tsx
1import Image from "next/image";
2import Link from "next/link";
3
4const ShowcaseCard = ({
5 title,
6 image,
7 link,
8 video,
9 icon,
10 color
11}: {
12 title: string;
13 video?: string;
14 image: string;
15 link: string;
16 icon: string;
17 color: string;
18}) => {
19 return (
20 <div className=" w-full group/card">
21 {video ? (
22 <div style={{ backgroundColor: color }} className=" rounded-xl w-full p-2.5 md:p-4 relative">
23 <Link rel="" target="_blank" href={link}>
24 <div className="absolute hidden rounded-lg group-hover/card:block inset-0 z-20 hover:block cursor-pointer opacity-40" style={{ backgroundColor: color }} />
25 </Link>
26 <video
27 className="w-full h-auto aspect-square rounded-lg"
28 playsInline
29 autoPlay
30 loop
31 muted
32 poster={image}
33 >
34 <source src={video} type="video/mp4" />
35 Your browser does not support the video tag.
36 </video>
37 </div>
38 ) : (
39 <div className=" w-full relative h-auto aspect-square">
40 <Link rel="" target="_blank" href={link}>
41 <div className="absolute hidden rounded-lg group-hover/card:block inset-0 z-20 hover:block cursor-pointer opacity-40" style={{ backgroundColor: color }} />
42 </Link>
43 <Image
44 src={image}
45 alt={title}
46 fill
47 className="object-cover rounded-lg"
48 />
49 </div>
50 )}
51 <div className="flex items-center gap-4 mt-4">
52 <Image width={40} height={40} src={icon} className=" object-cover border rounded-full" alt={title} />
53 <div className="flex flex-col items-start text-white">
54 <p>{link}</p>
55 <p>{title}</p>
56 </div>
57 </div>
58 </div>
59 );
60}
61
62export default ShowcaseCard;
Update Carousel Component
src/components/ui/carousel.tsx
1"use client"
2
3import useEmblaCarousel, {
4 type UseEmblaCarouselType,
5} from "embla-carousel-react"
6import { ChevronLeft, ChevronRight } from "lucide-react"
7import * as React from "react"
8
9import { Button } from "@/components/ui/button"
10import { cn } from "@/lib/utils"
11
12type CarouselApi = UseEmblaCarouselType[1]
13type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
14type CarouselOptions = UseCarouselParameters[0]
15type CarouselPlugin = UseCarouselParameters[1]
16
17type CarouselProps = {
18 opts?: CarouselOptions
19 plugins?: CarouselPlugin
20 orientation?: "horizontal" | "vertical"
21 setApi?: (api: CarouselApi) => void
22}
23
24type CarouselContextProps = {
25 carouselRef: ReturnType<typeof useEmblaCarousel>[0]
26 api: ReturnType<typeof useEmblaCarousel>[1]
27 scrollPrev: () => void
28 scrollNext: () => void
29 canScrollPrev: boolean
30 canScrollNext: boolean
31} & CarouselProps
32
33const CarouselContext = React.createContext<CarouselContextProps | null>(null)
34
35function useCarousel() {
36 const context = React.useContext(CarouselContext)
37
38 if (!context) {
39 throw new Error("useCarousel must be used within a <Carousel />")
40 }
41
42 return context
43}
44
45function Carousel({
46 orientation = "horizontal",
47 opts,
48 setApi,
49 plugins,
50 className,
51 children,
52 ...props
53}: React.ComponentProps<"div"> & CarouselProps) {
54 const [carouselRef, api] = useEmblaCarousel(
55 {
56 ...opts,
57 axis: orientation === "horizontal" ? "x" : "y",
58 },
59 plugins
60 )
61 const [canScrollPrev, setCanScrollPrev] = React.useState(false)
62 const [canScrollNext, setCanScrollNext] = React.useState(false)
63
64 const onSelect = React.useCallback((api: CarouselApi) => {
65 if (!api) return
66 setCanScrollPrev(api.canScrollPrev())
67 setCanScrollNext(api.canScrollNext())
68 }, [])
69
70 const scrollPrev = React.useCallback(() => {
71 api?.scrollPrev()
72 }, [api])
73
74 const scrollNext = React.useCallback(() => {
75 api?.scrollNext()
76 }, [api])
77
78 const handleKeyDown = React.useCallback(
79 (event: React.KeyboardEvent<HTMLDivElement>) => {
80 if (event.key === "ArrowLeft") {
81 event.preventDefault()
82 scrollPrev()
83 } else if (event.key === "ArrowRight") {
84 event.preventDefault()
85 scrollNext()
86 }
87 },
88 [scrollPrev, scrollNext]
89 )
90
91 React.useEffect(() => {
92 if (!api || !setApi) return
93 setApi(api)
94 }, [api, setApi])
95
96 React.useEffect(() => {
97 if (!api) return
98 onSelect(api)
99 api.on("reInit", onSelect)
100 api.on("select", onSelect)
101
102 return () => {
103 api?.off("select", onSelect)
104 }
105 }, [api, onSelect])
106
107 return (
108 <CarouselContext.Provider
109 value={{
110 carouselRef,
111 api: api,
112 opts,
113 orientation:
114 orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
115 scrollPrev,
116 scrollNext,
117 canScrollPrev,
118 canScrollNext,
119 }}
120 >
121 <div
122 onKeyDownCapture={handleKeyDown}
123 className={cn("relative", className)}
124 role="region"
125 aria-roledescription="carousel"
126 data-slot="carousel"
127 {...props}
128 >
129 {children}
130 </div>
131 </CarouselContext.Provider>
132 )
133}
134
135function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
136 const { carouselRef, orientation } = useCarousel()
137
138 return (
139 <div
140 ref={carouselRef}
141 className="overflow-hidden"
142 data-slot="carousel-content"
143 >
144 <div
145 className={cn(
146 "flex",
147 orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
148 className
149 )}
150 {...props}
151 />
152 </div>
153 )
154}
155
156function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
157 const { orientation } = useCarousel()
158
159 return (
160 <div
161 role="group"
162 aria-roledescription="slide"
163 data-slot="carousel-item"
164 className={cn(
165 "min-w-0 shrink-0 grow-0 basis-full",
166 orientation === "horizontal" ? "pl-4" : "pt-4",
167 className
168 )}
169 {...props}
170 />
171 )
172}
173
174function CarouselPrevious({
175 className,
176 variant = "outline",
177 size = "icon",
178 ...props
179}: React.ComponentProps<typeof Button>) {
180 const { orientation, scrollPrev, canScrollPrev } = useCarousel()
181
182 return (
183 <Button
184 data-slot="carousel-previous"
185 variant={variant}
186 size={size}
187 className={cn(
188 "absolute h-8 md:h-9 w-8 md:w-9 rounded-full hover:bg-white hover:text-black bg-black/50 backdrop-blur-md text-white border-none z-50",
189 orientation === "horizontal"
190 ? " left-12 md:left-[28rem] top-1/2 -translate-y-[100%]"
191 : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
192 className
193 )}
194 disabled={!canScrollPrev}
195 onClick={scrollPrev}
196 {...props}
197 >
198 <ChevronLeft className=" size-5" />
199 <span className="sr-only">Previous slide</span>
200 </Button>
201 )
202}
203
204function CarouselNext({
205 className,
206 variant = "outline",
207 size = "icon",
208 ...props
209}: React.ComponentProps<typeof Button>) {
210 const { orientation, scrollNext, canScrollNext } = useCarousel()
211
212 return (
213 <Button
214 data-slot="carousel-next"
215 variant={variant}
216 size={size}
217 className={cn(
218 "absolute h-8 md:h-9 w-8 md:w-9 rounded-full hover:bg-white bg-black/50 hover:text-black backdrop-blur-md text-white border-none z-50",
219 orientation === "horizontal"
220 ? " right-12 md:right-[28rem] top-1/2 -translate-y-[100%]"
221 : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
222 className
223 )}
224 disabled={!canScrollNext}
225 onClick={scrollNext}
226 {...props}
227 >
228 <ChevronRight className=" size-5" />
229 <span className="sr-only">Next slide</span>
230 </Button>
231 )
232}
233
234export {
235 Carousel,
236 CarouselContent,
237 CarouselItem, CarouselNext, CarouselPrevious, type CarouselApi
238}
Add Button Component
src/components/ui/button.tsx
1import * as React from "react"
2import { Slot } from "@radix-ui/react-slot"
3import { cva, type VariantProps } from "class-variance-authority"
4
5import { cn } from "@/lib/utils"
6
7const buttonVariants = cva(
8 "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9 {
10 variants: {
11 variant: {
12 default:
13 "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14 destructive:
15 "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
16 outline:
17 "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
18 secondary:
19 "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20 ghost:
21 "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22 link: "text-primary underline-offset-4 hover:underline",
23 },
24 size: {
25 default: "h-9 px-4 py-2 has-[>svg]:px-3",
26 sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
27 lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28 icon: "size-9",
29 },
30 },
31 defaultVariants: {
32 variant: "default",
33 size: "default",
34 },
35 }
36)
37
38function Button({
39 className,
40 variant,
41 size,
42 asChild = false,
43 ...props
44}: React.ComponentProps<"button"> &
45 VariantProps<typeof buttonVariants> & {
46 asChild?: boolean
47 }) {
48 const Comp = asChild ? Slot : "button"
49
50 return (
51 <Comp
52 data-slot="button"
53 className={cn(buttonVariants({ variant, size, className }))}
54 {...props}
55 />
56 )
57}
58
59export { Button, buttonVariants }
Add Global Styles
src/app/global.css
1@import "tailwindcss";
2@import "tw-animate-css";
3
4@custom-variant dark (&:is(.dark *));
5
6@theme inline {
7 --color-background: var(--background);
8 --color-foreground: var(--foreground);
9 --font-inter: var(--font-inter);
10 --color-sidebar-ring: var(--sidebar-ring);
11 --color-sidebar-border: var(--sidebar-border);
12 --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
13 --color-sidebar-accent: var(--sidebar-accent);
14 --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
15 --color-sidebar-primary: var(--sidebar-primary);
16 --color-sidebar-foreground: var(--sidebar-foreground);
17 --color-sidebar: var(--sidebar);
18 --color-chart-5: var(--chart-5);
19 --color-chart-4: var(--chart-4);
20 --color-chart-3: var(--chart-3);
21 --color-chart-2: var(--chart-2);
22 --color-chart-1: var(--chart-1);
23 --color-ring: var(--ring);
24 --color-input: var(--input);
25 --color-border: var(--border);
26 --color-destructive: var(--destructive);
27 --color-accent-foreground: var(--accent-foreground);
28 --color-accent: var(--accent);
29 --color-muted-foreground: var(--muted-foreground);
30 --color-muted: var(--muted);
31 --color-secondary-foreground: var(--secondary-foreground);
32 --color-secondary: var(--secondary);
33 --color-primary-foreground: var(--primary-foreground);
34 --color-primary: var(--primary);
35 --color-popover-foreground: var(--popover-foreground);
36 --color-popover: var(--popover);
37 --color-card-foreground: var(--card-foreground);
38 --color-card: var(--card);
39 --radius-sm: calc(var(--radius) - 4px);
40 --radius-md: calc(var(--radius) - 2px);
41 --radius-lg: var(--radius);
42 --radius-xl: calc(var(--radius) + 4px);
43}
44
45:root {
46 --radius: 0.625rem;
47 --background: oklch(1 0 0);
48 --foreground: oklch(0.145 0 0);
49 --card: oklch(1 0 0);
50 --card-foreground: oklch(0.145 0 0);
51 --popover: oklch(1 0 0);
52 --popover-foreground: oklch(0.145 0 0);
53 --primary: oklch(0.205 0 0);
54 --primary-foreground: oklch(0.985 0 0);
55 --secondary: oklch(0.97 0 0);
56 --secondary-foreground: oklch(0.205 0 0);
57 --muted: oklch(0.97 0 0);
58 --muted-foreground: oklch(0.556 0 0);
59 --accent: oklch(0.97 0 0);
60 --accent-foreground: oklch(0.205 0 0);
61 --destructive: oklch(0.577 0.245 27.325);
62 --border: oklch(0.922 0 0);
63 --input: oklch(0.922 0 0);
64 --ring: oklch(0.708 0 0);
65 --chart-1: oklch(0.646 0.222 41.116);
66 --chart-2: oklch(0.6 0.118 184.704);
67 --chart-3: oklch(0.398 0.07 227.392);
68 --chart-4: oklch(0.828 0.189 84.429);
69 --chart-5: oklch(0.769 0.188 70.08);
70 --sidebar: oklch(0.985 0 0);
71 --sidebar-foreground: oklch(0.145 0 0);
72 --sidebar-primary: oklch(0.205 0 0);
73 --sidebar-primary-foreground: oklch(0.985 0 0);
74 --sidebar-accent: oklch(0.97 0 0);
75 --sidebar-accent-foreground: oklch(0.205 0 0);
76 --sidebar-border: oklch(0.922 0 0);
77 --sidebar-ring: oklch(0.708 0 0);
78}
79
80.dark {
81 --background: oklch(0.145 0 0);
82 --foreground: oklch(0.985 0 0);
83 --card: oklch(0.205 0 0);
84 --card-foreground: oklch(0.985 0 0);
85 --popover: oklch(0.205 0 0);
86 --popover-foreground: oklch(0.985 0 0);
87 --primary: oklch(0.922 0 0);
88 --primary-foreground: oklch(0.205 0 0);
89 --secondary: oklch(0.269 0 0);
90 --secondary-foreground: oklch(0.985 0 0);
91 --muted: oklch(0.269 0 0);
92 --muted-foreground: oklch(0.708 0 0);
93 --accent: oklch(0.269 0 0);
94 --accent-foreground: oklch(0.985 0 0);
95 --destructive: oklch(0.704 0.191 22.216);
96 --border: oklch(1 0 0 / 10%);
97 --input: oklch(1 0 0 / 15%);
98 --ring: oklch(0.556 0 0);
99 --chart-1: oklch(0.488 0.243 264.376);
100 --chart-2: oklch(0.696 0.17 162.48);
101 --chart-3: oklch(0.769 0.188 70.08);
102 --chart-4: oklch(0.627 0.265 303.9);
103 --chart-5: oklch(0.645 0.246 16.439);
104 --sidebar: oklch(0.205 0 0);
105 --sidebar-foreground: oklch(0.985 0 0);
106 --sidebar-primary: oklch(0.488 0.243 264.376);
107 --sidebar-primary-foreground: oklch(0.985 0 0);
108 --sidebar-accent: oklch(0.269 0 0);
109 --sidebar-accent-foreground: oklch(0.985 0 0);
110 --sidebar-border: oklch(1 0 0 / 10%);
111 --sidebar-ring: oklch(0.556 0 0);
112}
113
114@layer base {
115 * {
116 @apply border-border outline-ring/50;
117 }
118 body {
119 @apply bg-background text-foreground;
120 }
121}