diff --git a/bun.lock b/bun.lock index 38ffadb..1a58771 100644 --- a/bun.lock +++ b/bun.lock @@ -17,6 +17,7 @@ "react-dom": "19.2.3", "shadcn": "^3.6.3", "sonner": "^2.0.7", + "swiper": "^12.1.2", "tailwind-merge": "^3.4.0", "tw-animate-css": "^1.4.0", "ua-parser-js": "^2.0.8", @@ -1328,6 +1329,8 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "swiper": ["swiper@12.1.2", "", {}, "sha512-4gILrI3vXZqoZh71I1PALqukCFgk+gpOwe1tOvz5uE9kHtl2gTDzmYflYCwWvR4LOvCrJi6UEEU+gnuW5BtkgQ=="], + "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="], "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], diff --git a/features/home/index.tsx b/features/home/index.tsx index 65eb3c1..2de1d38 100644 --- a/features/home/index.tsx +++ b/features/home/index.tsx @@ -1,7 +1,11 @@ -"use client"; +import Hero from "./sections/Hero/wrapper"; const HomeIndex = () => { - return
HomePage
; + return ( +
+ +
+ ); }; export default HomeIndex; diff --git a/features/home/sections/Hero/components/Swiper.tsx b/features/home/sections/Hero/components/Swiper.tsx new file mode 100644 index 0000000..82239d8 --- /dev/null +++ b/features/home/sections/Hero/components/Swiper.tsx @@ -0,0 +1,118 @@ +"use client"; +import "swiper/css"; +import { Badge } from "@/shared/libs/shadcn/ui/badge"; +import { Button } from "@/shared/libs/shadcn/ui/button"; +import { useRouter } from "next/navigation"; +import { Autoplay, Navigation, Pagination } from "swiper/modules"; +import { Swiper, SwiperSlide } from "swiper/react"; + +export interface HeroSwiperProps { + data: { + id: string; + isClickable: boolean; + title: string; + tags: string[]; + description: string; + buttonContent: string; + buttonLink: string; + imageUrl: string; + startDate: string; + endDate: string; + }[]; +} + +const HeroSwiper = (props: HeroSwiperProps) => { + const router = useRouter(); + return ( +
+ console.log("slide change")} + onSwiper={(swiper) => console.log(swiper)} + className="h-full" + autoplay={{ delay: 5000, disableOnInteraction: false }} + modules={[Autoplay, Pagination, Navigation]} + > + {props.data.map((slide) => + slide.imageUrl ? ( + // Slide with image background + + {slide.title} + {slide.title && slide.description && ( +
+

+ {slide.title} +

+
+ {slide.tags.map((tag) => ( + + {tag} + + ))} +
+

+ {slide.description} +

+ {slide.isClickable && ( + + )} +
+ )} +
+ ) : ( + // Fallback for slides without image + +

+ {slide.title} +

+
+ {slide.tags.map((tag) => ( + + {tag} + + ))} +
+

+ {slide.description} +

+ {slide.isClickable && ( + + )} +
+ ), + )} +
+
+ ); +}; + +export default HeroSwiper; diff --git a/features/home/sections/Hero/main.tsx b/features/home/sections/Hero/main.tsx new file mode 100644 index 0000000..1261741 --- /dev/null +++ b/features/home/sections/Hero/main.tsx @@ -0,0 +1,17 @@ +import { backendFetch, BackendResponse } from "@/shared/helpers/backendFetch"; +import HeroSwiper, { HeroSwiperProps } from "./components/Swiper"; + +const HeroMain = async () => { + const testing = async () => { + return (await backendFetch("hero-banner")) as BackendResponse< + HeroSwiperProps["data"] + >; + }; + + const response = await testing(); + if (!response.data) return
; + + return ; +}; + +export default HeroMain; diff --git a/features/home/sections/Hero/skeleton.tsx b/features/home/sections/Hero/skeleton.tsx new file mode 100644 index 0000000..ad7825c --- /dev/null +++ b/features/home/sections/Hero/skeleton.tsx @@ -0,0 +1,8 @@ +import { Skeleton } from "@/shared/libs/shadcn/ui/skeleton"; +import React from "react"; + +const HeroSkeleton = () => { + return ; +}; + +export default HeroSkeleton; diff --git a/features/home/sections/Hero/wrapper.tsx b/features/home/sections/Hero/wrapper.tsx new file mode 100644 index 0000000..f8aae8a --- /dev/null +++ b/features/home/sections/Hero/wrapper.tsx @@ -0,0 +1,15 @@ +import { Suspense } from "react"; +import HeroSkeleton from "./skeleton"; +import HeroMain from "./main"; + +const Hero = () => { + return ( +
+ }> + + +
+ ); +}; + +export default Hero; diff --git a/package.json b/package.json index 74d3a25..016837d 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "react-dom": "19.2.3", "shadcn": "^3.6.3", "sonner": "^2.0.7", + "swiper": "^12.1.2", "tailwind-merge": "^3.4.0", "tw-animate-css": "^1.4.0", "ua-parser-js": "^2.0.8" diff --git a/shared/libs/shadcn/ui/badge.tsx b/shared/libs/shadcn/ui/badge.tsx new file mode 100644 index 0000000..6d11b09 --- /dev/null +++ b/shared/libs/shadcn/ui/badge.tsx @@ -0,0 +1,49 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/shared/libs/shadcn/lib/utils" + +const badgeVariants = cva( + "h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 [&>svg]:pointer-events-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 overflow-hidden group/badge", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", + secondary: + "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", + destructive: + "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20", + outline: + "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground", + ghost: + "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", + link: "text-primary underline-offset-4 hover:underline", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant = "default", + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot.Root : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/shared/libs/shadcn/ui/button.tsx b/shared/libs/shadcn/ui/button.tsx index 9704063..c38a3ba 100644 --- a/shared/libs/shadcn/ui/button.tsx +++ b/shared/libs/shadcn/ui/button.tsx @@ -1,29 +1,36 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" -import { Slot } from "radix-ui" +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { Slot } from "radix-ui"; -import { cn } from "@/shared/libs/shadcn/lib/utils" +import { cn } from "@/shared/libs/shadcn/lib/utils"; const buttonVariants = cva( - "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none", + "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 aria-invalid:ring-3 cursor-pointer [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none", { variants: { variant: { default: "bg-primary text-primary-foreground hover:bg-primary/80", - outline: "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground shadow-xs", - secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", - ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground", - destructive: "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30", + outline: + "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground shadow-xs", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", + ghost: + "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground", + destructive: + "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30", link: "text-primary underline-offset-4 hover:underline", }, size: { - default: "h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + default: + "h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5", lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3", icon: "size-9", - "icon-xs": "size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3", - "icon-sm": "size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md", + "icon-xs": + "size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3", + "icon-sm": + "size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md", "icon-lg": "size-10", }, }, @@ -31,8 +38,8 @@ const buttonVariants = cva( variant: "default", size: "default", }, - } -) + }, +); function Button({ className, @@ -42,9 +49,9 @@ function Button({ ...props }: React.ComponentProps<"button"> & VariantProps & { - asChild?: boolean + asChild?: boolean; }) { - const Comp = asChild ? Slot.Root : "button" + const Comp = asChild ? Slot.Root : "button"; return ( - ) + ); } -export { Button, buttonVariants } +export { Button, buttonVariants }; diff --git a/shared/libs/shadcn/ui/skeleton.tsx b/shared/libs/shadcn/ui/skeleton.tsx new file mode 100644 index 0000000..76f9fcd --- /dev/null +++ b/shared/libs/shadcn/ui/skeleton.tsx @@ -0,0 +1,13 @@ +import { cn } from "@/shared/libs/shadcn/lib/utils" + +function Skeleton({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Skeleton } diff --git a/shared/types/swiper.d.ts b/shared/types/swiper.d.ts new file mode 100644 index 0000000..6c975f1 --- /dev/null +++ b/shared/types/swiper.d.ts @@ -0,0 +1,2 @@ +declare module "swiper/css"; +declare module "swiper/css/*";