feat(screen): add services, approach, media production pages

This commit is contained in:
shafin-r
2025-12-06 19:48:33 +06:00
parent 2707d4bef7
commit 5c7183ba88
12 changed files with 840 additions and 122 deletions

View File

@ -1,8 +1,43 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
/* Global default font = Lato */
:root {
font-family: var(--font-lato), sans-serif;
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
/* Tailwind theme overrides */
@ -11,3 +46,84 @@
--font-lato: var(--font-lato), sans-serif;
--font-inter: var(--font-inter), sans-serif;
}
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@ -1,120 +1,217 @@
import CardCarousel from "@/components/Carousel";
import {
Carousel,
CarouselContent,
CarouselItem,
} from "@/components/ui/carousel";
import Image from "next/image";
export default function Home() {
const serviceCarousel = [
{
id: 0,
content: (
<>
<section className="space-y-6">
<div>
<Image
src={"/images/campaign-strategy.png"}
width={614}
height={418}
alt="campaign-strategy"
/>
</div>
<div className="space-y-4">
<p className="px-6 py-2 text-[#7B2E45] font-lato border-2 border-[#7B2E45] rounded-full w-fit">
01
</p>
<h2 className="text-4xl text-white font-lato">
Campaign Strategy
</h2>
<p className="text-lg font-lato text-white/60">
Comprehensive planning from research to execution, crafted to
win hearts and minds.
</p>
</div>
</section>
</>
),
},
{
id: 1,
content: (
<>
<section className="space-y-6">
<div>
<Image
src={"/images/media-production.png"}
width={614}
height={418}
alt="media-production"
/>
</div>
<div className="space-y-4">
<p className="px-6 py-2 text-[#7B2E45] font-lato border-2 border-[#7B2E45] rounded-full w-fit">
02
</p>
<h2 className="text-4xl text-white font-lato">
Media Production
</h2>
<p className="text-lg font-lato text-white/60">
Cinematic storytelling through video, photography, and
multimedia content that captivates.
</p>
</div>
</section>
</>
),
},
{
id: 2,
content: (
<>
<section className="space-y-6">
<div>
<Image
src={"/images/research.png"}
width={614}
height={418}
alt="research"
/>
</div>
<div className="space-y-4">
<p className="px-6 py-2 text-[#7B2E45] font-lato border-2 border-[#7B2E45] rounded-full w-fit">
03
</p>
<h2 className="text-4xl text-white font-lato">
Research & Analytics
</h2>
<p className="text-lg font-lato text-white/60">
Data-driven insights that inform every decision and optimize
every outcome
</p>
</div>
</section>
</>
),
},
];
return (
<main className="min-h-screen bg-radial-[at_50%_50%] from-[#1a0a0e] to-[#0D0F11] flex flex-col space-y-12 items-center pt-20">
<section className="flex flex-col space-y-4">
<div className="flex gap-4 justify-between items-center">
<hr className="text-[#7B2E45]/50 w-1/2" />
<div className="flex gap-2">
<div className="bg-[#7B2E45] h-3 w-3 rotate-45"></div>
<div className="border-[#7B2E45] border h-3 w-3 rotate-45"></div>
<main className="bg-radial-[at_50%_50%] from-[#1a0a0e] to-[#0D0F11] pt-20">
<section className="min-h-screen space-y-8 flex flex-col items-center">
<section className="flex flex-col space-y-4">
<div className="flex gap-4 justify-between items-center">
<hr className="text-[#7B2E45]/50 w-1/2" />
<div className="flex gap-2">
<div className="bg-[#7B2E45] h-3 w-3 rotate-45"></div>
<div className="border-[#7B2E45] border h-3 w-3 rotate-45"></div>
</div>
<hr className="text-[#7B2E45]/50 w-1/2" />
</div>
<hr className="text-[#7B2E45]/50 w-1/2" />
</div>
<div>
<h1
className="
<div>
<h1
className="
font-inter text-[80px] leading-20 font-black tracking-tight
bg-[linear-gradient(123.54deg,#FFFFFF_0%,#D8DFE4_38.8%,#7B2E45_71.52%,#9B3E55_102.17%)]
bg-clip-text text-transparent
"
>
Da Next
</h1>
<h1
className="
>
Da Next
</h1>
<h1
className="
font-inter text-[80px] font-black tracking-tight
bg-[linear-gradient(123.54deg,#7B2E45_7.16%,#9B3E55_53.58%,#D8DFE4_100%)]
bg-clip-text text-transparent pb-2 leading-20
"
>
Agency
</h1>
</div>
</section>
<section className="flex flex-col items-center h-fit space-y-6">
<div className="flex flex-col lg:md:flex-row lg:md:gap-2">
<h3 className="text-[#D8DFE4] font-lato font-normal text-center text-2xl">
Shaping Narratives.
</h3>
<h3 className="text-[#D8DFE4] font-lato font-normal text-center text-2xl">
Building Leaders.
</h3>
<h3 className="text-[#D8DFE4] font-lato font-normal text-center text-2xl">
Winning Campaigns.
</h3>
</div>
<div className="flex items-center gap-2 border ">
<div className="h-2 w-2 rounded-full bg-[#7B2E45]"></div>
<hr className="text-[#7B2E45]/50 w-32" />
</div>
</section>
<section className="">
<div className="relative inline-block">
<button
className="
>
Agency
</h1>
</div>
</section>
<section className="flex flex-col items-center h-fit space-y-6">
<div className="flex flex-col lg:md:flex-row lg:md:gap-2">
<h3 className="text-[#D8DFE4] font-lato font-normal text-center text-2xl">
Shaping Narratives.
</h3>
<h3 className="text-[#D8DFE4] font-lato font-normal text-center text-2xl">
Building Leaders.
</h3>
<h3 className="text-[#D8DFE4] font-lato font-normal text-center text-2xl">
Winning Campaigns.
</h3>
</div>
<div className="flex items-center gap-2 ">
<div className="h-2 w-2 rounded-full bg-[#7B2E45]"></div>
<hr className="text-[#7B2E45]/50 w-32" />
</div>
</section>
<section className="">
<div className="relative inline-block">
<button
className="
relative px-8 py-3 rounded-xl
bg-linear-to-b from-[#7B2E45] to-[#9B3E55]
hover:from-[#913651] hover:to-[#b34762]
"
>
{/* diamonds */}
{/* <span className="absolute -top-2 -left-30 rotate-45 w-20 h-20 border border-[#7B2E45]/30"></span>
>
{/* diamonds */}
{/* <span className="absolute -top-2 -left-30 rotate-45 w-20 h-20 border border-[#7B2E45]/30"></span>
<span className="absolute -top-2 -right-30 rotate-45 w-20 h-20 border border-[#7B2E45]/30"></span> */}
<span className="text-white font-medium font-lato text-xl">
Start Your Campaign
</span>
</button>
<span className="text-white font-medium font-lato text-xl">
Start Your Campaign
</span>
</button>
{/* corner borders OUTSIDE the clipped button */}
<span className="absolute -left-4 -top-4 w-6 h-6 border-t border-l border-[#7B2E45]" />
<span className="absolute -right-4 -top-4 w-6 h-6 border-t border-r border-[#7B2E45]" />
<span className="absolute -left-4 -bottom-4 w-6 h-6 border-b border-l border-[#7B2E45]" />
<span className="absolute -right-4 -bottom-4 w-6 h-6 border-b border-r border-[#7B2E45]" />
</div>
</section>
<section className="flex flex-col lg:md:flex-row gap-10">
<section className="flex items-center gap-4">
<div className="flex gap-1 px-2">
<div className="w-3 h-16 bg-[#7B2E45]"></div>
<div className="w-3 h-12 bg-[#7B2E45]/50"></div>
<div className="w-3 h-8 bg-[#7B2E45]/30"></div>
</div>
<div className="flex flex-col ">
<h4 className="text-3xl text-[#7B2E45] font-lato">500+</h4>
<h5 className="text-xl font-lato font-light text-white/50">
CAMPAIGNS
</h5>
{/* corner borders OUTSIDE the clipped button */}
<span className="absolute -left-4 -top-4 w-6 h-6 border-t border-l border-[#7B2E45]" />
<span className="absolute -right-4 -top-4 w-6 h-6 border-t border-r border-[#7B2E45]" />
<span className="absolute -left-4 -bottom-4 w-6 h-6 border-b border-l border-[#7B2E45]" />
<span className="absolute -right-4 -bottom-4 w-6 h-6 border-b border-r border-[#7B2E45]" />
</div>
</section>
<section className="flex items-center gap-4">
<div className="flex gap-1 px-2">
<div className="w-3 h-16 bg-[#7B2E45]"></div>
<div className="w-3 h-12 bg-[#7B2E45]/50"></div>
<div className="w-3 h-8 bg-[#7B2E45]/30"></div>
</div>
<div className="flex flex-col ">
<h4 className="text-3xl text-[#7B2E45] font-lato">98%+</h4>
<h5 className="text-xl font-lato font-light text-white/50">
SUCCESS RATE
</h5>
</div>
</section>
<section className="flex items-center gap-4">
<div className="flex gap-1 px-2">
<div className="w-3 h-16 bg-[#7B2E45]"></div>
<div className="w-3 h-12 bg-[#7B2E45]/50"></div>
<div className="w-3 h-8 bg-[#7B2E45]/30"></div>
</div>
<div className="flex flex-col ">
<h4 className="text-3xl text-[#7B2E45] font-lato">15+</h4>
<h5 className="text-xl font-lato font-light text-white/50">
YEARS
</h5>
</div>
<section className="flex flex-col lg:md:flex-row gap-10">
<section className="flex items-center gap-4">
<div className="flex gap-1 px-2">
<div className="w-3 h-16 bg-[#7B2E45]"></div>
<div className="w-3 h-12 bg-[#7B2E45]/50"></div>
<div className="w-3 h-8 bg-[#7B2E45]/30"></div>
</div>
<div className="flex flex-col ">
<h4 className="text-3xl text-[#7B2E45] font-lato">500+</h4>
<h5 className="text-xl font-lato font-light text-white/50">
CAMPAIGNS
</h5>
</div>
</section>
<section className="flex items-center gap-4">
<div className="flex gap-1 px-2">
<div className="w-3 h-16 bg-[#7B2E45]"></div>
<div className="w-3 h-12 bg-[#7B2E45]/50"></div>
<div className="w-3 h-8 bg-[#7B2E45]/30"></div>
</div>
<div className="flex flex-col ">
<h4 className="text-3xl text-[#7B2E45] font-lato">98%+</h4>
<h5 className="text-xl font-lato font-light text-white/50">
SUCCESS RATE
</h5>
</div>
</section>
<section className="flex items-center gap-4">
<div className="flex gap-1 px-2">
<div className="w-3 h-16 bg-[#7B2E45]"></div>
<div className="w-3 h-12 bg-[#7B2E45]/50"></div>
<div className="w-3 h-8 bg-[#7B2E45]/30"></div>
</div>
<div className="flex flex-col ">
<h4 className="text-3xl text-[#7B2E45] font-lato">15+</h4>
<h5 className="text-xl font-lato font-light text-white/50">
YEARS
</h5>
</div>
</section>
</section>
</section>
<section className="px-10 space-y-8">
@ -149,7 +246,7 @@ export default function Home() {
</span>
</section>
</section>
<section className="min-h-screen flex flex-col items-center justify-center space-y-6">
<section className="min-h-screen px-5 flex flex-col items-center justify-center space-y-6">
<div className="aspect-square overflow-hidden w-.20 h-20">
<Image
src={"/icons/quotation.png"}
@ -170,7 +267,7 @@ export default function Home() {
OUR MISSION
</h4>
</section>
<section className="px-10">
<section className="min-h-screen px-10 space-y-4 ">
<h2 className="font-lato text-xl text-[#7B2E45] tracking-[0.5em]">
WHAT WE DO
</h2>
@ -180,7 +277,90 @@ export default function Home() {
Transform
</span>
</h2>
<section></section>
<h5 className="font-lato text-white/50 text-lg">
Strategic excellence across every dimension of modern PR
</h5>
<section className="flex flex-col items-start space-y-7">
<Carousel className="w-full h-fit ">
<CarouselContent>
{serviceCarousel.map(({ id, content }) => (
<CarouselItem key={id}>{content}</CarouselItem>
))}
</CarouselContent>
</Carousel>
<div className="flex gap-4 w-5/6">
<div className="bg-[#7B2E45]/80 rounded-full w-1/4 h-1" />
<div className="bg-[#7B2E45]/65 rounded-full w-1/5 h-1" />
<div className="bg-[#7B2E45]/50 rounded-full w-1/6 h-1" />
<div className="bg-[#7B2E45]/35 rounded-full w-1/7 h-1" />
<div className="bg-[#7B2E45]/20 rounded-full w-1/8 h-1" />
</div>
</section>
</section>
<section className="min-h-screen px-10 space-y-4">
<h2 className="font-lato text-xl text-[#7B2E45] tracking-[0.5em]">
OUR APPROACH
</h2>
<h2 className="font-lato text-6xl text-white">
Strategic{" "}
<span className="bg-linear-to-b from-[#7B2E45] to-[#9B3E55] bg-clip-text text-transparent leading-16">
Content
</span>
</h2>
<section className="space-y-8">
<div className="bg-black p-8 space-y-2">
<h2 className="font-lato text-md text-[#7B2E45] tracking-wide">
PROTECT YOUR REPUTATION
</h2>
<h2 className="font-lato text-2xl text-white">Crisis Management</h2>
<ul className="font-lato text-white/60 list-disc pl-4 space-y-3">
<li>24/7 Response Team</li>
<li>Media Relations</li>
<li>Damage Control</li>
<li>Recovery Strategy</li>
</ul>
</div>
<div className="bg-black p-8 space-y-2">
<h2 className="font-lato text-md text-[#7B2E45] tracking-wide">
DEFINE YOUR IDENTITY
</h2>
<h2 className="font-lato text-2xl text-white">Brand Positioning</h2>
<ul className="font-lato text-white/60 list-disc pl-4 space-y-3">
<li>Market Research</li>
<li>Competitive Analysis</li>
<li>Messaging Framework</li>
<li>Brand Guidelines</li>
</ul>
</div>
<div className="bg-black p-8 space-y-2">
<h2 className="font-lato text-md text-[#7B2E45] tracking-wide">
ENGAGE YOUR AUDIENCE
</h2>
<h2 className="font-lato text-2xl text-white">Digital Campaigns</h2>
<ul className="font-lato text-white/60 list-disc pl-4 space-y-3">
<li>Social Media Strategy</li>
<li>Content Calendar</li>
<li>Influencer Partnerships</li>
<li>Performance Analystics</li>
</ul>
</div>
</section>
</section>
<section className="min-h-screen px-10 flex flex-col items-center justify-center space-y-8">
<h2 className="font-lato text-xl text-[#7B2E45] tracking-[0.5em] text-center">
MEDIA PRODUCTION
</h2>
<h2 className="font-lato text-6xl text-white text-center">
Visual{" "}
<span className="bg-linear-to-b from-[#7B2E45] to-[#9B3E55] bg-clip-text text-transparent leading-16">
Storytelling{" "}
</span>
That Moves
</h2>
<p className="font-lato text-2xl text-white/80 text-center">
From campaign ads to documentary-style content, we produce visual
narratives that captivate audiences and drive results.
</p>
</section>
</main>
);

22
components.json Normal file
View File

@ -0,0 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}

View File

@ -1,23 +0,0 @@
"use client";
export default function CardCarousel() {
const cards = [
{ id: 1, title: "Card 1" },
{ id: 2, title: "Card 2" },
{ id: 3, title: "Card 3" },
{ id: 4, title: "Card 4" },
];
return (
<div className="w-full overflow-x-scroll flex gap-4 snap-x snap-mandatory scroll-smooth no-scrollbar px-4 py-6">
{cards.map((card) => (
<div
key={card.id}
className="snap-center min-w-[250px] h-[180px] rounded-xl bg-gray-800 text-white flex items-center justify-center text-xl font-semibold"
>
{card.title}
</div>
))}
</div>
);
}

60
components/ui/button.tsx Normal file
View File

@ -0,0 +1,60 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"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",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

241
components/ui/carousel.tsx Normal file
View File

@ -0,0 +1,241 @@
"use client"
import * as React from "react"
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react"
import { ArrowLeft, ArrowRight } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
type CarouselProps = {
opts?: CarouselOptions
plugins?: CarouselPlugin
orientation?: "horizontal" | "vertical"
setApi?: (api: CarouselApi) => void
}
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
api: ReturnType<typeof useEmblaCarousel>[1]
scrollPrev: () => void
scrollNext: () => void
canScrollPrev: boolean
canScrollNext: boolean
} & CarouselProps
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
function useCarousel() {
const context = React.useContext(CarouselContext)
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />")
}
return context
}
function Carousel({
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
}: React.ComponentProps<"div"> & CarouselProps) {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins
)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) return
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext]
)
React.useEffect(() => {
if (!api || !setApi) return
setApi(api)
}, [api, setApi])
React.useEffect(() => {
if (!api) return
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => {
api?.off("select", onSelect)
}
}, [api, onSelect])
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
data-slot="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
)
}
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
const { carouselRef, orientation } = useCarousel()
return (
<div
ref={carouselRef}
className="overflow-hidden"
data-slot="carousel-content"
>
<div
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className
)}
{...props}
/>
</div>
)
}
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
const { orientation } = useCarousel()
return (
<div
role="group"
aria-roledescription="slide"
data-slot="carousel-item"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className
)}
{...props}
/>
)
}
function CarouselPrevious({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
<Button
data-slot="carousel-previous"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -left-12 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft />
<span className="sr-only">Previous slide</span>
</Button>
)
}
function CarouselNext({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
<Button
data-slot="carousel-next"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -right-12 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight />
<span className="sr-only">Next slide</span>
</Button>
)
}
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
}

6
lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@ -9,9 +9,15 @@
"lint": "eslint"
},
"dependencies": {
"@radix-ui/react-slot": "^1.2.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"embla-carousel-react": "^8.6.0",
"lucide-react": "^0.556.0",
"next": "16.0.6",
"react": "19.2.0",
"react-dom": "19.2.0"
"react-dom": "19.2.0",
"tailwind-merge": "^3.4.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@ -21,6 +27,7 @@
"eslint": "^9",
"eslint-config-next": "16.0.6",
"tailwindcss": "^4",
"tw-animate-css": "^1.4.0",
"typescript": "^5"
}
}

109
pnpm-lock.yaml generated
View File

@ -8,6 +8,21 @@ importers:
.:
dependencies:
'@radix-ui/react-slot':
specifier: ^1.2.4
version: 1.2.4(@types/react@19.2.7)(react@19.2.0)
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
clsx:
specifier: ^2.1.1
version: 2.1.1
embla-carousel-react:
specifier: ^8.6.0
version: 8.6.0(react@19.2.0)
lucide-react:
specifier: ^0.556.0
version: 0.556.0(react@19.2.0)
next:
specifier: 16.0.6
version: 16.0.6(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@ -17,6 +32,9 @@ importers:
react-dom:
specifier: 19.2.0
version: 19.2.0(react@19.2.0)
tailwind-merge:
specifier: ^3.4.0
version: 3.4.0
devDependencies:
'@tailwindcss/postcss':
specifier: ^4
@ -39,6 +57,9 @@ importers:
tailwindcss:
specifier: ^4
version: 4.1.17
tw-animate-css:
specifier: ^1.4.0
version: 1.4.0
typescript:
specifier: ^5
version: 5.9.3
@ -405,6 +426,24 @@ packages:
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
engines: {node: '>=12.4.0'}
'@radix-ui/react-compose-refs@1.1.2':
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-slot@1.2.4':
resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
@ -796,9 +835,16 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@ -877,6 +923,19 @@ packages:
electron-to-chromium@1.5.263:
resolution: {integrity: sha512-DrqJ11Knd+lo+dv+lltvfMDLU27g14LMdH2b0O3Pio4uk0x+z7OR+JrmyacTPN2M8w3BrZ7/RTwG3R9B7irPlg==}
embla-carousel-react@8.6.0:
resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==}
peerDependencies:
react: ^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
embla-carousel-reactive-utils@8.6.0:
resolution: {integrity: sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==}
peerDependencies:
embla-carousel: 8.6.0
embla-carousel@8.6.0:
resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==}
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
@ -1453,6 +1512,11 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
lucide-react@0.556.0:
resolution: {integrity: sha512-iOb8dRk7kLaYBZhR2VlV1CeJGxChBgUthpSP8wom9jfj79qovgG6qcSdiy6vkoREKPnbUYzJsCn4o4PtG3Iy+A==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@ -1788,6 +1852,9 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
tailwind-merge@3.4.0:
resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==}
tailwindcss@4.1.17:
resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==}
@ -1815,6 +1882,9 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tw-animate-css@1.4.0:
resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@ -2251,6 +2321,19 @@ snapshots:
'@nolyfill/is-core-module@1.0.39': {}
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.7)(react@19.2.0)':
dependencies:
react: 19.2.0
optionalDependencies:
'@types/react': 19.2.7
'@radix-ui/react-slot@1.2.4(@types/react@19.2.7)(react@19.2.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0)
react: 19.2.0
optionalDependencies:
'@types/react': 19.2.7
'@rtsao/scc@1.1.0': {}
'@swc/helpers@0.5.15':
@ -2651,8 +2734,14 @@ snapshots:
ansi-styles: 4.3.0
supports-color: 7.2.0
class-variance-authority@0.7.1:
dependencies:
clsx: 2.1.1
client-only@0.0.1: {}
clsx@2.1.1: {}
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@ -2727,6 +2816,18 @@ snapshots:
electron-to-chromium@1.5.263: {}
embla-carousel-react@8.6.0(react@19.2.0):
dependencies:
embla-carousel: 8.6.0
embla-carousel-reactive-utils: 8.6.0(embla-carousel@8.6.0)
react: 19.2.0
embla-carousel-reactive-utils@8.6.0(embla-carousel@8.6.0):
dependencies:
embla-carousel: 8.6.0
embla-carousel@8.6.0: {}
emoji-regex@9.2.2: {}
enhanced-resolve@5.18.3:
@ -3433,6 +3534,10 @@ snapshots:
dependencies:
yallist: 3.1.1
lucide-react@0.556.0(react@19.2.0):
dependencies:
react: 19.2.0
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@ -3836,6 +3941,8 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
tailwind-merge@3.4.0: {}
tailwindcss@4.1.17: {}
tapable@2.3.0: {}
@ -3862,6 +3969,8 @@ snapshots:
tslib@2.8.1: {}
tw-animate-css@1.4.0: {}
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

BIN
public/images/research.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB