From d830115f8911c9f7390b6f8e5c5a9bf12da02e6e Mon Sep 17 00:00:00 2001 From: Abdullah Khan <85450222+AbdullahDaGoat@users.noreply.github.com> Date: Tue, 18 Jun 2024 21:45:19 -0400 Subject: [PATCH 1/7] Update ModalEpisodeSelector.tsx --- src/components/media/ModalEpisodeSelector.tsx | 97 ++++++++++--------- 1 file changed, 53 insertions(+), 44 deletions(-) diff --git a/src/components/media/ModalEpisodeSelector.tsx b/src/components/media/ModalEpisodeSelector.tsx index 298a74b9..ae3a6f3c 100644 --- a/src/components/media/ModalEpisodeSelector.tsx +++ b/src/components/media/ModalEpisodeSelector.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { get } from "@/backend/metadata/tmdb"; import { conf } from "@/setup/config"; @@ -11,6 +11,24 @@ export function EpisodeSelector({ tmdbId }: ModalEpisodeSelectorProps) { const [seasonsData, setSeasonsData] = useState([]); const [selectedSeason, setSelectedSeason] = useState(null); + const handleSeasonSelect = useCallback( + async (season: any) => { + try { + const seasonDetails = await get( + `/tv/${tmdbId}/season/${season.season_number}`, + { + api_key: conf().TMDB_READ_API_KEY, + language: "en-US", + }, + ); + setSelectedSeason(seasonDetails); + } catch (err) { + console.error(err); + } + }, + [tmdbId], + ); + useEffect(() => { const fetchSeasons = async () => { try { @@ -19,65 +37,56 @@ export function EpisodeSelector({ tmdbId }: ModalEpisodeSelectorProps) { language: "en-US", }); setSeasonsData(showDetails.seasons); + handleSeasonSelect(showDetails.seasons[0]); // Default to first season } catch (err) { console.error(err); } }; fetchSeasons(); - }, [tmdbId]); - - const handleSeasonSelect = async (season: any) => { - try { - const seasonDetails = await get( - `/tv/${tmdbId}/season/${season.season_number}`, - { - api_key: conf().TMDB_READ_API_KEY, - language: "en-US", - }, - ); - setSelectedSeason(seasonDetails); - } catch (err) { - console.error(err); - } - }; + }, [handleSeasonSelect, tmdbId]); return ( -
-
+
+
{seasonsData.map((season) => (
handleSeasonSelect(season)} - className="cursor-pointer hover:bg-search-background p-1 text-center rounded" + className="cursor-pointer hover:bg-search-background p-1 text-center rounded hover:scale-95 transition-transform duration-300" > S{season.season_number}
))}
-
- {selectedSeason ? ( -
-

- {selectedSeason.name} - {selectedSeason.episodes.length} episodes -

-
    - {selectedSeason.episodes.map( - ( - episode: { episode_number: number; name: string }, - index: number, - ) => ( -
  • - {episode.episode_number}. {episode.name} -
  • - ), - )} -
-
- ) : ( -
-

Select a season to see details

-
- )} +
+
+ {selectedSeason ? ( + selectedSeason.episodes.map( + (episode: { + episode_number: number; + name: string; + still_path: string; + }) => ( +
+ {episode.name} +

{episode.name}

+
+
+ ), + ) + ) : ( +
+ Select a season to see episodes +
+ )} +
); From 7643c777b8c7a0c64412936495c13ab8d82124f3 Mon Sep 17 00:00:00 2001 From: Abdullah Khan <85450222+AbdullahDaGoat@users.noreply.github.com> Date: Tue, 18 Jun 2024 21:45:40 -0400 Subject: [PATCH 2/7] Update PopupModal.tsx --- src/components/media/PopupModal.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/media/PopupModal.tsx b/src/components/media/PopupModal.tsx index 5ec8f9cc..e6693370 100644 --- a/src/components/media/PopupModal.tsx +++ b/src/components/media/PopupModal.tsx @@ -37,6 +37,7 @@ function formatRuntime(runtime: number) { export function PopupModal({ isVisible, onClose, + // eslint-disable-next-line @typescript-eslint/no-unused-vars playingTitle, media, }: PopupModalProps) { @@ -48,6 +49,8 @@ export function PopupModal({ const [data, setData] = useState(null); const [mediaInfo, setMediaInfo] = useState(null); const [error, setError] = useState(null); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [menuOpen, setMenuOpen] = useState(false); // Added definition for menuOpen const navigate = useNavigate(); useEffect(() => { @@ -149,7 +152,7 @@ export function PopupModal({ return (
)) - : Array.from({ length: 3 }).map((_) => ( -
+ : Array.from({ length: 3 }).map((_, i) => ( + // eslint-disable-next-line react/no-array-index-key +
))} From 08243acaf93e71f8d00b4547dd12c23f6501a61d Mon Sep 17 00:00:00 2001 From: Abdullah Khan <85450222+AbdullahDaGoat@users.noreply.github.com> Date: Wed, 19 Jun 2024 01:22:40 -0400 Subject: [PATCH 3/7] Update PopupModal.tsx (Better Responsivness) --- src/components/media/PopupModal.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/media/PopupModal.tsx b/src/components/media/PopupModal.tsx index e6693370..b88e72fd 100644 --- a/src/components/media/PopupModal.tsx +++ b/src/components/media/PopupModal.tsx @@ -152,15 +152,19 @@ export function PopupModal({ return (
-
+
{data?.backdrop_path ? ( {media.poster ) : ( From d607f012388e995b3243c79e2f97825e787b0b43 Mon Sep 17 00:00:00 2001 From: Abdullah Khan <85450222+AbdullahDaGoat@users.noreply.github.com> Date: Wed, 19 Jun 2024 01:23:05 -0400 Subject: [PATCH 4/7] Update ModalEpisodeSelector.tsx (Responviness Fixed) --- src/components/media/ModalEpisodeSelector.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/media/ModalEpisodeSelector.tsx b/src/components/media/ModalEpisodeSelector.tsx index ae3a6f3c..d05d1c22 100644 --- a/src/components/media/ModalEpisodeSelector.tsx +++ b/src/components/media/ModalEpisodeSelector.tsx @@ -47,7 +47,7 @@ export function EpisodeSelector({ tmdbId }: ModalEpisodeSelectorProps) { return (
-
+
{seasonsData.map((season) => (
))}
-
+
{selectedSeason ? ( selectedSeason.episodes.map( From 42d12cfb96c44027c0c9d828f54cafe62f858f75 Mon Sep 17 00:00:00 2001 From: Abdullah Khan <85450222+AbdullahDaGoat@users.noreply.github.com> Date: Wed, 19 Jun 2024 01:23:52 -0400 Subject: [PATCH 5/7] Media Item Error Fixed --- src/backend/metadata/types/tmdb.ts | 570 +++++++++++++++-------------- 1 file changed, 295 insertions(+), 275 deletions(-) diff --git a/src/backend/metadata/types/tmdb.ts b/src/backend/metadata/types/tmdb.ts index 5d082f55..a265b051 100644 --- a/src/backend/metadata/types/tmdb.ts +++ b/src/backend/metadata/types/tmdb.ts @@ -1,289 +1,309 @@ -export enum TMDBContentTypes { - MOVIE = "movie", - TV = "tv", +import slugify from "slugify"; + +import { conf } from "@/setup/config"; +import { MediaItem } from "@/utils/mediaTypes"; + +import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types/mw"; +import { + ExternalIdMovieSearchResult, + TMDBContentTypes, + TMDBEpisodeShort, + TMDBMediaResult, + TMDBMovieData, + TMDBMovieSearchResult, + TMDBSearchResult, + TMDBSeason, + TMDBSeasonMetaResult, + TMDBShowData, + TMDBShowSearchResult, +} from "./types/tmdb"; +import { mwFetch } from "../helpers/fetch"; + +export function mediaTypeToTMDB(type: MWMediaType): TMDBContentTypes { + if (type === MWMediaType.MOVIE) return TMDBContentTypes.MOVIE; + if (type === MWMediaType.SERIES) return TMDBContentTypes.TV; + throw new Error("unsupported type"); } -export type TMDBSeasonShort = { - title: string; - id: number; - season_number: number; -}; +export function mediaItemTypeToMediaType(type: MediaItem["type"]): MWMediaType { + if (type === "movie") return MWMediaType.MOVIE; + if (type === "show") return MWMediaType.SERIES; + throw new Error("unsupported type"); +} -export type TMDBEpisodeShort = { - title: string; - id: number; - episode_number: number; - air_date: string; -}; +export function TMDBMediaToMediaType(type: TMDBContentTypes): MWMediaType { + if (type === TMDBContentTypes.MOVIE) return MWMediaType.MOVIE; + if (type === TMDBContentTypes.TV) return MWMediaType.SERIES; + throw new Error("unsupported type"); +} -export type TMDBMediaResult = { - title: string; - poster?: string; - id: number; - original_release_date?: Date; - object_type: TMDBContentTypes; - seasons?: TMDBSeasonShort[]; -}; +export function TMDBMediaToMediaItemType( + type: TMDBContentTypes, +): MediaItem["type"] { + if (type === TMDBContentTypes.MOVIE) return "movie"; + if (type === TMDBContentTypes.TV) return "show"; + throw new Error("unsupported type"); +} -export type TMDBSeasonMetaResult = { - title: string; - id: string; - season_number: number; - episodes: TMDBEpisodeShort[]; -}; +export function formatTMDBMeta( + media: TMDBMediaResult, + season?: TMDBSeasonMetaResult, +): MWMediaMeta { + const type = TMDBMediaToMediaType(media.object_type); + let seasons: undefined | MWSeasonMeta[]; + if (type === MWMediaType.SERIES) { + seasons = media.seasons + ?.sort((a, b) => a.season_number - b.season_number) + .map( + (v): MWSeasonMeta => ({ + title: v.title, + id: v.id.toString(), + number: v.season_number, + }), + ); + } -export interface TMDBShowData { - adult: boolean; - backdrop_path: string | null; - created_by: { - id: number; - credit_id: string; - name: string; - gender: number; - profile_path: string | null; - }[]; - episode_run_time: number[]; - first_air_date: string; - genres: { - id: number; - name: string; - }[]; - homepage: string; - id: number; - in_production: boolean; - languages: string[]; - last_air_date: string; - last_episode_to_air: { - id: number; - name: string; - overview: string; - vote_average: number; - vote_count: number; - air_date: string; - episode_number: number; - production_code: string; - runtime: number | null; - season_number: number; - show_id: number; - still_path: string | null; - } | null; - name: string; - next_episode_to_air: { - id: number; - name: string; - overview: string; - vote_average: number; - vote_count: number; - air_date: string; - episode_number: number; - production_code: string; - runtime: number | null; - season_number: number; - show_id: number; - still_path: string | null; - } | null; - networks: { - id: number; - logo_path: string; - name: string; - origin_country: string; - }[]; - number_of_episodes: number; - number_of_seasons: number; - origin_country: string[]; - original_language: string; - original_name: string; - overview: string; - popularity: number; - poster_path: string | null; - production_companies: { - id: number; - logo_path: string | null; - name: string; - origin_country: string; - }[]; - production_countries: { - iso_3166_1: string; - name: string; - }[]; - seasons: { - air_date: string; - episode_count: number; - id: number; - name: string; - overview: string; - poster_path: string | null; - season_number: number; - }[]; - spoken_languages: { - english_name: string; - iso_639_1: string; - name: string; - }[]; - status: string; - tagline: string; - type: string; - vote_average: number; - vote_count: number; - external_ids: { - imdb_id: string | null; + return { + title: media.title, + id: media.id.toString(), + year: media.original_release_date?.getFullYear()?.toString(), + poster: media.poster, + type, + seasons: seasons as any, + seasonData: season + ? { + id: season.id.toString(), + number: season.season_number, + title: season.title, + episodes: season.episodes + .sort((a, b) => a.episode_number - b.episode_number) + .map((v) => ({ + id: v.id.toString(), + number: v.episode_number, + title: v.title, + air_date: v.air_date, + })), + } + : (undefined as any), }; } -export interface TMDBMovieData { - adult: boolean; - backdrop_path: string | null; - belongs_to_collection: { - id: number; - name: string; - poster_path: string | null; - backdrop_path: string | null; - } | null; - budget: number; - genres: { - id: number; - name: string; - }[]; - homepage: string | null; - id: number; - imdb_id: string | null; - original_language: string; - original_title: string; - overview: string | null; - popularity: number; - poster_path: string | null; - production_companies: { - id: number; - logo_path: string | null; - name: string; - origin_country: string; - }[]; - production_countries: { - iso_3166_1: string; - name: string; - }[]; - release_date: string; - revenue: number; - runtime: number | null; - spoken_languages: { - english_name: string; - iso_639_1: string; - name: string; - }[]; - status: string; - tagline: string | null; - title: string; - video: boolean; - vote_average: number; - vote_count: number; - external_ids: { - imdb_id: string | null; +export function formatTMDBMetaToMediaItem(media: TMDBMediaResult): MediaItem { + const type = TMDBMediaToMediaItemType(media.object_type); + + // Define the basic structure of MediaItem + const mediaItem: MediaItem = { + title: media.title, + id: media.id.toString(), + year: media.original_release_date?.getFullYear() ?? 0, + release_date: media.original_release_date, + poster: media.poster, + type, + seasons: undefined, + }; + + // If it's a TV show, include the seasons information + if (type === "show") { + const seasons = media.seasons?.map((season) => ({ + title: season.title, + id: season.id.toString(), + number: season.season_number, + })); + mediaItem.seasons = seasons as MWSeasonMeta[]; + } + + return mediaItem; +} + +export function TMDBIdToUrlId( + type: MWMediaType, + tmdbId: string, + title: string, +) { + return [ + "tmdb", + mediaTypeToTMDB(type), + tmdbId, + slugify(title, { lower: true, strict: true }), + ].join("-"); +} + +export function TMDBMediaToId(media: MWMediaMeta): string { + return TMDBIdToUrlId(media.type, media.id, media.title); +} + +export function mediaItemToId(media: MediaItem): string { + return TMDBIdToUrlId( + mediaItemTypeToMediaType(media.type), + media.id, + media.title, + ); +} + +export function decodeTMDBId( + paramId: string, +): { id: string; type: MWMediaType } | null { + const [prefix, type, id] = paramId.split("-", 3); + if (prefix !== "tmdb") return null; + let mediaType; + try { + mediaType = TMDBMediaToMediaType(type as TMDBContentTypes); + } catch { + return null; + } + return { + type: mediaType, + id, }; } -export interface TMDBEpisodeResult { - season: number; - number: number; - title: string; - ids: { - trakt: number; - tvdb: number; - imdb: string; - tmdb: number; +const tmdbBaseUrl1 = "https://api.themoviedb.org/3"; +const tmdbBaseUrl2 = "https://api.tmdb.org/3"; + +const apiKey = conf().TMDB_READ_API_KEY; + +const tmdbHeaders = { + accept: "application/json", + Authorization: `Bearer ${apiKey}`, +}; + +function abortOnTimeout(timeout: number): AbortSignal { + const controller = new AbortController(); + setTimeout(() => controller.abort(), timeout); + return controller.signal; +} + +export async function get(url: string, params?: object): Promise { + if (!apiKey) throw new Error("TMDB API key not set"); + try { + return await mwFetch(encodeURI(url), { + headers: tmdbHeaders, + baseURL: tmdbBaseUrl1, + params: { + ...params, + }, + signal: abortOnTimeout(5000), + }); + } catch (err) { + return mwFetch(encodeURI(url), { + headers: tmdbHeaders, + baseURL: tmdbBaseUrl2, + params: { + ...params, + }, + signal: abortOnTimeout(30000), + }); + } +} + +export async function multiSearch( + query: string, +): Promise<(TMDBMovieSearchResult | TMDBShowSearchResult)[]> { + const data = await get("search/multi", { + query, + include_adult: false, + language: "en-US", + page: 1, + }); + // filter out results that aren't movies or shows + const results = data.results.filter( + (r) => + r.media_type === TMDBContentTypes.MOVIE || + r.media_type === TMDBContentTypes.TV, + ); + return results; +} + +export async function generateQuickSearchMediaUrl( + query: string, +): Promise { + const data = await multiSearch(query); + if (data.length === 0) return undefined; + const result = data[0]; + const title = + result.media_type === TMDBContentTypes.MOVIE ? result.title : result.name; + + return `/media/${TMDBIdToUrlId( + TMDBMediaToMediaType(result.media_type), + result.id.toString(), + title, + )}`; +} + +// Conditional type which for inferring the return type based on the content type +type MediaDetailReturn = + T extends TMDBContentTypes.MOVIE + ? TMDBMovieData + : T extends TMDBContentTypes.TV + ? TMDBShowData + : never; + +export function getMediaDetails< + T extends TMDBContentTypes, + TReturn = MediaDetailReturn, +>(id: string, type: T): Promise { + if (type === TMDBContentTypes.MOVIE) { + return get(`/movie/${id}`, { append_to_response: "external_ids" }); + } + if (type === TMDBContentTypes.TV) { + return get(`/tv/${id}`, { append_to_response: "external_ids" }); + } + throw new Error("Invalid media type"); +} + +export function getMediaPoster(posterPath: string | null): string | undefined { + if (posterPath) return `https://image.tmdb.org/t/p/w342/${posterPath}`; +} + +export async function getEpisodes( + id: string, + season: number, +): Promise { + const data = await get(`/tv/${id}/season/${season}`); + return data.episodes.map((e) => ({ + id: e.id, + episode_number: e.episode_number, + title: e.name, + air_date: e.air_date, + })); +} + +export async function getMovieFromExternalId( + imdbId: string, +): Promise { + const data = await get(`/find/${imdbId}`, { + external_source: "imdb_id", + }); + + const movie = data.movie_results[0]; + if (!movie) return undefined; + + return movie.id.toString(); +} + +export function formatTMDBSearchResult( + result: TMDBMovieSearchResult | TMDBShowSearchResult, + mediatype: TMDBContentTypes, +): TMDBMediaResult { + const type = TMDBMediaToMediaType(mediatype); + if (type === MWMediaType.SERIES) { + const show = result as TMDBShowSearchResult; + return { + title: show.name, + poster: getMediaPoster(show.poster_path), + id: show.id, + original_release_date: new Date(show.first_air_date), + object_type: mediatype, + }; + } + + const movie = result as TMDBMovieSearchResult; + + return { + title: movie.title, + poster: getMediaPoster(movie.poster_path), + id: movie.id, + original_release_date: new Date(movie.release_date), + object_type: mediatype, }; } - -export interface TMDBEpisode { - air_date: string; - episode_number: number; - id: number; - name: string; - overview: string; - production_code: string; - runtime: number; - season_number: number; - show_id: number; - still_path: string | null; - vote_average: number; - vote_count: number; - crew: any[]; - guest_stars: any[]; -} - -export interface TMDBSeason { - _id: string; - air_date: string; - episodes: TMDBEpisode[]; - name: string; - overview: string; - id: number; - poster_path: string | null; - season_number: number; -} - -export interface ExternalIdMovieSearchResult { - movie_results: { - adult: boolean; - backdrop_path: string; - id: number; - title: string; - original_language: string; - original_title: string; - overview: string; - poster_path: string; - media_type: string; - genre_ids: number[]; - popularity: number; - release_date: string; - video: boolean; - vote_average: number; - vote_count: number; - }[]; - person_results: any[]; - tv_results: any[]; - tv_episode_results: any[]; - tv_season_results: any[]; -} - -export interface TMDBMovieSearchResult { - adult: boolean; - backdrop_path: string; - id: number; - title: string; - original_language: string; - original_title: string; - overview: string; - poster_path: string; - media_type: TMDBContentTypes.MOVIE; - genre_ids: number[]; - popularity: number; - release_date: string; - video: boolean; - vote_average: number; - vote_count: number; -} - -export interface TMDBShowSearchResult { - adult: boolean; - backdrop_path: string; - id: number; - name: string; - original_language: string; - original_name: string; - overview: string; - poster_path: string; - media_type: TMDBContentTypes.TV; - genre_ids: number[]; - popularity: number; - first_air_date: string; - vote_average: number; - vote_count: number; - origin_country: string[]; -} - -export interface TMDBSearchResult { - page: number; - results: (TMDBMovieSearchResult | TMDBShowSearchResult)[]; - total_pages: number; - total_results: number; -} From f6348e94ad6bc66efa93ebfb2bb5a82410cbf244 Mon Sep 17 00:00:00 2001 From: Abdullah Khan <85450222+AbdullahDaGoat@users.noreply.github.com> Date: Wed, 19 Jun 2024 01:25:09 -0400 Subject: [PATCH 6/7] Update BookmarksPart.tsx (Fixed Error Code) --- src/pages/parts/home/BookmarksPart.tsx | 59 +++++++++++++++----------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/src/pages/parts/home/BookmarksPart.tsx b/src/pages/parts/home/BookmarksPart.tsx index 4f9f13bc..94cccae1 100644 --- a/src/pages/parts/home/BookmarksPart.tsx +++ b/src/pages/parts/home/BookmarksPart.tsx @@ -17,39 +17,48 @@ export function BookmarksPart({ onItemsChange: (hasItems: boolean) => void; }) { const { t } = useTranslation(); - const progressItems = useProgressStore((s) => s.items); - const bookmarks = useBookmarkStore((s) => s.bookmarks); - const removeBookmark = useBookmarkStore((s) => s.removeBookmark); + const progressItems = useProgressStore((state) => state.items); + const bookmarks = useBookmarkStore((state) => state.bookmarks); + const removeBookmark = useBookmarkStore((state) => state.removeBookmark); const [editing, setEditing] = useState(false); const [gridRef] = useAutoAnimate(); const items = useMemo(() => { - let output: MediaItem[] = []; - Object.entries(bookmarks).forEach((entry) => { - output.push({ - id: entry[0], - ...entry[1], - }); + // Transform bookmarks object into an array of MediaItem + const transformedItems: MediaItem[] = Object.keys(bookmarks).map((id) => { + const { title, year, poster, type, updatedAt } = bookmarks[id]; + return { + id, + title, + year, + poster, + type, + updatedAt, + seasons: type === "show" ? [] : undefined, // Ensure seasons is defined for 'show' type + }; }); - output = output.sort((a, b) => { - const bookmarkA = bookmarks[a.id]; - const bookmarkB = bookmarks[b.id]; - const progressA = progressItems[a.id]; - const progressB = progressItems[b.id]; - const dateA = Math.max(bookmarkA.updatedAt, progressA?.updatedAt ?? 0); - const dateB = Math.max(bookmarkB.updatedAt, progressB?.updatedAt ?? 0); - - return dateB - dateA; + // Sort items based on the latest update time + transformedItems.sort((a, b) => { + const aUpdatedAt = Math.max( + bookmarks[a.id].updatedAt, + progressItems[a.id]?.updatedAt ?? 0, + ); + const bUpdatedAt = Math.max( + bookmarks[b.id].updatedAt, + progressItems[b.id]?.updatedAt ?? 0, + ); + return bUpdatedAt - aUpdatedAt; }); - return output; + + return transformedItems; }, [bookmarks, progressItems]); useEffect(() => { - onItemsChange(items.length > 0); + onItemsChange(items.length > 0); // Notify parent component if there are items }, [items, onItemsChange]); - if (items.length === 0) return null; + if (items.length === 0) return null; // If there are no items, return null return (
@@ -60,12 +69,12 @@ export function BookmarksPart({ - {items.map((v) => ( + {items.map((item) => ( removeBookmark(v.id)} + onClose={() => removeBookmark(item.id)} /> ))} From 84ad2ce70039bacb02cf2930b70539420d2a2f5d Mon Sep 17 00:00:00 2001 From: Abdullah Khan <85450222+AbdullahDaGoat@users.noreply.github.com> Date: Wed, 19 Jun 2024 01:54:21 -0400 Subject: [PATCH 7/7] Added a Light blur effect such that the modal stands out a bit more --- src/components/media/PopupModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/media/PopupModal.tsx b/src/components/media/PopupModal.tsx index b88e72fd..0b47241f 100644 --- a/src/components/media/PopupModal.tsx +++ b/src/components/media/PopupModal.tsx @@ -152,7 +152,7 @@ export function PopupModal({ return (