From 09ca2c23728dca7a8beff8727c876d8072a14622 Mon Sep 17 00:00:00 2001 From: Sebastian Gumprich Date: Mon, 13 Oct 2025 11:23:12 +0200 Subject: [PATCH 1/2] feat: add trivia page this is coded via AI - feel free to reject. --- src/components/title/DidYouKnow.tsx | 18 +- src/interfaces/shared/trivia.ts | 7 + src/pages/api/title/[titleId]/trivia.ts | 34 ++ src/pages/title/[titleId]/index.tsx | 2 +- src/pages/title/[titleId]/trivia/index.tsx | 436 ++++++++++++++++++ .../modules/pages/title/trivia.module.scss | 117 +++++ src/utils/constants/keys.ts | 1 + src/utils/fetchers/titleTrivia.ts | 99 ++++ 8 files changed, 709 insertions(+), 5 deletions(-) create mode 100644 src/interfaces/shared/trivia.ts create mode 100644 src/pages/api/title/[titleId]/trivia.ts create mode 100644 src/pages/title/[titleId]/trivia/index.tsx create mode 100644 src/styles/modules/pages/title/trivia.module.scss create mode 100644 src/utils/fetchers/titleTrivia.ts diff --git a/src/components/title/DidYouKnow.tsx b/src/components/title/DidYouKnow.tsx index 66c5ab2..5f28f18 100644 --- a/src/components/title/DidYouKnow.tsx +++ b/src/components/title/DidYouKnow.tsx @@ -1,12 +1,13 @@ import Link from 'next/link'; -import { DidYouKnow } from 'src/interfaces/shared/title'; +import type { DidYouKnow as DidYouKnowType } from 'src/interfaces/shared/title'; import styles from 'src/styles/modules/components/title/did-you-know.module.scss'; type Props = { - data: DidYouKnow; + data: DidYouKnowType; + titleId: string; }; -const DidYouKnow = ({ data }: Props) => { +const DidYouKnow = ({ data, titleId }: Props) => { if (!Object.keys(data).length) return (
@@ -20,7 +21,16 @@ const DidYouKnow = ({ data }: Props) => {
{data.trivia && (
-

Trivia

+

+ Trivia + {data.trivia.total > 1 && ( + + + See all {data.trivia.total} trivia ยป + + + )} +

>; +export type { Trivia as default }; + +export type TriviaItem = Trivia['items'][0]; +export type TriviaMeta = Trivia['meta']; diff --git a/src/pages/api/title/[titleId]/trivia.ts b/src/pages/api/title/[titleId]/trivia.ts new file mode 100644 index 0000000..6daea2a --- /dev/null +++ b/src/pages/api/title/[titleId]/trivia.ts @@ -0,0 +1,34 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import trivia from 'src/utils/fetchers/titleTrivia'; +import { getErrorProperties } from 'src/utils/helpers'; +import getOrSetApiCache from 'src/utils/getOrSetApiCache'; +import { titleTriviaKey } from 'src/utils/constants/keys'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const { titleId, page } = req.query; + + if (typeof titleId !== 'string') { + return res.status(400).json({ error: 'Invalid title ID' }); + } + + const pageNum = typeof page === 'string' ? parseInt(page, 10) : 1; + + if (isNaN(pageNum) || pageNum < 1) { + return res.status(400).json({ error: 'Invalid page number' }); + } + + try { + // Cache each page separately + const cacheKey = `${titleTriviaKey(titleId)}:page:${pageNum}`; + const data = await getOrSetApiCache(cacheKey, trivia, titleId, pageNum); + + return res.status(200).json(data); + } catch (e) { + const err = getErrorProperties(e); + console.error('Error fetching trivia:', err); + return res.status(err.statusCode).json({ + error: err.message, + statusCode: err.statusCode, + }); + } +} diff --git a/src/pages/title/[titleId]/index.tsx b/src/pages/title/[titleId]/index.tsx index a6ed616..f75804e 100644 --- a/src/pages/title/[titleId]/index.tsx +++ b/src/pages/title/[titleId]/index.tsx @@ -39,7 +39,7 @@ const TitleInfo = ({ data, error, originalPath }: Props) => {
- +
diff --git a/src/pages/title/[titleId]/trivia/index.tsx b/src/pages/title/[titleId]/trivia/index.tsx new file mode 100644 index 0000000..905b420 --- /dev/null +++ b/src/pages/title/[titleId]/trivia/index.tsx @@ -0,0 +1,436 @@ +import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; +import Link from 'next/link'; +import { useState, useEffect } from 'react'; +import Meta from 'src/components/meta/Meta'; +import Layout from 'src/components/layout'; +import ErrorInfo from 'src/components/error/ErrorInfo'; +import Trivia from 'src/interfaces/shared/trivia'; +import type { AppError } from 'src/interfaces/shared/error'; +import getOrSetApiCache from 'src/utils/getOrSetApiCache'; +import trivia from 'src/utils/fetchers/titleTrivia'; +import { getErrorProperties, getProxiedIMDbImgUrl } from 'src/utils/helpers'; +import { titleTriviaKey } from 'src/utils/constants/keys'; +import styles from 'src/styles/modules/pages/title/trivia.module.scss'; + +type Props = InferGetServerSidePropsType; + +const TriviaPage = ({ data, error, originalPath }: Props) => { + const [regularItems, setRegularItems] = useState( + data?.items?.filter(item => !item.isSpoiler) || [] + ); + const [spoilerItems, setSpoilerItems] = useState( + data?.items?.filter(item => item.isSpoiler) || [] + ); + const [regularPage, setRegularPage] = useState(1); + const [spoilerPage, setSpoilerPage] = useState(1); + const [isLoadingRegular, setIsLoadingRegular] = useState(false); + const [isLoadingSpoiler, setIsLoadingSpoiler] = useState(false); + const [isLoadingAllRegular, setIsLoadingAllRegular] = useState(false); + const [isLoadingAllSpoiler, setIsLoadingAllSpoiler] = useState(false); + const [hasMoreRegular, setHasMoreRegular] = useState(data?.hasMore || false); + const [hasMoreSpoiler, setHasMoreSpoiler] = useState(data?.hasMore || false); + const [totalRegular, setTotalRegular] = useState(0); + const [totalSpoiler, setTotalSpoiler] = useState(0); + const [showCopiedToast, setShowCopiedToast] = useState(false); + + // Calculate totals for each category on mount + useEffect(() => { + if (data?.items) { + const regularCount = data.items.filter(item => !item.isSpoiler).length; + const spoilerCount = data.items.filter(item => item.isSpoiler).length; + + // Estimate totals based on ratio if we don't have exact counts + if (data.total > 0 && regularCount + spoilerCount > 0) { + const ratio = data.total / (regularCount + spoilerCount); + setTotalRegular(Math.ceil(regularCount * ratio)); + setTotalSpoiler(Math.ceil(spoilerCount * ratio)); + } + } + }, [data]); + + if (error) return ; + + const { meta, total } = data; + const totalItems = regularItems.length + spoilerItems.length; + + const loadMoreRegular = async () => { + if (isLoadingRegular || !hasMoreRegular) return; + + setIsLoadingRegular(true); + try { + const response = await fetch(`/api/title/${meta.titleId}/trivia?page=${regularPage + 1}`); + const newData = await response.json(); + + if (newData.items && newData.items.length > 0) { + const regular = newData.items.filter((item: any) => !item.isSpoiler); + + if (regular.length > 0) { + setRegularItems(prev => [...prev, ...regular]); + setRegularPage(prev => prev + 1); + } + + setHasMoreRegular(newData.hasMore); + } else { + setHasMoreRegular(false); + } + } catch (err) { + console.error('Error loading more regular trivia:', err); + setHasMoreRegular(false); + } finally { + setIsLoadingRegular(false); + } + }; + + const loadMoreSpoiler = async () => { + if (isLoadingSpoiler || !hasMoreSpoiler) return; + + setIsLoadingSpoiler(true); + try { + const response = await fetch(`/api/title/${meta.titleId}/trivia?page=${spoilerPage + 1}`); + const newData = await response.json(); + + if (newData.items && newData.items.length > 0) { + const spoilers = newData.items.filter((item: any) => item.isSpoiler); + + if (spoilers.length > 0) { + setSpoilerItems(prev => [...prev, ...spoilers]); + setSpoilerPage(prev => prev + 1); + } + + setHasMoreSpoiler(newData.hasMore); + } else { + setHasMoreSpoiler(false); + } + } catch (err) { + console.error('Error loading more spoiler trivia:', err); + setHasMoreSpoiler(false); + } finally { + setIsLoadingSpoiler(false); + } + }; + + const loadAllRegular = async () => { + if (isLoadingAllRegular || !hasMoreRegular) return; + + setIsLoadingAllRegular(true); + try { + let currentPage = regularPage + 1; + let hasMore: boolean = hasMoreRegular; + + while (hasMore) { + const response = await fetch(`/api/title/${meta.titleId}/trivia?page=${currentPage}`); + const newData = await response.json(); + + if (newData.items && newData.items.length > 0) { + const regular = newData.items.filter((item: any) => !item.isSpoiler); + + if (regular.length > 0) { + setRegularItems(prev => [...prev, ...regular]); + } + + hasMore = newData.hasMore; + currentPage++; + } else { + hasMore = false; + } + } + + setRegularPage(currentPage - 1); + setHasMoreRegular(false); + } catch (err) { + console.error('Error loading all regular trivia:', err); + setHasMoreRegular(false); + } finally { + setIsLoadingAllRegular(false); + } + }; + + const loadAllSpoiler = async () => { + if (isLoadingAllSpoiler || !hasMoreSpoiler) return; + + setIsLoadingAllSpoiler(true); + try { + let currentPage = spoilerPage + 1; + let hasMore: boolean = hasMoreSpoiler; + + while (hasMore) { + const response = await fetch(`/api/title/${meta.titleId}/trivia?page=${currentPage}`); + const newData = await response.json(); + + if (newData.items && newData.items.length > 0) { + const spoilers = newData.items.filter((item: any) => item.isSpoiler); + + if (spoilers.length > 0) { + setSpoilerItems(prev => [...prev, ...spoilers]); + } + + hasMore = newData.hasMore; + currentPage++; + } else { + hasMore = false; + } + } + + setSpoilerPage(currentPage - 1); + setHasMoreSpoiler(false); + } catch (err) { + console.error('Error loading all spoiler trivia:', err); + setHasMoreSpoiler(false); + } finally { + setIsLoadingAllSpoiler(false); + } + }; + + const copyLinkToClipboard = (index: number, isSpoiler: boolean) => { + const anchor = isSpoiler ? `spoiler-${index}` : `regular-${index}`; + const url = `${window.location.origin}${window.location.pathname}#${anchor}`; + navigator.clipboard.writeText(url).then(() => { + setShowCopiedToast(true); + setTimeout(() => setShowCopiedToast(false), 2000); + }); + }; + + return ( + <> + + + <> + {showCopiedToast && ( +
+ โœ“ Link copied to clipboard! +
+ )} +
+ + +
+

+ {meta.title} {meta.year} +

+
+ +
+

+ Trivia + {total > 0 && ( + + ({totalItems} of {total} {total === 1 ? 'item' : 'items'}) + + )} +

+ + {totalItems > 0 ? ( + <> + {regularItems.length > 0 && ( + <> +

+ Regular Trivia ({regularItems.length} + {totalRegular > 0 && ` of ${totalRegular}`}) +

+
+ {regularItems.map((item, index) => ( +
+
+ + #{index + 1} + + +
+
+
+ ))} +
+ + {hasMoreRegular && ( +
+ + +
+ )} + + )} + + {spoilerItems.length > 0 && ( + <> +

+ Spoiler Trivia ({spoilerItems.length} + {totalSpoiler > 0 && ` of ${totalSpoiler}`}) +

+
+ {spoilerItems.map((item, index) => ( +
+
+
+
โš  Spoiler
+
+
+ + #{index + 1} + + +
+
+
+
+ ))} +
+ + {hasMoreSpoiler && ( +
+ + +
+ )} + + )} + + ) : ( +
+

No trivia available for this title.

+
+ )} +
+
+ +
+ + ); +}; + +type Data = ({ data: Trivia; error: null } | { error: AppError; data: null }) & { + originalPath: string; +}; +type Params = { titleId: string }; + +export const getServerSideProps: GetServerSideProps = async ctx => { + const titleId = ctx.params!.titleId; + const originalPath = ctx.resolvedUrl; + + try { + // Fetch only the first page on server-side + const cacheKey = `${titleTriviaKey(titleId)}:page:1`; + const data = await getOrSetApiCache(cacheKey, trivia, titleId, 1); + + return { props: { data, error: null, originalPath } }; + } catch (e) { + const err = getErrorProperties(e); + ctx.res.statusCode = err.statusCode; + ctx.res.statusMessage = err.message; + + const error = { + message: err.message, + statusCode: err.statusCode, + stack: err.format(), + }; + console.error(err); + return { props: { error, data: null, originalPath } }; + } +}; + +export default TriviaPage; diff --git a/src/styles/modules/pages/title/trivia.module.scss b/src/styles/modules/pages/title/trivia.module.scss new file mode 100644 index 0000000..262c6ba --- /dev/null +++ b/src/styles/modules/pages/title/trivia.module.scss @@ -0,0 +1,117 @@ +.trivia { + padding: 2rem; + max-width: 1200px; + margin: 0 auto; +} + +.backLink { + margin-bottom: 2rem; + font-size: 0.9rem; +} + +.header { + margin-bottom: 2rem; +} + +.section { + margin-top: 2rem; +} + +.sectionHeader { + margin-bottom: 1.5rem; +} + +.count { + color: #888; + font-size: 0.9em; + margin-left: 1rem; +} + +.triviaList { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.triviaItem { + padding: 1.5rem; + background-color: var(--color-background-secondary, #1a1a1a); + border-radius: 8px; + border-left: 4px solid var(--color-primary, #f5c518); +} + +.triviaItemSpoiler { + composes: triviaItem; + border-left-color: #dc2626; +} + +.spoilerBadge { + display: inline-block; + padding: 0.25rem 0.5rem; + margin-bottom: 0.75rem; + background-color: #dc2626; + color: #fff; + font-size: 0.75rem; + font-weight: bold; + border-radius: 4px; + text-transform: uppercase; +} + +.triviaContent { + line-height: 1.7; + font-size: 1rem; +} + +.emptyState { + padding: 3rem 2rem; + text-align: center; + background-color: var(--color-background-secondary, #1a1a1a); + border-radius: 8px; +} + +.emptyText { + color: #888; + font-size: 1.1rem; +} + +.loadMoreButton { + padding: 0.75rem 2rem; + font-size: 1rem; + background-color: var(--color-primary, #f5c518); + color: #000; + border: none; + border-radius: 4px; + cursor: pointer; + font-weight: 600; + transition: all 0.2s ease; + + &:hover:not(:disabled) { + background-color: #e5b508; + transform: translateY(-1px); + } + + &:active:not(:disabled) { + transform: translateY(0); + } + + &:disabled { + cursor: not-allowed; + opacity: 0.6; + } +} + +.loadMoreContainer { + text-align: center; + margin-top: 2rem; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/src/utils/constants/keys.ts b/src/utils/constants/keys.ts index 25ebafb..cee11f3 100644 --- a/src/utils/constants/keys.ts +++ b/src/utils/constants/keys.ts @@ -1,6 +1,7 @@ export const titleKey = (titleId: string) => `title:${titleId}`; export const titleReviewsKey = (titleId: string, queryStr: string, paginationKey: string | null) => `title/reviews:${titleId}?${queryStr}&paginationKey=${paginationKey}`; +export const titleTriviaKey = (titleId: string) => `title/trivia:${titleId}`; export const nameKey = (nameId: string) => `name:${nameId}`; export const listKey = (listId: string, pageNum = '1') => `list:${listId}?page=${pageNum}`; export const findKey = (query: string) => `find:${query}`; diff --git a/src/utils/fetchers/titleTrivia.ts b/src/utils/fetchers/titleTrivia.ts new file mode 100644 index 0000000..8022e76 --- /dev/null +++ b/src/utils/fetchers/titleTrivia.ts @@ -0,0 +1,99 @@ +import * as cheerio from 'cheerio'; +import axiosInstance, { isSaneError } from 'src/utils/axiosInstance'; +import { AppError } from 'src/utils/helpers'; + +const trivia = async (titleId: string, page: number = 1) => { + try { + // Fetch the trivia page (with optional page parameter) + const url = page > 1 ? `/title/${titleId}/trivia?page=${page}` : `/title/${titleId}/trivia`; + + const res = await axiosInstance(url); + const $ = cheerio.load(res.data); + const rawData = $('script#__NEXT_DATA__').text(); + + if (!rawData) { + throw new AppError('Could not find trivia data', 404); + } + + let parsedData; + try { + parsedData = JSON.parse(rawData); + } catch (e) { + throw new AppError('Invalid JSON data', 500); + } + + const pageProps = parsedData?.props?.pageProps; + + if (!pageProps) { + throw new AppError('Invalid data structure - no pageProps', 500); + } + + // Extract metadata + const mainData = pageProps.contentData?.entityMetadata; + const triviaCategories = pageProps.contentData?.data?.title?.triviaCategories; + const titleData = pageProps.contentData?.data?.title; + + const meta = { + title: mainData?.titleText?.text || mainData?.originalTitleText?.text || '', + year: mainData?.releaseYear?.year ? `(${mainData.releaseYear.year})` : '', + image: mainData?.primaryImage?.url || null, + titleId: mainData?.id || titleId, + }; + + // Extract trivia items from triviaCategories array + const triviaList: Array<{ html: string; text: string; isSpoiler?: boolean }> = []; + + if (triviaCategories && typeof triviaCategories === 'object') { + const categoriesArray = Object.values(triviaCategories); + + categoriesArray.forEach((category: any) => { + // Extract regular trivia + if (category?.trivia?.edges && Array.isArray(category.trivia.edges)) { + category.trivia.edges.forEach((edge: any) => { + const htmlContent = edge?.node?.displayableArticle?.body?.plaidHtml; + if (htmlContent) { + triviaList.push({ + html: htmlContent, + text: htmlContent.replace(/<[^>]*>/g, '').trim(), + }); + } + }); + } + + // Extract spoiler trivia + if (category?.spoilerTrivia?.edges && Array.isArray(category.spoilerTrivia.edges)) { + category.spoilerTrivia.edges.forEach((edge: any) => { + const htmlContent = edge?.node?.displayableArticle?.body?.plaidHtml; + if (htmlContent) { + triviaList.push({ + html: htmlContent, + text: htmlContent.replace(/<[^>]*>/g, '').trim(), + isSpoiler: true, + }); + } + }); + } + }); + } + + // Get total count from subNavTrivia + const total = titleData?.subNavTrivia?.total || triviaList.length; + + return { + meta, + total, + items: triviaList, + hasMore: triviaList.length > 0 && triviaList.length < total, + }; + } catch (err) { + if (isSaneError(err) && err.response?.status === 404) { + throw new AppError('not found', 404, err); + } + + if (err instanceof AppError) throw err; + + throw new AppError('something went wrong', 500, err); + } +}; + +export default trivia; From 0ee31abf4ba796d1e135edf1d9a9c513ad1568cc Mon Sep 17 00:00:00 2001 From: Sebastian Gumprich Date: Mon, 13 Oct 2025 17:48:09 +0200 Subject: [PATCH 2/2] feat: change styling to better match overall project --- src/pages/title/[titleId]/trivia/index.tsx | 145 +++++--------- .../modules/pages/title/trivia.module.scss | 184 ++++++++++++++---- 2 files changed, 191 insertions(+), 138 deletions(-) diff --git a/src/pages/title/[titleId]/trivia/index.tsx b/src/pages/title/[titleId]/trivia/index.tsx index 905b420..17226b9 100644 --- a/src/pages/title/[titleId]/trivia/index.tsx +++ b/src/pages/title/[titleId]/trivia/index.tsx @@ -199,28 +199,11 @@ const TriviaPage = ({ data, error, originalPath }: Props) => { /> <> - {showCopiedToast && ( -
- โœ“ Link copied to clipboard! -
- )} + {showCopiedToast &&
โœ“ Link copied to clipboard!
}
@@ -244,7 +227,7 @@ const TriviaPage = ({ data, error, originalPath }: Props) => { <> {regularItems.length > 0 && ( <> -

+

Regular Trivia ({regularItems.length} {totalRegular > 0 && ` of ${totalRegular}`})

@@ -255,27 +238,11 @@ const TriviaPage = ({ data, error, originalPath }: Props) => { id={`regular-${index}`} className={styles.triviaItem} > -
- - #{index + 1} - +
+ #{index + 1} @@ -314,10 +279,7 @@ const TriviaPage = ({ data, error, originalPath }: Props) => { {spoilerItems.length > 0 && ( <> -

+

Spoiler Trivia ({spoilerItems.length} {totalSpoiler > 0 && ` of ${totalSpoiler}`})

@@ -328,36 +290,16 @@ const TriviaPage = ({ data, error, originalPath }: Props) => { id={`spoiler-${index}`} className={styles.triviaItemSpoiler} > -
-
-
โš  Spoiler
-
-
- - #{index + 1} - - -
+
โš  Spoiler
+
+ #{index + 1} +
{ onClick={loadMoreSpoiler} disabled={isLoadingSpoiler || isLoadingAllSpoiler} className={styles.loadMoreButton} - style={{ marginRight: '1rem' }} > {isLoadingSpoiler ? 'Loading...' : 'Load More'} @@ -403,34 +343,37 @@ const TriviaPage = ({ data, error, originalPath }: Props) => { ); }; -type Data = ({ data: Trivia; error: null } | { error: AppError; data: null }) & { - originalPath: string; +export default TriviaPage; + +type Data = { + meta: Trivia['meta']; + items: Trivia['items']; + total: number; + hasMore: boolean; }; type Params = { titleId: string }; -export const getServerSideProps: GetServerSideProps = async ctx => { - const titleId = ctx.params!.titleId; - const originalPath = ctx.resolvedUrl; +const getServerSideProps: GetServerSideProps< + | { data: Data; error: null; originalPath: string } + | { data: null; error: AppError; originalPath: string } +> = async context => { + const { titleId } = context.params as Params; + const key = titleTriviaKey(titleId); + const originalPath = context.resolvedUrl; try { - // Fetch only the first page on server-side - const cacheKey = `${titleTriviaKey(titleId)}:page:1`; - const data = await getOrSetApiCache(cacheKey, trivia, titleId, 1); - - return { props: { data, error: null, originalPath } }; - } catch (e) { - const err = getErrorProperties(e); - ctx.res.statusCode = err.statusCode; - ctx.res.statusMessage = err.message; - - const error = { - message: err.message, - statusCode: err.statusCode, - stack: err.format(), + const data = await getOrSetApiCache(key, trivia, titleId, 1); + return { + props: { + data, + error: null, + originalPath, + }, }; - console.error(err); - return { props: { error, data: null, originalPath } }; + } catch (error) { + const errorProperties = getErrorProperties(error); + return { props: { data: null, error: errorProperties, originalPath } }; } }; -export default TriviaPage; +export { getServerSideProps }; diff --git a/src/styles/modules/pages/title/trivia.module.scss b/src/styles/modules/pages/title/trivia.module.scss index 262c6ba..60eb71e 100644 --- a/src/styles/modules/pages/title/trivia.module.scss +++ b/src/styles/modules/pages/title/trivia.module.scss @@ -1,43 +1,65 @@ +@use '../../../abstracts' as helper; + .trivia { - padding: 2rem; - max-width: 1200px; - margin: 0 auto; + --doc-whitespace: var(--spacer-8); + --comp-whitespace: var(--spacer-3); + + display: grid; + gap: var(--doc-whitespace); + padding: var(--doc-whitespace); + + @include helper.bp('bp-700') { + --doc-whitespace: var(--spacer-5); + } + + @include helper.bp('bp-450') { + padding: var(--spacer-3); + } } .backLink { - margin-bottom: 2rem; - font-size: 0.9rem; + margin-bottom: var(--comp-whitespace); + font-size: 0.9em; + + a { + @include helper.prettify-link(var(--clr-link)); + } } .header { - margin-bottom: 2rem; + margin-bottom: var(--comp-whitespace); } .section { - margin-top: 2rem; + display: grid; + gap: var(--comp-whitespace); } .sectionHeader { - margin-bottom: 1.5rem; + margin-bottom: var(--spacer-2); } .count { - color: #888; + color: var(--clr-text-muted); font-size: 0.9em; - margin-left: 1rem; + margin-left: var(--spacer-2); } .triviaList { - display: flex; - flex-direction: column; - gap: 2rem; + display: grid; + gap: var(--comp-whitespace); } .triviaItem { - padding: 1.5rem; - background-color: var(--color-background-secondary, #1a1a1a); - border-radius: 8px; - border-left: 4px solid var(--color-primary, #f5c518); + padding: var(--spacer-3); + background-color: var(--clr-bg-accent); + border-radius: 0.5rem; + border-left: 4px solid var(--clr-fill); + transition: background-color 200ms ease; + + &:hover { + background-color: var(--clr-bg-muted); + } } .triviaItemSpoiler { @@ -47,46 +69,89 @@ .spoilerBadge { display: inline-block; - padding: 0.25rem 0.5rem; - margin-bottom: 0.75rem; + padding: var(--spacer-0) var(--spacer-1); + margin-bottom: var(--spacer-1); background-color: #dc2626; color: #fff; font-size: 0.75rem; font-weight: bold; - border-radius: 4px; + border-radius: 0.25rem; text-transform: uppercase; + letter-spacing: 0.05em; } .triviaContent { - line-height: 1.7; - font-size: 1rem; + line-height: var(--line-height); + color: var(--clr-text); + + p { + margin-bottom: var(--spacer-1); + + &:last-child { + margin-bottom: 0; + } + } + + a { + @include helper.prettify-link(var(--clr-link)); + } +} + +.triviaHeader { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: var(--spacer-1); +} + +.triviaNumber { + color: var(--clr-text-muted); + font-size: 0.85em; +} + +.shareButton { + background: none; + border: none; + color: var(--clr-text-muted); + cursor: pointer; + font-size: 0.85em; + padding: var(--spacer-0) var(--spacer-1); + transition: color 200ms ease; + + &:hover { + color: var(--clr-link); + } + + &:focus-visible { + @include helper.focus-rules; + } } .emptyState { - padding: 3rem 2rem; + padding: var(--spacer-5) var(--spacer-3); text-align: center; - background-color: var(--color-background-secondary, #1a1a1a); - border-radius: 8px; + background-color: var(--clr-bg-accent); + border-radius: 0.5rem; } .emptyText { - color: #888; - font-size: 1.1rem; + color: var(--clr-text-muted); + font-size: 1.1em; } .loadMoreButton { - padding: 0.75rem 2rem; + padding: var(--spacer-1) var(--spacer-3); font-size: 1rem; - background-color: var(--color-primary, #f5c518); - color: #000; + background-color: var(--clr-fill); + color: var(--clr-bg); border: none; - border-radius: 4px; + border-radius: 0.25rem; cursor: pointer; font-weight: 600; - transition: all 0.2s ease; + transition: all 200ms ease; &:hover:not(:disabled) { - background-color: #e5b508; + opacity: 0.85; transform: translateY(-1px); } @@ -94,21 +159,66 @@ transform: translateY(0); } + &:focus-visible { + @include helper.focus-rules; + } + &:disabled { cursor: not-allowed; - opacity: 0.6; + opacity: 0.5; } } +.loadMoreButtonSecondary { + composes: loadMoreButton; + background-color: var(--clr-link); + color: var(--clr-bg); +} + .loadMoreContainer { - text-align: center; - margin-top: 2rem; + display: flex; + gap: var(--spacer-2); + justify-content: center; + margin-top: var(--comp-whitespace); + + @include helper.bp('bp-450') { + flex-direction: column; + } +} + +.categoryHeader { + margin-top: var(--doc-whitespace); + margin-bottom: var(--spacer-2); + + &:first-of-type { + margin-top: 0; + } +} + +.toast { + position: fixed; + top: var(--spacer-3); + right: var(--spacer-3); + background-color: #4caf50; + color: #fff; + padding: var(--spacer-2) var(--spacer-3); + border-radius: 0.25rem; + box-shadow: var(--clr-shadow); + z-index: 1000; + animation: fadeIn 300ms ease-in; + + @include helper.bp('bp-450') { + top: var(--spacer-2); + right: var(--spacer-2); + left: var(--spacer-2); + text-align: center; + } } @keyframes fadeIn { from { opacity: 0; - transform: translateY(-10px); + transform: translateY(-1rem); } to { opacity: 1;