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 (
-
+
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.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}