Skip to content

Commit

Permalink
Merge pull request #3 from alfaruqii/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
alfaruqii authored Nov 5, 2024
2 parents a0c3530 + f04ca76 commit 44e17a7
Show file tree
Hide file tree
Showing 85 changed files with 2,696 additions and 1,340 deletions.
56 changes: 56 additions & 0 deletions __mocks__/zustand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// __mocks__/zustand.ts
import * as zustand from "zustand";
import { act } from "@testing-library/react";

const { create: actualCreate, createStore: actualCreateStore } =
jest.requireActual<typeof zustand>("zustand");

// a variable to hold reset functions for all stores declared in the app
export const storeResetFns = new Set<() => void>();

const createUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {
const store = actualCreate(stateCreator);
const initialState = store.getInitialState();
storeResetFns.add(() => {
store.setState(initialState, true);
});
return store;
};

// when creating a store, we get its initial state, create a reset function and add it in the set
export const create = (<T>(stateCreator: zustand.StateCreator<T>) => {
console.log("zustand create mock");

// to support curried version of create
return typeof stateCreator === "function"
? createUncurried(stateCreator)
: createUncurried;
}) as typeof zustand.create;

const createStoreUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {
const store = actualCreateStore(stateCreator);
const initialState = store.getInitialState();
storeResetFns.add(() => {
store.setState(initialState, true);
});
return store;
};

// when creating a store, we get its initial state, create a reset function and add it in the set
export const createStore = (<T>(stateCreator: zustand.StateCreator<T>) => {
console.log("zustand createStore mock");

// to support curried version of createStore
return typeof stateCreator === "function"
? createStoreUncurried(stateCreator)
: createStoreUncurried;
}) as typeof zustand.createStore;

// reset all stores after each test run
afterEach(() => {
act(() => {
storeResetFns.forEach((resetFn) => {
resetFn();
});
});
});
49 changes: 31 additions & 18 deletions app/anime/detail/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import { Suspense } from 'react';
import { Suspense } from "react";

import { Banner } from '@/components/detail/banner/Banner';
import CardBanner from '@/components/detail/cardbanner/CardBanner';
import EpisodesContainer from '@/components/detail/episodes/EpisodesContainer';
import RelationComponent from '@/components/detail/relation/RelationComponent';
import InfoDetails from '@/components/detail/infodetails/InfoDetails';
import Trailer from '@/components/media/Trailer';
import SkeletonEpisodes from '@/components/skeleton/SkeletonEpisodes';
import Banner from "@/components/detail/banner/Banner";
import CardBanner from "@/components/detail/cardbanner/CardBanner";
import AnimeContainerCard from "@/components/card/animecard/AnimeContainerCard";
import EpisodesContainer from "@/components/detail/episodes/EpisodesContainer";
import RelationComponent from "@/components/detail/relation/RelationComponent";
import InfoDetails from "@/components/detail/infodetails/InfoDetails";
import Trailer from "@/components/media/Trailer";
import SkeletonEpisodes from "@/components/skeleton/SkeletonEpisodes";

import { AnimeServiceV2 } from '@/services';
import { AnimeInfo } from '@/types/anime.type';
import { AnimeServiceV2 } from "@/services";
import { AnimeInfo, RelationOrRecommendation } from "@/types/anime.type";

async function DetailPage({ params }: { params: { id: string } }) {
const { id } = params;
const { data: dataInfo }: { data: AnimeInfo } = await AnimeServiceV2.getAnimeInfoV2(id);
const { data: dataInfo }: { data: AnimeInfo } =
await AnimeServiceV2.getAnimeInfoV2(id);
const {
data: { results: dataRecommendations },
}: { data: { results: RelationOrRecommendation[] } } =
await AnimeServiceV2.getRecommendationAnime(id);

return (
<>
Expand All @@ -23,18 +29,25 @@ async function DetailPage({ params }: { params: { id: string } }) {
<CardBanner item={dataInfo} />
<InfoDetails item={dataInfo} />
<Trailer trailer={dataInfo.trailer} />
<Suspense
fallback={<SkeletonEpisodes />}>
{
(dataInfo.id_provider?.idGogo || dataInfo.id_provider?.idGogoDub) &&
<Suspense fallback={<SkeletonEpisodes />}>
{(dataInfo.id_provider?.idGogo ||
dataInfo.id_provider?.idGogoDub) && (
<EpisodesContainer {...dataInfo} />
}
)}
</Suspense>
<RelationComponent relation={dataInfo.relation} />
<div className="flex flex-col gap-4">
<RelationComponent relation={dataInfo.relation} />
{dataRecommendations.length > 0 && (
<AnimeContainerCard
animes={dataRecommendations}
containerTitle="Recommendations 👌"
/>
)}
</div>
</div>
</div>
</>
)
);
}

export default DetailPage;
16 changes: 10 additions & 6 deletions app/anime/page.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
import HeroMediaCarousel from "@/components/hero/HeroMediaCarousel";
import { AnimeContainerCard } from "../components/card/animecard/AnimeContainerCard";
import AnimeContainerCard from "../components/card/animecard/AnimeContainerCard";
import { AnimeServiceV1, AnimeServiceV2 } from "../services";

export default async function Home() {
const { data: dataTrending } = await AnimeServiceV2.getTrendingAnime();
const { data: dataRecent } = await AnimeServiceV1.getRecentEpisode();
const { data: dataRecent } = await AnimeServiceV1.getRecentEpisodeGogo();
const { data: dataPopular } = await AnimeServiceV2.getPopularAnime();

return (
<>
<div className="flex flex-col gap-4 min-h-fit">
<HeroMediaCarousel items={dataTrending.results} />
<div className="p-4 sm:p-0">
<AnimeContainerCard containerTitle="Recently Updated 🎬" animes={dataRecent.results} />
<AnimeContainerCard
containerTitle="Recently Updated 🎬"
animes={dataRecent.results}
/>
</div>
<div className="p-4 sm:p-0">
<AnimeContainerCard containerTitle="Most Popular 💯" animes={dataPopular.results} />
<AnimeContainerCard
containerTitle="Most Popular 💯"
animes={dataPopular.results}
/>
</div>
</div>
</>
);
}


32 changes: 0 additions & 32 deletions app/anime/watch/[id]/page.tsx

This file was deleted.

13 changes: 6 additions & 7 deletions app/anime/watch/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import SkeletonEpisodeNum from '@/components/skeleton/SkeletonEpisodeNum';
import SkeletonMediaPlayer from '@/components/skeleton/SkeletonMediaPlayer';
import SkeletonEpisodeNum from "@/components/skeleton/SkeletonEpisodeNum";
import SkeletonMediaPlayer from "@/components/skeleton/SkeletonMediaPlayer";

function loading() {
function Loading() {
return (
<>
<div className="flex flex-col gap-6 p-6 lg:flex-row">
<div className="flex flex-col gap-6 p-6 lg:grid lg:grid-cols-5">
<SkeletonMediaPlayer />
<SkeletonEpisodeNum />
</div>
</>
)
);
}

export default loading

export default Loading;
94 changes: 94 additions & 0 deletions app/anime/watch/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// WatchPage.tsx
"use client";
import useSWR from "swr";
import EpisodesComponent from "@/components/watch/episodes/EpisodesComponent";
import Media from "@/components/media/Media";
import Error from "@/error";
import Loading from "./loading";
import { AnimeDetails, AnimeInfo } from "@/types/anime.type";
import { useState, useEffect, useCallback } from "react";

type WatchPageParams = {
searchParams: { id: string; ep: string; isDub?: string };
};

const doesIdNumber = (id: unknown): boolean => Number.isInteger(Number(id));

// Fetch Anime Info based on ID
const fetchAnimeInfoV1 = async (idProvider: string) => {
if (!idProvider) return null;
const res = await fetch(`/api/anime-infov1?query=${idProvider}`);
return res.json();
};

const fetchAnimeInfoV2 = async (id: string) => {
if (doesIdNumber(id)) {
const res = await fetch(`/api/anime-infov2?query=${id}`);
return res.json();
}
};

function WatchPage({ searchParams }: WatchPageParams) {
const { id, ep, isDub } = searchParams;

const [episodeId, setEpisodeId] = useState<string>("");
const [isLoading, setIsLoading] = useState(true);

// Fetch data from API v2 and API v1 separately
const { data: animeInfoV2, error: errorInfoV2 } = useSWR<AnimeInfo>(
doesIdNumber(id) ? id : null,
fetchAnimeInfoV2
);

const idProvider = isDub
? animeInfoV2?.id_provider.idGogoDub
: animeInfoV2?.id_provider.idGogo || id;

const { data: animeInfoV1, error: errorInfoV1 } = useSWR<AnimeDetails>(
idProvider ? [idProvider, "v1"] : null,
() => fetchAnimeInfoV1(idProvider!)
);

// Set the initial episode ID once the anime info and episodes are loaded
useEffect(() => {
if (animeInfoV1?.episodes && ep) {
const selectedEpisode = animeInfoV1?.episodes.find(
(e) => Number(e.number) === Number(ep)
);
if (selectedEpisode) {
setEpisodeId(selectedEpisode.id);
setIsLoading(false);
}
}
}, [animeInfoV1, ep]);

// Handle episode change through the EpisodesComponent
const handleEpisodeChange = useCallback((newEpisodeId: string) => {
setEpisodeId(newEpisodeId);
}, []);

// Handle loading and error states
if (errorInfoV2 || errorInfoV1) return <Error />;
if (!animeInfoV2 && doesIdNumber(id)) return <Loading />;
if (isLoading || !animeInfoV1) return <Loading />;

return (
<div className="flex flex-col gap-6 p-6 lg:grid lg:grid-cols-5">
<Media
title={animeInfoV1.title}
poster={animeInfoV1.image}
episodeId={episodeId}
ep={ep}
/>
<EpisodesComponent
id={id}
item={animeInfoV1}
ep={ep}
isDub={isDub}
handleEpisodeChange={handleEpisodeChange}
/>
</div>
);
}

export default WatchPage;
25 changes: 15 additions & 10 deletions app/api/anime-infov1/route.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import { NextRequest, NextResponse } from 'next/server'
import { AnimeServiceV1 } from '@/services'
import { NextRequest, NextResponse } from "next/server";
import { AnimeServiceV1 } from "@/services";

export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const query = searchParams.get('query')
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get("query");

if (!query) {
return NextResponse.json({ error: 'Query parameter is required' }, { status: 400 })
return NextResponse.json(
{ error: "Query parameter is required" },
{ status: 400 }
);
}

try {
const { data } = await AnimeServiceV1.getAnimeInfoV1(query)
return NextResponse.json(data)
const { data } = await AnimeServiceV1.getAnimeInfoV1Gogo(query);
return NextResponse.json(data);
} catch (error) {
console.error('Error searching anime:', error)
return NextResponse.json({ error: 'Failed to search anime' }, { status: 500 })
console.error("Error searching anime:", error);
return NextResponse.json(
{ error: "Failed to search anime" },
{ status: 500 }
);
}
}

25 changes: 25 additions & 0 deletions app/api/anime-infov2/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { NextRequest, NextResponse } from "next/server";
import { AnimeServiceV2 } from "@/services";

export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get("query");

if (!query) {
return NextResponse.json(
{ error: "Query parameter is required" },
{ status: 400 }
);
}

try {
const { data } = await AnimeServiceV2.getAnimeInfoV2(query);
return NextResponse.json(data);
} catch (error) {
console.error("Error searching anime:", error);
return NextResponse.json(
{ error: "Failed to search anime" },
{ status: 500 }
);
}
}
Loading

0 comments on commit 44e17a7

Please sign in to comment.