Skip to content

Commit c6bab73

Browse files
authored
Merge pull request #29 from bitfreee/upstream/anime
Add anime tab
2 parents d000751 + 88c2a7f commit c6bab73

File tree

17 files changed

+687
-72
lines changed

17 files changed

+687
-72
lines changed

.github/workflows/cf-deploy.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@ on:
55
branches:
66
- main
77
- dev
8-
pull_request:
9-
branches:
10-
- main
11-
- dev
128

139
jobs:
1410
build:

src/app/(front)/anime/page.tsx

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import Hero from '@/components/hero';
2+
import ShowsContainer from '@/components/shows-container';
3+
import { siteConfig } from '@/configs/site';
4+
import { Genre } from '@/enums/genre';
5+
import { RequestType, type ShowRequest } from '@/enums/request-type';
6+
import { getRandomShow } from '@/lib/utils';
7+
import MovieService from '@/services/MovieService';
8+
import { type CategorizedShows, MediaType, type Show } from '@/types';
9+
10+
export const revalidate = 3600;
11+
12+
export default async function AnimePage() {
13+
const h1 = `${siteConfig.name} Anime`;
14+
const requests: ShowRequest[] = [
15+
{
16+
title: 'Anime TV Shows Latest',
17+
req: { requestType: RequestType.ANIME_LATEST, mediaType: MediaType.TV },
18+
visible: true,
19+
},
20+
{
21+
title: 'Anime TV Shows Trending',
22+
req: {
23+
requestType: RequestType.ANIME_TRENDING,
24+
mediaType: MediaType.TV,
25+
},
26+
visible: true,
27+
},
28+
{
29+
title: 'Anime TV Shows Top Rated',
30+
req: {
31+
requestType: RequestType.ANIME_TOP_RATED,
32+
mediaType: MediaType.TV,
33+
},
34+
visible: true,
35+
},
36+
{
37+
title: 'Netflix Anime TV Shows',
38+
req: { requestType: RequestType.ANIME_NETFLIX, mediaType: MediaType.TV },
39+
visible: true,
40+
},
41+
{
42+
title: 'Anime Movies Latest',
43+
req: {
44+
requestType: RequestType.ANIME_LATEST,
45+
mediaType: MediaType.MOVIE,
46+
},
47+
visible: true,
48+
},
49+
{
50+
title: 'Anime Movies Top Rated',
51+
req: {
52+
requestType: RequestType.ANIME_TOP_RATED,
53+
mediaType: MediaType.MOVIE,
54+
},
55+
visible: true,
56+
},
57+
];
58+
let allShows = await MovieService.getShows(requests);
59+
allShows = allShows.map((category: CategorizedShows) => {
60+
return {
61+
...category,
62+
shows: category.shows.map((show: Show) => {
63+
return {
64+
...show,
65+
media_type: category.title.includes('Movies')
66+
? MediaType.MOVIE
67+
: MediaType.TV,
68+
};
69+
}),
70+
};
71+
});
72+
const randomShow: Show | null = getRandomShow(allShows);
73+
74+
return (
75+
<>
76+
<h1 className="hidden">{h1}</h1>
77+
<Hero randomShow={randomShow} />
78+
<ShowsContainer shows={allShows} />
79+
</>
80+
);
81+
}

src/app/watch/anime/[slug]/page.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React from 'react';
2+
import EmbedPlayer from '@/components/watch/embed-player';
3+
import { MediaType } from '@/types';
4+
5+
export const revalidate = 3600;
6+
7+
export default function Page({ params }: { params: { slug: string } }) {
8+
const id = params.slug.split('-').pop();
9+
const movieId: string | undefined = params.slug.split('/').pop();
10+
return (
11+
<EmbedPlayer
12+
movieId={movieId}
13+
mediaType={movieId?.includes('t') ? MediaType.ANIME : undefined}
14+
url={`https://vidsrc.cc/v2/embed/anime/tmdb${id}/1/sub?autoPlay=false`}
15+
/>
16+
);
17+
}

src/components/hero.tsx

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22
import { Icons } from '@/components/icons';
33
import { Button } from '@/components/ui/button';
4-
import { getIdFromSlug } from '@/lib/utils';
4+
import { getIdFromSlug, getNameFromShow, getSlug } from '@/lib/utils';
55
import MovieService from '@/services/MovieService';
66
import { useModalStore } from '@/stores/modal';
77
import { useSearchStore } from '@/stores/search';
@@ -10,12 +10,14 @@ import { type AxiosResponse } from 'axios';
1010
import Link from 'next/link';
1111
import React from 'react';
1212
import CustomImage from './custom-image';
13+
import { usePathname } from 'next/navigation';
1314

1415
interface HeroProps {
1516
randomShow: Show | null;
1617
}
1718

1819
const Hero = ({ randomShow }: HeroProps) => {
20+
const path = usePathname();
1921
React.useEffect(() => {
2022
window.addEventListener('popstate', handlePopstateEvent, false);
2123
return () => {
@@ -43,7 +45,7 @@ const Hero = ({ randomShow }: HeroProps) => {
4345
useModalStore.setState({ show: data, open: true, play: true });
4446
})
4547
.catch((error) => {
46-
console.log(`findMovie: `, error);
48+
console.error(`findMovie: `, error);
4749
});
4850
}
4951
};
@@ -56,6 +58,20 @@ const Hero = ({ randomShow }: HeroProps) => {
5658
return null;
5759
}
5860

61+
const handleHref = (): string => {
62+
if (!randomShow) {
63+
return '#';
64+
}
65+
if (!path.includes('/anime')) {
66+
const type = randomShow.media_type === MediaType.MOVIE ? 'movie' : 'tv';
67+
return `/watch/${type}/${randomShow.id}`;
68+
}
69+
const prefix: string =
70+
randomShow?.media_type === MediaType.MOVIE ? 'm' : 't';
71+
const id = `${prefix}-${randomShow.id}`;
72+
return `/watch/anime/${id}`;
73+
};
74+
5975
return (
6076
<section aria-label="Hero" className="w-full">
6177
{randomShow && (
@@ -88,20 +104,10 @@ const Hero = ({ randomShow }: HeroProps) => {
88104
{randomShow?.overview ?? '-'}
89105
</p>
90106
<div className="mt-[1.5vw] flex items-center space-x-2">
91-
<Link
92-
prefetch={false}
93-
href={`/watch/${
94-
randomShow.media_type === MediaType.MOVIE ? 'movie' : 'tv'
95-
}/${randomShow.id}`}>
107+
<Link prefetch={false} href={handleHref()}>
96108
<Button
97109
aria-label="Play video"
98-
className="h-auto flex-shrink-0 gap-2 rounded-xl"
99-
// onClick={() => {
100-
// modalStore.setShow(randomShow);
101-
// modalStore.setOpen(true);
102-
// modalStore.setPlay(true);
103-
// }}
104-
>
110+
className="h-auto flex-shrink-0 gap-2 rounded-xl">
105111
<Icons.play className="fill-current" aria-hidden="true" />
106112
Play
107113
</Button>
@@ -111,9 +117,21 @@ const Hero = ({ randomShow }: HeroProps) => {
111117
variant="outline"
112118
className="h-auto flex-shrink-0 gap-2 rounded-xl"
113119
onClick={() => {
114-
modalStore.setShow(randomShow);
115-
modalStore.setOpen(true);
116-
modalStore.setPlay(true);
120+
const name = getNameFromShow(randomShow);
121+
const path: string =
122+
randomShow.media_type === MediaType.TV
123+
? 'tv-shows'
124+
: 'movies';
125+
window.history.pushState(
126+
null,
127+
'',
128+
`${path}/${getSlug(randomShow.id, name)}`,
129+
);
130+
useModalStore.setState({
131+
show: randomShow,
132+
open: true,
133+
play: true,
134+
});
117135
}}>
118136
<Icons.info aria-hidden="true" />
119137
More Info

src/components/season.tsx

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
'use client';
2+
import React from 'react';
3+
import { type IEpisode, type ISeason } from '@/types';
4+
import {
5+
DropdownMenu,
6+
DropdownMenuContent,
7+
DropdownMenuItem,
8+
DropdownMenuTrigger,
9+
} from './ui/dropdown-menu';
10+
11+
interface SeasonProps {
12+
seasons: ISeason[];
13+
onChangeEpisode: (episode: IEpisode) => void;
14+
}
15+
16+
export default function Season(props: SeasonProps) {
17+
const panelRef = React.useRef<HTMLDivElement>(null);
18+
19+
const [activeSeason, setActiveSeason] = React.useState<ISeason>(
20+
props.seasons[0],
21+
);
22+
23+
const handleShowPanel = () => {
24+
panelRef.current?.classList.toggle('active');
25+
};
26+
const handleHidePanel = () => {
27+
panelRef.current?.classList.remove('active');
28+
};
29+
30+
return (
31+
<React.Fragment>
32+
<div className="header-top absolute left-0 right-0 top-0 z-[3] flex h-12 items-center justify-between gap-x-5 px-4 md:gap-x-8 md:px-10">
33+
<div
34+
onClick={handleShowPanel}
35+
className="flex flex-1 cursor-pointer items-center gap-x-5 md:gap-x-8">
36+
<svg
37+
xmlns="http://www.w3.org/2000/svg"
38+
width="24"
39+
height="24"
40+
viewBox="0 0 24 24"
41+
fill="none"
42+
stroke="currentColor"
43+
strokeWidth="2"
44+
strokeLinecap="round"
45+
strokeLinejoin="round"
46+
className="lucide lucide-square-menu">
47+
<rect width="18" height="18" x="3" y="3" rx="2" />
48+
<path d="M7 8h10" />
49+
<path d="M7 12h10" />
50+
<path d="M7 16h10" />
51+
</svg>
52+
</div>
53+
</div>
54+
<aside ref={panelRef} id="ep-panel" className="panel from-left z-10">
55+
<DropdownMenu>
56+
<DropdownMenuTrigger asChild>
57+
<div className="seasons dropdown">
58+
<a className="btn dropdown-toggle season-current !block max-w-[180px] cursor-pointer truncate">
59+
{`SS ${activeSeason.season_number}: ${activeSeason.name}`}
60+
</a>
61+
</div>
62+
</DropdownMenuTrigger>
63+
<DropdownMenuContent className="max-h-[220px] max-w-[240px] overflow-auto">
64+
{props.seasons.map((season: ISeason) => (
65+
<DropdownMenuItem
66+
key={season.id}
67+
onClick={() => setActiveSeason(season)}
68+
className="block w-full cursor-pointer truncate">
69+
{`SS ${season.season_number}: ${season.name}`}
70+
</DropdownMenuItem>
71+
))}
72+
</DropdownMenuContent>
73+
</DropdownMenu>
74+
<div id="close-ep-btn" className="close" onClick={handleHidePanel}>
75+
<svg
76+
xmlns="http://www.w3.org/2000/svg"
77+
width="24"
78+
height="24"
79+
viewBox="0 0 24 24"
80+
fill="none"
81+
stroke="currentColor"
82+
strokeWidth="2"
83+
strokeLinecap="round"
84+
strokeLinejoin="round"
85+
className="lucide lucide-x">
86+
<path d="M18 6 6 18" />
87+
<path d="m6 6 12 12" />
88+
</svg>
89+
</div>
90+
{activeSeason.episodes?.length ? (
91+
<ul className="episodes">
92+
{activeSeason.episodes.map((episode: IEpisode) => {
93+
return (
94+
<li
95+
key={episode.id}
96+
className="cursor-pointer"
97+
onClick={() => {
98+
handleHidePanel();
99+
props.onChangeEpisode(episode);
100+
}}>
101+
<a>{`EP ${episode.episode_number}: ${episode.name}`}</a>
102+
</li>
103+
);
104+
})}
105+
</ul>
106+
) : null}
107+
</aside>
108+
</React.Fragment>
109+
);
110+
}

src/components/shows-container.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const ShowsContainer = ({ shows }: ShowsContainerProps) => {
4949
? await MovieService.findTvSeries(movieId)
5050
: await MovieService.findMovie(movieId);
5151
const data: Show = response.data;
52+
5253
if (data)
5354
useModalStore.setState({
5455
show: data,

0 commit comments

Comments
 (0)