Skip to content

Commit

Permalink
feat: Improve loading message and add server selection UI in EpisodeP…
Browse files Browse the repository at this point in the history
…layer component
  • Loading branch information
DeveloperJosh committed Jul 6, 2024
1 parent 592e716 commit 487b32f
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 144 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
29 changes: 7 additions & 22 deletions src/components/EpisodePlayer.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
import React, { useMemo, useState } from 'react';
import React, { useMemo, useState, useEffect } from 'react';
import dynamic from 'next/dynamic';
import CustomDropdown from '@/components/CustomDropdown';

const ReactPlayer = dynamic(() => import('react-player'), { ssr: false });

const EpisodePlayer = ({ episode, selectedQuality, setSelectedQuality, setSelectedServer, selectedServer }) => {
const EpisodePlayer = ({ episode, selectedQuality, setSelectedQuality }) => {
const [loading, setLoading] = useState(true);

const sources = episode?.sources ?? [];
const sourceUrl = useMemo(() => {
const source = sources.find(source => source.quality === selectedQuality);
return source ? source.source : sources[0]?.source;
return source ? source.url : sources[0]?.url;
}, [sources, selectedQuality]);

const handleReady = () => {
setLoading(false);
};

useEffect(() => {
setLoading(true);
}, [sourceUrl]);

return (
<div className="bg-gray-800 p-6 rounded-lg shadow-lg">
<h2 className="text-2xl font-bold text-yellow-500 mb-4 text-center">Episode {episode ? episode.episodeNumber : '1'}</h2>
Expand All @@ -34,29 +38,10 @@ const EpisodePlayer = ({ episode, selectedQuality, setSelectedQuality, setSelect
height="100%"
className="react-player"
onReady={handleReady}
onStart={() => setLoading(true)}
/>
)}
</div>
<div className="flex justify-center mt-4 space-x-4">
<p className="font-bold">Select Server:</p>
</div>
<div className="flex justify-center mt-4 space-x-4">
<p className="font-bold text-sm">If one server isn't working, Try the next</p>
</div>
<div className="flex justify-center mt-4 space-x-4">
<button
onClick={() => setSelectedServer('gogocdn')}
className={`px-4 py-2 mx-2 rounded ${selectedServer === 'gogocdn' ? 'bg-yellow-500 text-gray-800' : 'bg-gray-700 text-gray-300'}`}
>
Neko
</button>
<button
onClick={() => setSelectedServer('streamwish')}
className={`px-4 py-2 mx-2 rounded ${selectedServer === 'streamwish' ? 'bg-yellow-500 text-gray-800' : 'bg-gray-700 text-gray-300'}`}
>
StreamWish
</button>
<CustomDropdown
label="Quality"
options={sources.map(source => source.quality)}
Expand Down
213 changes: 96 additions & 117 deletions src/pages/anime/[name].js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { useRouter } from 'next/router';
import useSWR from 'swr';
import axios from 'axios';
import Image from 'next/image';
import Head from 'next/head';
Expand All @@ -9,107 +8,68 @@ import { useSession } from 'next-auth/react';
import Comments from '@/components/Comments';
import CommentForm from '@/components/CommentForm';

const api = "/api"; // Use the relative path for Next.js API
const fetcher = url => axios.get(url).then(res => res.data);
const apiBaseUrl = "https://api-anime.sziwyz.easypanel.host/anime/gogoanime"; // Use the new API base URL

const AnimePage = () => {
const router = useRouter();
const { name, ep } = router.query; // Extract the 'ep' parameter from the query
const { data: animeData, error } = useSWR(name ? `${api}/anime/${name}` : null, fetcher);
const { data: session, status } = useSession(); // Fetch the session
const [currentPage, setCurrentPage] = useState(1);
const { data: session, status: sessionStatus } = useSession(); // Rename session status to sessionStatus
const [animeData, setAnimeData] = useState(null);
const [error, setError] = useState(null);
const [selectedEpisode, setSelectedEpisode] = useState(null);
const [episodeSources, setEpisodeSources] = useState({});
const [selectedQuality, setSelectedQuality] = useState('1080p');
const [selectedServer, setSelectedServer] = useState('gogocdn');

useEffect(() => {
if (name) {
const episodeToPlay = ep ? parseInt(ep, 10) : 1; // Use 'ep' parameter or default to 1
if (episodeToPlay === 0) {
playByAnimeName(name); // Play the anime by its name if episode is 0
} else {
fetchEpisodeDetails(episodeToPlay);
}
}
}, [name, ep, selectedServer]);

const fetchEpisodeDetails = async (episodeNumber) => {
const episodeId = `${name}-episode-${episodeNumber}`;
setSelectedEpisode({ episodeNumber, episodeId, sources: null });

if (!episodeSources[episodeId] || episodeSources[episodeId].server !== selectedServer) { // Check for server
const fetchAnimeData = async () => {
try {
const response = await axios.get(`${api}/watch/${episodeId}?server=${selectedServer}`); // Include server in API request
const sources = response.data;
setEpisodeSources(prevState => ({ ...prevState, [episodeId]: { sources, server: selectedServer } })); // Store server with sources
const defaultQuality = sources.find(source => source.quality === '1080p') ? '1080p' : sources[0].quality;
setSelectedQuality(defaultQuality);
setSelectedEpisode(prevState => ({ ...prevState, sources }));
let AnimeName = episodeId.split('-episode-')[0];
AnimeName = AnimeName.replace(/-/g, ' ').toUpperCase();
addHistory(AnimeName, episodeId, episodeNumber);
const response = await axios.get(`${apiBaseUrl}/info/${name}`);
setAnimeData(response.data);
} catch (error) {
console.error('Error fetching episode sources:', error);
}
} else {
setSelectedEpisode(prevState => ({ ...prevState, sources: episodeSources[episodeId].sources }));
if (!episodeSources[episodeId].sources.some(source => source.quality === selectedQuality)) {
setSelectedQuality(episodeSources[episodeId].sources[0].quality);
setError(error);
}
};

if (name) {
fetchAnimeData();
}
};
}, [name]);

const playByAnimeName = async (animeName) => {
const fetchEpisodeDetails = useCallback(async (episodeNumber) => {
const episodeId = episodeNumber === 0 ? name : `${name}-episode-${episodeNumber}`;
try {
const response = await axios.get(`${api}/watch/${animeName}`);
const sources = response.data;
setEpisodeSources(prevState => ({ ...prevState, [animeName]: { sources, server: selectedServer } }));
const response = await axios.get(`${apiBaseUrl}/watch/${episodeId}`);
const sources = response.data.sources;
setEpisodeSources(prevState => ({ ...prevState, [episodeId]: { sources } }));
const defaultQuality = sources.find(source => source.quality === '1080p') ? '1080p' : sources[0].quality;
setSelectedQuality(defaultQuality);
setSelectedEpisode({ episodeNumber: 0, episodeId: animeName, sources });
addHistory(animeName, animeName, 0);
setSelectedEpisode({ episodeNumber, episodeId, sources });
} catch (error) {
console.error('Error fetching anime sources:', error);
console.error('Error fetching episode sources:', error);
}
};
}, [name]);

const addHistory = async (name, animeId, episodeNumber) => {
try {
const response = await axios.post(`${api}/history/add`, {
name,
animeId,
episodeNumber
}, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}` // Ensure the token is correctly set
}
});
console.log('History added:', response.data.message);
} catch (error) {
console.error('Error adding history:', error.response?.data?.error || error.message);
}
};

const handleEpisodeSelect = episodeNumber => {
const episodeNum = parseInt(episodeNumber, 10); // Convert to number
if (episodeNum === 0) {
router.push({
pathname: `/anime/${name}`
}, undefined, { shallow: true });
playByAnimeName(name);
} else {
const playFirstEpisode = useCallback(() => {
if (animeData && animeData.episodes.length > 0) {
const firstEpisode = animeData.episodes[0];
fetchEpisodeDetails(firstEpisode.number);
router.push({
pathname: `/anime/${name}`,
query: { ep: episodeNum }
query: { ep: firstEpisode.number }
}, undefined, { shallow: true });
fetchEpisodeDetails(episodeNum);
}
};
}, [animeData, name, router, fetchEpisodeDetails]);

const handlePageChange = (direction) => {
setCurrentPage(prev => prev + direction);
};
useEffect(() => {
if (name && !selectedEpisode) {
const episodeToPlay = ep ? parseInt(ep, 10) : 1; // Use 'ep' parameter or default to 1
if (episodeToPlay === 0) {
playFirstEpisode(); // Play the first episode in the list if episode is 0
} else {
fetchEpisodeDetails(episodeToPlay);
}
}
}, [name, ep, selectedEpisode, playFirstEpisode, fetchEpisodeDetails]);

if (!animeData && !error) {
return <div className="text-center mt-8 text-white">Loading...</div>;
Expand All @@ -118,30 +78,53 @@ const AnimePage = () => {
return <div className="text-center mt-8 text-red-500">Failed to load anime data</div>;
}

const { animeInfo, episodes } = animeData;
const episodesPerPage = 13;
const totalPages = Math.ceil(episodes.length / episodesPerPage);
const currentEpisodes = episodes.slice((currentPage - 1) * episodesPerPage, currentPage * episodesPerPage);
const { title, description, status, genres, releaseDate, totalEpisodes, image, episodes } = animeData;

const handleEpisodeSelect = (episodeNumber) => {
const episodeNum = parseInt(episodeNumber, 10); // Convert to number
fetchEpisodeDetails(episodeNum);
router.push({
pathname: `/anime/${name}`,
query: { ep: episodeNum }
}, undefined, { shallow: true });
};

const episodeButtons = currentEpisodes.map(episode => (
<li key={episode.episodeNumber} className="mb-2">
const episodeButtons = episodes.map(episode => (
<li key={episode.number} className="mb-2">
<button
className={`w-full text-left px-4 py-2 rounded ${selectedEpisode && selectedEpisode.episodeNumber === episode.episodeNumber ? 'bg-gray-700 text-yellow-500' : 'bg-gray-700 text-gray-300'}`}
onClick={() => handleEpisodeSelect(episode.episodeNumber)}
className={`w-full text-left px-4 py-2 rounded ${selectedEpisode && selectedEpisode.episodeNumber === episode.number ? 'bg-gray-700 text-yellow-500' : 'bg-gray-700 text-gray-300'}`}
onClick={() => handleEpisodeSelect(episode.number)}
>
{`EP ${episode.episodeNumber}`}
{`EP ${episode.number}`}
</button>
</li>
));

const addHistory = async (name, animeId, episodeNumber) => {
try {
const response = await axios.post(`${apiBaseUrl}/history/add`, {
name,
animeId,
episodeNumber
}, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}` // Ensure the token is correctly set
}
});
console.log('History added:', response.data.message);
} catch (error) {
console.error('Error adding history:', error.response?.data?.error || error.message);
}
};

return (
<>
<Head>
<title>{animeInfo.title}</title>
<meta name="description" content={animeInfo.description} />
<meta property="og:title" content={animeInfo.title} />
<meta property="og:description" content={animeInfo.description} />
<meta property="og:image" content={animeInfo.image} />
<title>{title}</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
</Head>
<div className="bg-gray-900 min-h-screen text-gray-200">
<div className="container mx-auto px-4 py-8">
Expand All @@ -154,9 +137,9 @@ const AnimePage = () => {
className="w-full bg-gray-700 text-yellow-500 p-2 rounded"
onChange={(e) => handleEpisodeSelect(parseInt(e.target.value, 10))}
>
{currentEpisodes.map(episode => (
<option key={episode.episodeNumber} value={episode.episodeNumber}>
{`EP ${episode.episodeNumber}`}
{episodes.map(episode => (
<option key={episode.number} value={episode.number}>
{`EP ${episode.number}`}
</option>
))}
</select>
Expand All @@ -165,31 +148,27 @@ const AnimePage = () => {
{episodeButtons}
</ul>
</div>
{episodes.length > episodesPerPage && (
<div className="flex justify-between mt-4">
<button onClick={() => handlePageChange(-1)} className="bg-yellow-500 text-gray-800 px-4 py-2 rounded hover:bg-yellow-600" disabled={currentPage <= 1}>
Previous
</button>
<button onClick={() => handlePageChange(1)} className="bg-yellow-500 text-gray-800 px-4 py-2 rounded hover:bg-yellow-600 ml-auto" disabled={currentPage >= totalPages}>
Next
</button>
</div>
)}
</aside>
<main className="w-full md:w-3/4 md:ml-4">
<div style={{ minHeight: '300px' }}>
<EpisodePlayer
episode={selectedEpisode}
selectedQuality={selectedQuality}
setSelectedQuality={setSelectedQuality}
setSelectedServer={setSelectedServer}
selectedServer={selectedServer}
/>
</div>
<br />
<AnimeDetails animeInfo={animeInfo} />
<AnimeDetails
title={title}
description={description}
status={status}
genres={genres}
releaseDate={releaseDate}
totalEpisodes={totalEpisodes}
image={image}
/>
<Comments animeId={name} episodeNumber={selectedEpisode ? selectedEpisode.episodeNumber : null} />
{status === 'authenticated' && <CommentForm animeId={name} episodeNumber={selectedEpisode ? selectedEpisode.episodeNumber : null} />}
{sessionStatus === 'authenticated' && <CommentForm animeId={name} episodeNumber={selectedEpisode ? selectedEpisode.episodeNumber : null} />}
</main>
</div>
</div>
Expand All @@ -198,16 +177,16 @@ const AnimePage = () => {
);
};

const AnimeDetails = ({ animeInfo }) => (
const AnimeDetails = ({ title, description, status, genres, releaseDate, totalEpisodes, image }) => (
<div className="bg-gray-800 p-6 rounded-lg shadow-lg flex flex-col md:flex-row items-center">
<ImageWrapper image={animeInfo.image} title={animeInfo.title} />
<ImageWrapper image={image} title={title} />
<div className="text-gray-300">
<h1 className="text-4xl font-bold text-left mb-4 text-yellow-500">{animeInfo.title}</h1>
<p className="mb-4"><span className="font-semibold">Description:</span> {animeInfo.description}</p>
<p className="mb-2"><span className="font-semibold">Status:</span> {animeInfo.status}</p>
<p className="mb-2"><span className="font-semibold">Genres:</span> {animeInfo.genres.join(', ')}</p>
<p className="mb-2"><span className="font-semibold">Released:</span> {animeInfo.released}</p>
<p className="mb-2"><span className="font-semibold">Total Episodes:</span> {animeInfo.totalEpisodes}</p>
<h1 className="text-4xl font-bold text-left mb-4 text-yellow-500">{title}</h1>
<p className="mb-4"><span className="font-semibold">Description:</span> {description}</p>
<p className="mb-2"><span className="font-semibold">Status:</span> {status}</p>
<p className="mb-2"><span className="font-semibold">Genres:</span> {genres.join(', ')}</p>
<p className="mb-2"><span className="font-semibold">Released:</span> {releaseDate}</p>
<p className="mb-2"><span className="font-semibold">Total Episodes:</span> {totalEpisodes}</p>
</div>
</div>
);
Expand Down
6 changes: 3 additions & 3 deletions src/pages/auth/profile.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { useSession, signOut } from 'next-auth/react';
import axios from 'axios';
import Link from 'next/link';
import History from '@/components/History';
import Image from 'next/image';

const Dashboard = () => {
const { data: session, status } = useSession();
Expand Down Expand Up @@ -118,7 +118,7 @@ const Dashboard = () => {

{activeTab === 'profile' && (
<div className="text-center">
<img src={session.user.image} alt={session.user.name} className="w-16 h-16 rounded-full mx-auto mb-4" />
<Image src={session.user.image} alt={session.user.name} className="w-16 h-16 rounded-full mx-auto mb-4" />
<p className="text-xl sm:text-2xl">Name: <span className="font-bold">{session.user.name}</span></p>
<p className="text-xl sm:text-2xl">Email: <span className="font-bold">{session.user.email}</span></p>
<p className="text-xl sm:text-2xl">ID: <span className="font-bold">{session.user.id}</span></p>
Expand Down Expand Up @@ -168,7 +168,7 @@ const Dashboard = () => {
comments.map((comment) => (
<div key={comment._id} className="bg-gray-700 p-4 rounded-lg">
<div className="flex items-center mb-2">
<img src={comment.user.image} alt={comment.user.name} className="w-10 h-10 rounded-full mr-2" />
<Image src={comment.user.image} alt={comment.user.name} className="w-10 h-10 rounded-full mr-2" />
<span className="font-semibold">{comment.user.name} | {comment.animeId}-episode-{comment.episodeNumber}</span>
</div>
<p>{comment.text}</p>
Expand Down
Loading

0 comments on commit 487b32f

Please sign in to comment.