diff --git a/frontend/src/functions/APIClient.ts b/frontend/src/functions/APIClient.ts index c01ba8b8..2c4a4110 100644 --- a/frontend/src/functions/APIClient.ts +++ b/frontend/src/functions/APIClient.ts @@ -55,6 +55,13 @@ const APIClient = async ( throw new Error('Forbidden: Access denied.'); } + if (response.status === 404) { + throw { + status: 404, + message: 'Resource not found', + } as ApiError; + } + let data; // expected empty response diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 4fc91d7a..e7dce8bc 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -27,6 +27,7 @@ import ChannelAbout from './pages/ChannelAbout'; import Download from './pages/Download'; import loadUserAccount from './api/loader/loadUserAccount'; import loadAppsettingsConfig from './api/loader/loadAppsettingsConfig'; +import NotFound from './pages/404Page'; const router = createBrowserRouter( [ @@ -145,6 +146,10 @@ const router = createBrowserRouter( element: , errorElement: , }, + { + path: '*', + element: , + }, ], { basename: import.meta.env.BASE_URL }, ); diff --git a/frontend/src/pages/404Page.tsx b/frontend/src/pages/404Page.tsx new file mode 100644 index 00000000..1b75f7ab --- /dev/null +++ b/frontend/src/pages/404Page.tsx @@ -0,0 +1,22 @@ +import { Link } from 'react-router-dom'; +import useColours from '../configuration/colours/useColours'; +import Routes from '../configuration/routes/RouteList'; + +const NotFound = ({ failType = 'page' }) => { + useColours(); + return ( + <> + 404 | Not found +
+

Oops!

+

+ 404 + : That {failType} does not exist. +

+ Go Home +
+ + ); +}; + +export default NotFound; diff --git a/frontend/src/pages/ChannelBase.tsx b/frontend/src/pages/ChannelBase.tsx index 2da7c552..cb63e0e7 100644 --- a/frontend/src/pages/ChannelBase.tsx +++ b/frontend/src/pages/ChannelBase.tsx @@ -8,6 +8,8 @@ import ChannelBanner from '../components/ChannelBanner'; import loadChannelNav, { ChannelNavResponseType } from '../api/loader/loadChannelNav'; import loadChannelById from '../api/loader/loadChannelById'; import useIsAdmin from '../functions/useIsAdmin'; +import { ApiError } from '../functions/APIClient'; +import NotFound from './404Page'; type ChannelParams = { channelId: string; @@ -18,6 +20,7 @@ export type ChannelResponseType = ChannelType; const ChannelBase = () => { const { channelId } = useParams() as ChannelParams; const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType; + const [error, setError] = useState(null); const isAdmin = useIsAdmin(); const [channelResponse, setChannelResponse] = useState(); @@ -29,14 +32,24 @@ const ChannelBase = () => { useEffect(() => { (async () => { + setError(null); + try { + const channelResponse = await loadChannelById(channelId); + setChannelResponse(channelResponse); + } catch (err) { + if ((err as ApiError).status === 404) { + setError(err as ApiError); + } else { + console.error('Failed to fetch item:', err); + } + } const channelNavResponse = await loadChannelNav(channelId); - const channelResponse = await loadChannelById(channelId); - - setChannelResponse(channelResponse); setChannelNav(channelNavResponse); })(); }, [channelId]); + if (error) return ; + if (!channelId) { return []; } diff --git a/frontend/src/pages/Playlist.tsx b/frontend/src/pages/Playlist.tsx index 0ee310e8..6af665ec 100644 --- a/frontend/src/pages/Playlist.tsx +++ b/frontend/src/pages/Playlist.tsx @@ -23,6 +23,8 @@ import Button from '../components/Button'; import loadVideoListByFilter from '../api/loader/loadVideoListByPage'; import useIsAdmin from '../functions/useIsAdmin'; import { useUserConfigStore } from '../stores/UserConfigStore'; +import { ApiError } from '../functions/APIClient'; +import NotFound from './404Page'; export type VideoResponseType = { data?: VideoType[]; @@ -31,6 +33,7 @@ export type VideoResponseType = { const Playlist = () => { const { playlistId } = useParams(); + const [error, setError] = useState(null); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const videoId = searchParams.get('videoId'); @@ -66,22 +69,28 @@ const Playlist = () => { useEffect(() => { (async () => { - const playlist = await loadPlaylistById(playlistId); - const video = await loadVideoListByFilter({ - playlist: playlistId, - page: currentPage, - watch: hideWatched ? 'unwatched' : undefined, - }); - - const isCustomPlaylist = playlist?.playlist_type === 'custom'; - if (!isCustomPlaylist) { - const channel = await loadChannelById(playlist.playlist_channel_id); - - setChannelResponse(channel); + setError(null); + try { + const playlist = await loadPlaylistById(playlistId); + const video = await loadVideoListByFilter({ + playlist: playlistId, + page: currentPage, + watch: hideWatched ? 'unwatched' : undefined, + }); + setPlaylistResponse(playlist); + setVideoResponse(video); + const isCustomPlaylist = playlist?.playlist_type === 'custom'; + if (!isCustomPlaylist) { + const channel = await loadChannelById(playlist.playlist_channel_id); + setChannelResponse(channel); + } + } catch (err) { + if ((err as ApiError).status === 404) { + setError(err as ApiError); + } else { + console.error('Failed to fetch item:', err); + } } - - setPlaylistResponse(playlist); - setVideoResponse(video); setRefresh(false); })(); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -94,9 +103,9 @@ const Playlist = () => { videoId, ]); - if (!playlistId || !playlist) { - return `Playlist ${playlistId} not found!`; - } + if (error) return ; + + if (!playlist || !playlistId) return []; const isCustomPlaylist = playlist.playlist_type === 'custom'; diff --git a/frontend/src/pages/Video.tsx b/frontend/src/pages/Video.tsx index ba659e32..127c23eb 100644 --- a/frontend/src/pages/Video.tsx +++ b/frontend/src/pages/Video.tsx @@ -43,6 +43,8 @@ import { useAppSettingsStore } from '../stores/AppSettingsStore'; import updateDownloadQueueStatusById from '../api/actions/updateDownloadQueueStatusById'; import { FileSizeUnits } from '../api/actions/updateUserConfig'; import { useUserConfigStore } from '../stores/UserConfigStore'; +import { ApiError } from '../functions/APIClient'; +import NotFound from './404Page'; const isInPlaylist = (videoId: string, playlist: PlaylistType) => { return playlist.playlist_entries.some(entry => { @@ -116,6 +118,7 @@ const Video = () => { const { appSettingsConfig } = useAppSettingsStore(); const { userConfig } = useUserConfigStore(); + const [error, setError] = useState(null); const [videoEnded, setVideoEnded] = useState(false); const [playlistAutoplay, setPlaylistAutoplay] = useState( localStorage.getItem('playlistAutoplay') === 'true', @@ -138,7 +141,18 @@ const Video = () => { useEffect(() => { (async () => { if (refreshVideoList || videoId !== videoResponse?.youtube_id) { - const videoByIdResponse = await loadVideoById(videoId); + setError(null); + try { + const videoByIdResponse = await loadVideoById(videoId); + setVideoResponse(videoByIdResponse); + } catch (err) { + if ((err as ApiError).status === 404) { + setError(err as ApiError); + } else { + console.error('Failed to fetch item:', err); + } + } + const simmilarVideosResponse = await loadSimmilarVideosById(videoId); const customPlaylistsResponse = await loadPlaylistList({ type: 'custom' }); const videoNavResponse = await loadVideoNav(videoId); @@ -150,7 +164,6 @@ const Video = () => { console.log('Comments not found', e); } - setVideoResponse(videoByIdResponse); setSimmilarVideos(simmilarVideosResponse); setVideoPlaylistNav(videoNavResponse); setCustomPlaylistsResponse(customPlaylistsResponse); @@ -191,6 +204,8 @@ const Video = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [videoEnded, playlistAutoplay]); + if (error) return ; + if (videoResponse === undefined) { return []; }