diff --git a/app/(session)/(main)/layout.tsx b/app/(session)/(main)/layout.tsx index fb2280e..5b83505 100644 --- a/app/(session)/(main)/layout.tsx +++ b/app/(session)/(main)/layout.tsx @@ -3,7 +3,7 @@ import React from "react"; const layout = ({ children }: Readonly<{ children: React.ReactNode }>) => { return ( -
+
{children}
diff --git a/app/globals.css b/app/globals.css index f4040d4..4998592 100644 --- a/app/globals.css +++ b/app/globals.css @@ -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 */ +} diff --git a/eslint.config.mjs b/eslint.config.mjs index 05e726d..e764709 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -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: diff --git a/features/home/actions/getRecommenationAnime.ts b/features/home/actions/getRecommenationAnime.ts new file mode 100644 index 0000000..c69cee6 --- /dev/null +++ b/features/home/actions/getRecommenationAnime.ts @@ -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", + }, + ]; +}; diff --git a/features/home/index.tsx b/features/home/index.tsx index 2de1d38..baea98a 100644 --- a/features/home/index.tsx +++ b/features/home/index.tsx @@ -1,9 +1,11 @@ import Hero from "./sections/Hero/wrapper"; +import Recommendation from "./sections/Recommendation/wrapper"; const HomeIndex = () => { return ( -
+
+
); }; diff --git a/features/home/sections/Recommendation/components/Card.tsx b/features/home/sections/Recommendation/components/Card.tsx new file mode 100644 index 0000000..d4662c4 --- /dev/null +++ b/features/home/sections/Recommendation/components/Card.tsx @@ -0,0 +1,40 @@ +import { RecommendationAnime } from "@/features/home/actions/getRecommenationAnime"; +import { Icon } from "@iconify/react"; + +const AnimeRecommendationCard = ({ data }: { data: RecommendationAnime }) => { + return ( +
+
+ {data.status === "airing" && ( +
+ + Airing +
+ )} +
+ + + {data.rating ?? "N/A"} + +
+ {data.title} +
+
+

{data.title}

+
+ {data.release_year} +
+
+
+ ); +}; + +export default AnimeRecommendationCard; diff --git a/features/home/sections/Recommendation/components/ScrollingButton.tsx b/features/home/sections/Recommendation/components/ScrollingButton.tsx new file mode 100644 index 0000000..ad30a98 --- /dev/null +++ b/features/home/sections/Recommendation/components/ScrollingButton.tsx @@ -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 ( +
+ + + + +
+ ); +}; + +export default ScrollingButton; diff --git a/features/home/sections/Recommendation/main.client.tsx b/features/home/sections/Recommendation/main.client.tsx new file mode 100644 index 0000000..3fae04f --- /dev/null +++ b/features/home/sections/Recommendation/main.client.tsx @@ -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(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 ( +
+
+ +
+
+ {result.map((item, index) => ( + + ))} +
+
+ ); +}; + +export default RecommendationClient; diff --git a/features/home/sections/Recommendation/main.tsx b/features/home/sections/Recommendation/main.tsx new file mode 100644 index 0000000..e4d3229 --- /dev/null +++ b/features/home/sections/Recommendation/main.tsx @@ -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 ; +}; + +export default RecommendationMain; diff --git a/features/home/sections/Recommendation/skeleton.tsx b/features/home/sections/Recommendation/skeleton.tsx new file mode 100644 index 0000000..7b9ee76 --- /dev/null +++ b/features/home/sections/Recommendation/skeleton.tsx @@ -0,0 +1,19 @@ +import { Skeleton } from "@/shared/libs/shadcn/ui/skeleton"; + +const RecommendationSkeleton = () => { + const skeletonLenght = 6; + + return ( +
+ {[...Array(skeletonLenght)].map((_, index) => ( +
+ + + +
+ ))} +
+ ); +}; + +export default RecommendationSkeleton; diff --git a/features/home/sections/Recommendation/wrapper.tsx b/features/home/sections/Recommendation/wrapper.tsx new file mode 100644 index 0000000..e30071c --- /dev/null +++ b/features/home/sections/Recommendation/wrapper.tsx @@ -0,0 +1,20 @@ +import { Suspense } from "react"; +import RecommendationMain from "./main"; +import RecommendationSkeleton from "./skeleton"; + +const Recommendation = async () => { + return ( +
+
+

+ Maybe You Like +

+
+ }> + + +
+ ); +}; + +export default Recommendation; diff --git a/shared/libs/shadcn/ui/button-group.tsx b/shared/libs/shadcn/ui/button-group.tsx new file mode 100644 index 0000000..9a022ff --- /dev/null +++ b/shared/libs/shadcn/ui/button-group.tsx @@ -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) { + return ( +
+ ) +} + +function ButtonGroupText({ + className, + asChild = false, + ...props +}: React.ComponentProps<"div"> & { + asChild?: boolean +}) { + const Comp = asChild ? Slot.Root : "div" + + return ( + + ) +} + +function ButtonGroupSeparator({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + ButtonGroup, + ButtonGroupSeparator, + ButtonGroupText, + buttonGroupVariants, +} diff --git a/shared/libs/shadcn/ui/separator.tsx b/shared/libs/shadcn/ui/separator.tsx index 99a09e3..4d1d21f 100644 --- a/shared/libs/shadcn/ui/separator.tsx +++ b/shared/libs/shadcn/ui/separator.tsx @@ -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}