Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions web/app/hooks/useMidnightRevalidate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useEffect, useRef } from 'react'
import { useRevalidator } from 'react-router'

export function useMidnightRevalidate(hasDateOverride: boolean) {
const revalidator = useRevalidator()
const timeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)

useEffect(() => {
if (hasDateOverride) return

const schedule = () => {
timeoutRef.current = setTimeout(() => {
revalidator.revalidate()
schedule()
}, msUntilNextUtcMidnightWithBuffer(30_000))
}

schedule()

return () => {
if (timeoutRef.current !== undefined) {
clearTimeout(timeoutRef.current)
timeoutRef.current = undefined
}
}
}, [hasDateOverride, revalidator])
}

const msUntilNextUtcMidnightWithBuffer = (bufferMs = 30_000) => {
const now = new Date()
const next = new Date(now)
next.setUTCHours(24, 0, 0, 0)
return Math.max(0, next.getTime() - now.getTime() + bufferMs)
}
12 changes: 6 additions & 6 deletions web/app/hooks/useScreenPagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ export function useScreenPagination<T>(
)

const [currentPage, setCurrentPage] = useState(Math.min(articlePage, totalPages - 1))
const timeoutRef = useRef<number | undefined>(undefined)
const intervalRef = useRef<number | undefined>(undefined)
const timeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
const intervalRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)

useEffect(() => {
if (!articleAmount || totalPages <= 1) return
Expand All @@ -23,20 +23,20 @@ export function useScreenPagination<T>(
const nowSeconds = Math.floor(Date.now() / 1000)
const untilBoundary = intervalSeconds - (nowSeconds % intervalSeconds)

timeoutRef.current = window.setTimeout(() => {
timeoutRef.current = setTimeout(() => {
setCurrentPage((page) => (page + 1) % totalPages)
intervalRef.current = window.setInterval(() => {
intervalRef.current = setInterval(() => {
setCurrentPage((page) => (page + 1) % totalPages)
}, rotationIntervalMs)
}, untilBoundary * 1000)

return () => {
if (timeoutRef.current !== undefined) {
window.clearTimeout(timeoutRef.current)
clearTimeout(timeoutRef.current)
timeoutRef.current = undefined
}
if (intervalRef.current !== undefined) {
window.clearInterval(intervalRef.current)
clearInterval(intervalRef.current)
intervalRef.current = undefined
}
}
Expand Down
30 changes: 28 additions & 2 deletions web/app/routes/screen.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { useEffect, useRef } from 'react'
import {
isRouteErrorResponse,
LoaderFunctionArgs,
MetaFunction,
redirect,
useLoaderData,
useRouteError,
useLocation,
useRevalidator,
} from 'react-router'
import { combinedHeaders } from 'utils/headers'
import { isNumericString } from 'utils/numbers'
Expand All @@ -17,9 +20,20 @@ import { ErrorPage } from '~/features/error-boundary/ErrorPage'
import { useScreenPagination } from '../hooks/useScreenPagination'
import { LandscapeView } from '~/features/screen/LandscapeView'
import { PortraitView } from '~/features/screen/PortraitView'
import { useMidnightRevalidate } from '~/hooks/useMidnightRevalidate'

const ROTATION_INTERVAL_DEFAULT = 30

type ScreenLoaderData = {
posts: POSTS_BY_YEAR_AND_DATEResult
year: string
date: string
articleAmount?: number
articlePage: number
rotationIntervalMs: number
hasDateOverride: boolean
}

export const meta: MetaFunction = () => {
const title = `Forhåndsvisning av årets Bekk Christmas`
const description = `Forhåndsvisning av årets Bekk Christmas julekalender for skjermpresentasjon`
Expand Down Expand Up @@ -80,14 +94,24 @@ export async function loader({ request }: LoaderFunctionArgs) {
? Math.max(5000, Number(rotationIntervalParam) * 1000)
: ROTATION_INTERVAL_DEFAULT * 1000

return {
const data: ScreenLoaderData = {
posts,
year,
date,
articleAmount,
articlePage,
rotationIntervalMs,
hasDateOverride: Boolean(dateOverrideParam),
}

// When dateOverride is not present, the same URL should not be cached across days.
const headers: HeadersInit | undefined = dateOverrideParam
? undefined
: {
'Cache-Control': 'no-store',
}

return Response.json(data, { headers })
} catch (error) {
console.error('Error loading posts:', error)
throw new Response('Error loading posts', { status: 500 })
Expand All @@ -97,8 +121,10 @@ export async function loader({ request }: LoaderFunctionArgs) {
export const headers = combinedHeaders

export default function ScreenPreviewRoute() {
const { posts, year, date, articleAmount, articlePage, rotationIntervalMs } = useLoaderData<typeof loader>()
const { posts, year, date, articleAmount, articlePage, rotationIntervalMs, hasDateOverride } =
useLoaderData<ScreenLoaderData>()
const { visiblePosts, currentPage } = useScreenPagination(posts, articleAmount, articlePage, rotationIntervalMs)
useMidnightRevalidate(hasDateOverride)

return (
<>
Expand Down