Compare commits

..

14 Commits

Author SHA1 Message Date
72f13c7c2e ♻️ refactor: restructure hero swiper to be media-specific 2026-03-25 19:44:54 +07:00
f9104c2580 Merge pull request 'feat/recommendation' (#10) from feat/recommendation into main
All checks were successful
Sync to GitHub / sync (push) Successful in 11s
Reviewed-on: #10
2026-03-15 22:36:54 +07:00
eecaeb13e8 🚨 fix: resolve linting type error
All checks were successful
Integration Tests / integration-tests (pull_request) Successful in 1m44s
2026-03-15 22:32:21 +07:00
74ad82c4f0 feat: add scroll button and card skeleton loading 2026-03-15 22:23:41 +07:00
97ef74e0f7 💄 style: add scroll button UI 2026-03-15 21:25:53 +07:00
5cb3b909be 👔 feat: add title and additional attributes to card 2026-03-15 21:11:20 +07:00
8393e6393c 🚧 wip: add rating to card 2026-03-14 12:00:00 +07:00
c02832674b 🚧 wip: add image support to card 2026-03-13 12:00:00 +07:00
e3211d240a 🚧 wip: add recommendation component 2026-03-12 12:00:00 +07:00
01a15210ea Merge pull request 'feat/hero' (#9) from feat/hero into main
All checks were successful
Sync to GitHub / sync (push) Successful in 8s
Reviewed-on: #9
2026-03-03 14:01:42 +07:00
29f2d3fa59 🔧 chore: replace dummy data with real data
All checks were successful
Integration Tests / integration-tests (pull_request) Successful in 1m4s
2026-03-03 13:58:11 +07:00
2f9fef54ff 💄 style: adjust UI for image-only banner 2026-03-01 21:55:24 +07:00
119e0f447c feat: add base banner elements 2026-03-01 21:40:14 +07:00
24ec3588d5 🚧 wip: add base hero section component 2026-02-28 14:33:39 +07:00
23 changed files with 690 additions and 22 deletions

View File

@ -3,7 +3,7 @@ import React from "react";
const layout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
return (
<div className="max-w-400 mx-auto relative">
<div className="max-w-396 mx-auto relative">
<Navbar />
<div className="pt-16">{children}</div>
</div>

View File

@ -124,3 +124,76 @@
@apply bg-background text-foreground;
}
}
/* ===== Scrollbar CSS ===== */
/* Firefox */
* {
scrollbar-width: auto;
scrollbar-color: #4a4a4a #0a0a0a;
}
/* Chrome, Edge, and Safari */
*::-webkit-scrollbar {
width: 12px;
}
*::-webkit-scrollbar-track {
background: #0a0a0a;
}
*::-webkit-scrollbar-thumb {
background-color: #4a4a4a;
border-radius: 9px;
border: 3px none #000000;
}
@keyframes aircraft-strobe {
/* Kedipan 1: Agak lambat/lama */
0%,
20% {
opacity: 1;
}
/* Jeda singkat */
35% {
opacity: 0;
}
/* Kedipan 2: Cepat */
40% {
opacity: 1;
}
43% {
opacity: 0;
}
/* Jeda sangat singkat */
48% {
opacity: 0;
}
/* Kedipan 3: Cepat */
53% {
opacity: 1;
}
56% {
opacity: 0;
}
/* Jeda panjang sebelum mengulang loop (gelap) */
100% {
opacity: 1;
}
}
/* Class untuk diterapkan ke elemen */
.blink-strobe {
animation: aircraft-strobe 2s linear infinite;
}
.hide-scrollbar {
scrollbar-width: none; /* Firefox */
}
.hide-scrollbar::-webkit-scrollbar {
display: none; /* Chrome, Edge, Safari */
}

View File

@ -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=="],

View File

@ -5,6 +5,13 @@ import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
{
rules: {
// Disable the rule that enforces the use of `next/image` for image optimization.
"@next/next/no-img-element": "off",
},
},
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:

View File

@ -0,0 +1,134 @@
"use server";
export type RecommendationAnime = {
title: string;
rating?: number;
type: string;
status: string;
episodes: number;
release_year: string;
thumbnail_url: string;
};
export const getRecommendationAnimeAction = async (): Promise<
RecommendationAnime[]
> => {
await new Promise((resolve) => setTimeout(resolve, 2000));
return [
{
title: "Frieren: Beyond Journey's End",
rating: 9.39,
type: "TV",
status: "finished",
episodes: 28,
release_year: "2023",
thumbnail_url: "https://m.media-amazon.com/images/I/816AbVQc+0L.jpg",
},
{
title: "Steins;Gate",
rating: 9.07,
type: "TV",
status: "finished",
episodes: 24,
release_year: "2011",
thumbnail_url:
"https://m.media-amazon.com/images/M/MV5BZjI1YjZiMDUtZTI3MC00YTA5LWIzMmMtZmQ0NTZiYWM4NTYwXkEyXkFqcGc@._V1_FMjpg_UX1000_.jpg",
},
{
title: "Spirited Away",
rating: 8.78,
type: "Movie",
status: "finished",
episodes: 1,
release_year: "2001",
thumbnail_url:
"https://printedoriginals.com/cdn/shop/products/spirited-away-french-143975.jpg?v=1602427397",
},
{
title: "One Piece",
rating: 8.72,
type: "TV",
status: "airing",
episodes: 1100,
release_year: "1999",
thumbnail_url: "https://myanimelist.net/images/anime/1244/138851.jpg",
},
{
title: "Cyberpunk: Edgerunners",
rating: 8.6,
type: "ONA",
status: "finished",
episodes: 10,
release_year: "2022",
thumbnail_url:
"https://myanimelist.net/images/about_me/ranking_items/14292440-859e4272-536e-4760-845f-78fb48eccafe.jpg?t=1767555420",
},
{
title: "Your Name",
rating: 8.85,
type: "Movie",
status: "finished",
episodes: 1,
release_year: "2016",
thumbnail_url:
"https://m.media-amazon.com/images/M/MV5BMjM4YTE3OGEtYTY1OS00ZWEzLTg1OTctMTkyODA0ZDM3ZmJlXkEyXkFqcGc@._V1_.jpg",
},
{
title: "Hunter x Hunter (2011)",
rating: 9.04,
type: "TV",
status: "finished",
episodes: 148,
release_year: "2011",
thumbnail_url: "https://myanimelist.net/images/anime/1337/99013.jpg",
},
{
title: "Hellsing Ultimate",
rating: 8.36,
type: "OVA",
status: "finished",
episodes: 10,
release_year: "2006",
thumbnail_url: "https://cdn.myanimelist.net/images/anime/6/7333l.jpg",
},
{
title: "Tower of God Season 2",
rating: 7.5,
type: "TV",
status: "airing",
episodes: 12,
release_year: "2024",
thumbnail_url:
"https://www.animationmagazine.net/wordpress/wp-content/uploads/TOG2_ENLOGO_v2.jpg",
},
{
title: "Violet Evergarden: The Movie",
rating: 8.89,
type: "Movie",
status: "finished",
episodes: 1,
release_year: "2020",
thumbnail_url: "https://myanimelist.net/images/anime/1614/106512l.jpg",
},
{
title: "Devilman Crybaby",
rating: 7.75,
type: "ONA",
status: "finished",
episodes: 10,
release_year: "2018",
thumbnail_url: "https://myanimelist.net/images/anime/1046/122722.jpg",
},
{
title:
"Mobile Suit Gundam: The Origin (lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua)",
rating: 8.42,
type: "OVA",
status: "finished",
episodes: 6,
release_year: "2015",
thumbnail_url: "https://myanimelist.net/images/anime/4/72702.jpg",
},
];
};

View File

@ -1,7 +1,13 @@
"use client";
import Hero from "./sections/Hero/wrapper";
import Recommendation from "./sections/Recommendation/wrapper";
const HomeIndex = () => {
return <div className="text-center w-full">HomePage</div>;
return (
<div className="w-full pt-4 pb-12">
<Hero />
<Recommendation />
</div>
);
};
export default HomeIndex;

View File

@ -0,0 +1,90 @@
"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";
import { Icon } from "@iconify/react";
export interface HeroSwiperProps {
data: {
id: string;
title: string;
slug: string;
imageUrl: string;
synopsis: string;
genres: {
slug: string;
name: string;
}[];
}[];
}
const HeroSwiper = (props: HeroSwiperProps) => {
const router = useRouter();
return (
<div className="h-full rounded-lg overflow-hidden">
<Swiper
spaceBetween={0}
slidesPerView={1}
onSlideChange={() => 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) => (
<SwiperSlide key={slide.id} className="relative overflow-hidden">
<img
src={slide.imageUrl}
alt={slide.title}
className="absolute top-0 left-0 z-0 object-cover w-full h-full opacity-80"
/>
<div
className="absolute top-0 left-0 z-10 h-full w-full py-16 px-20"
style={{
background:
"linear-gradient(90deg,rgba(0, 0, 0, 0.64) 0%, rgba(0, 0, 0, 0.42) 46%, rgba(0, 0, 0, 0) 100%)",
}}
>
<h1 className="text-6xl font-semibold tracking-tight">
{slide.title}
</h1>
<div className="mt-4 flex gap-1.5">
{slide.genres.map((genre) => (
<Badge
className="bg-neutral-200 text-neutral-800"
key={genre.slug}
>
{genre.name}
</Badge>
))}
</div>
<p className="mt-4 font-medium text-base max-w-[40vw] line-clamp-6">
{slide.synopsis}
</p>
<Button
size="lg"
onClick={() => router.push(`/media/${slide.slug}`)}
className="mt-6 h-12 rounded-xl flex gap-2 px-4 hover:bg-neutral-950 group"
>
<div className="bg-neutral-950 p-2 rounded-full group-hover:bg-primary">
<Icon
icon="solar:play-bold"
className="text-primary group-hover:text-neutral-950"
/>
</div>
<span className="font-semibold text-neutral-950 group-hover:text-primary">
Watch Now
</span>
</Button>
</div>
</SwiperSlide>
))}
</Swiper>
</div>
);
};
export default HeroSwiper;

View File

@ -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 <div></div>;
return <HeroSwiper data={response.data} />;
};
export default HeroMain;

View File

@ -0,0 +1,8 @@
import { Skeleton } from "@/shared/libs/shadcn/ui/skeleton";
import React from "react";
const HeroSkeleton = () => {
return <Skeleton className="w-full h-full" />;
};
export default HeroSkeleton;

View File

@ -0,0 +1,15 @@
import { Suspense } from "react";
import HeroSkeleton from "./skeleton";
import HeroMain from "./main";
const Hero = () => {
return (
<div className="h-120 w-full">
<Suspense fallback={<HeroSkeleton />}>
<HeroMain />
</Suspense>
</div>
);
};
export default Hero;

View File

@ -0,0 +1,40 @@
import { RecommendationAnime } from "@/features/home/actions/getRecommenationAnime";
import { Icon } from "@iconify/react";
const AnimeRecommendationCard = ({ data }: { data: RecommendationAnime }) => {
return (
<div>
<div className="w-64 h-88 rounded-lg overflow-hidden relative">
{data.status === "airing" && (
<div className="absolute top-2 left-2 bg-neutral-800 flex items-center gap-0.5 px-2 py-1 rounded-full">
<Icon
icon="icon-park-outline:dot"
className="h-auto w-4 text-red-500 blink-strobe"
/>
<span className="text-xs font-medium">Airing</span>
</div>
)}
<div className="absolute right-2 top-2 bg-amber-400 text-neutral-950 flex items-center py-1 px-1.5 rounded">
<Icon icon="material-symbols:star-rounded" className="h-auto w-4" />
<span className="text-xs tracking-tight font-medium">
{data.rating ?? "N/A"}
</span>
</div>
<img
className="w-full h-full object-cover"
src={data.thumbnail_url}
alt={data.title}
draggable={false}
/>
</div>
<div className="mt-3 px-1 mb-1">
<h3 className=" font-semibold mt-1 line-clamp-1">{data.title}</h3>
<div className="flex gap-2 text-sm text-neutral-400 mt-0.5">
<span>{data.release_year}</span>
</div>
</div>
</div>
);
};
export default AnimeRecommendationCard;

View File

@ -0,0 +1,26 @@
import { Button } from "@/shared/libs/shadcn/ui/button";
import { ButtonGroup } from "@/shared/libs/shadcn/ui/button-group";
import { ArrowLeft, ArrowRight } from "lucide-react";
const ScrollingButton = ({
scrollLeft,
scrollRight,
}: {
scrollLeft: () => void;
scrollRight: () => void;
}) => {
return (
<div>
<ButtonGroup>
<Button variant="outline" onClick={scrollLeft}>
<ArrowLeft />
</Button>
<Button variant="outline" onClick={scrollRight}>
<ArrowRight />
</Button>
</ButtonGroup>
</div>
);
};
export default ScrollingButton;

View File

@ -0,0 +1,45 @@
"use client";
import { useRef } from "react";
import { RecommendationAnime } from "../../actions/getRecommenationAnime";
import AnimeRecommendationCard from "./components/Card";
import ScrollingButton from "./components/ScrollingButton";
const RecommendationClient = ({
result,
}: {
result: RecommendationAnime[];
}) => {
const scrollingContainer = useRef<HTMLDivElement | null>(null);
const scrollLeft = () => {
console.log("scroll left");
if (scrollingContainer.current) {
scrollingContainer.current.scrollBy({ left: -788, behavior: "smooth" });
}
};
const scrollRight = () => {
console.log("scroll right");
if (scrollingContainer.current) {
scrollingContainer.current.scrollBy({ left: 788, behavior: "smooth" });
}
};
return (
<div>
<div className="absolute top-0 right-0">
<ScrollingButton scrollLeft={scrollLeft} scrollRight={scrollRight} />
</div>
<div
className="flex gap-2 w-full overflow-x-scroll py-2 mt-2 hide-scrollbar relative"
ref={scrollingContainer}
>
{result.map((item, index) => (
<AnimeRecommendationCard data={item} key={index} />
))}
</div>
</div>
);
};
export default RecommendationClient;

View File

@ -0,0 +1,10 @@
import { getRecommendationAnimeAction } from "../../actions/getRecommenationAnime";
import RecommendationClient from "./main.client";
const RecommendationMain = async () => {
const data = async () => await getRecommendationAnimeAction();
const result = await data();
return <RecommendationClient result={result} />;
};
export default RecommendationMain;

View File

@ -0,0 +1,19 @@
import { Skeleton } from "@/shared/libs/shadcn/ui/skeleton";
const RecommendationSkeleton = () => {
const skeletonLenght = 6;
return (
<div className="flex gap-2 w-full overflow-hidden mt-4">
{[...Array(skeletonLenght)].map((_, index) => (
<div key={index}>
<Skeleton className="h-88 w-64" />
<Skeleton className="mt-3 h-6 w-64 rounded-full" />
<Skeleton className="mt-1 h-4 w-12 rounded-full" />
</div>
))}
</div>
);
};
export default RecommendationSkeleton;

View File

@ -0,0 +1,20 @@
import { Suspense } from "react";
import RecommendationMain from "./main";
import RecommendationSkeleton from "./skeleton";
const Recommendation = async () => {
return (
<div className="mt-12 relative">
<div className="flex justify-between">
<h1 className="text-[26px] text-neutral-100 font-semibold w-fit tracking-tight">
Maybe You Like
</h1>
</div>
<Suspense fallback={<RecommendationSkeleton />}>
<RecommendationMain />
</Suspense>
</div>
);
};
export default Recommendation;

View File

@ -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"

View File

@ -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<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "span"
return (
<Comp
data-slot="badge"
data-variant={variant}
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

View File

@ -0,0 +1,83 @@
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/shared/libs/shadcn/lib/utils"
import { Separator } from "@/shared/libs/shadcn/ui/separator"
const buttonGroupVariants = cva(
"flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
{
variants: {
orientation: {
horizontal:
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-md!",
vertical:
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-md!",
},
},
defaultVariants: {
orientation: "horizontal",
},
}
)
function ButtonGroup({
className,
orientation,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
return (
<div
role="group"
data-slot="button-group"
data-orientation={orientation}
className={cn(buttonGroupVariants({ orientation }), className)}
{...props}
/>
)
}
function ButtonGroupText({
className,
asChild = false,
...props
}: React.ComponentProps<"div"> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot.Root : "div"
return (
<Comp
className={cn(
"flex items-center gap-2 rounded-md border bg-muted px-2.5 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function ButtonGroupSeparator({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="button-group-separator"
orientation={orientation}
className={cn(
"relative self-stretch bg-input data-horizontal:mx-px data-horizontal:w-auto data-vertical:my-px data-vertical:h-auto",
className
)}
{...props}
/>
)
}
export {
ButtonGroup,
ButtonGroupSeparator,
ButtonGroupText,
buttonGroupVariants,
}

View File

@ -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<typeof buttonVariants> & {
asChild?: boolean
asChild?: boolean;
}) {
const Comp = asChild ? Slot.Root : "button"
const Comp = asChild ? Slot.Root : "button";
return (
<Comp
@ -54,7 +61,7 @@ function Button({
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
);
}
export { Button, buttonVariants }
export { Button, buttonVariants };

View File

@ -17,7 +17,7 @@ function Separator({
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
className
)}
{...props}

View File

@ -0,0 +1,13 @@
import { cn } from "@/shared/libs/shadcn/lib/utils"
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}
export { Skeleton }

2
shared/types/swiper.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
declare module "swiper/css";
declare module "swiper/css/*";