diff --git a/DorkNet.Server/admin-ui/src/lib/types.ts b/DorkNet.Server/admin-ui/src/lib/types.ts index c625a7d..c082069 100644 --- a/DorkNet.Server/admin-ui/src/lib/types.ts +++ b/DorkNet.Server/admin-ui/src/lib/types.ts @@ -38,13 +38,18 @@ export interface PlayerDetail extends Player { // 2020 watch uses (cropSquare=1 produces a face-zoomed square, sig // matches the host's signing key id). export function profileImageUrl(name: string | null | undefined, width = 96): string | null { - if (!name) return null; - const apex = adminApex(); - const scheme = apex === 'localhost' ? 'http' : 'https'; - return `${scheme}://img.${apex}/${encodeURIComponent(name)}?width=${width}&cropSquare=1&sig=p1`; + return imageCdnUrl(name, `width=${width}&cropSquare=1&sig=p1`); } -function adminApex(): string { +export function imageCdnUrl(name: string | null | undefined, query?: string): string | null { + return subdomainUrl('img', name, query, false); +} + +export function assetCdnUrl(path: string | null | undefined): string | null { + return subdomainUrl('cdn', path, undefined, true); +} + +export function adminApex(): string { if (typeof window === 'undefined') return 'rec.net'; const host = window.location.hostname.toLowerCase(); @@ -54,6 +59,17 @@ function adminApex(): string { return parts.length > 2 ? parts.slice(1).join('.') : host; } +function subdomainUrl(subdomain: string, path: string | null | undefined, query?: string, preserveSlashes = false): string | null { + if (!path) return null; + const apex = adminApex(); + const scheme = apex === 'localhost' ? 'http' : 'https'; + const suffix = query ? `?${query}` : ''; + const encodedPath = preserveSlashes + ? path.split('/').map(encodeURIComponent).join('/') + : encodeURIComponent(path); + return `${scheme}://${subdomain}.${apex}/${encodedPath}${suffix}`; +} + export interface Room { id: number; name: string; diff --git a/DorkNet.Server/admin-ui/src/pages/CommunityBoard.tsx b/DorkNet.Server/admin-ui/src/pages/CommunityBoard.tsx index f0ac6ce..db214b5 100644 --- a/DorkNet.Server/admin-ui/src/pages/CommunityBoard.tsx +++ b/DorkNet.Server/admin-ui/src/pages/CommunityBoard.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from 'react'; import { api, get } from '../lib/api'; -import type { CommunityBoardState, Player, Room } from '../lib/types'; +import { assetCdnUrl, imageCdnUrl, type CommunityBoardState, type Player, type Room } from '../lib/types'; import { useApi } from '../lib/useApi'; import { PageHeader } from '../components/PageHeader'; import { useToast } from '../components/Toast'; @@ -276,8 +276,7 @@ function InstagramImageRow({ } }; - const apex = typeof window !== 'undefined' && window.location.host.endsWith('localhost') ? 'localhost' : 'rec.net'; - const preview = img.imageName ? `https://img.${apex}/${encodeURIComponent(img.imageName)}?width=128&sig=p1` : null; + const preview = imageCdnUrl(img.imageName, 'width=128&sig=p1'); return (
@@ -439,15 +438,14 @@ function VideoRow({ // because it overwrites thumbnailBlobName). const [autoThumb, setAutoThumb] = useState(true); - const apex = typeof window !== 'undefined' && window.location.host.endsWith('localhost') ? 'localhost' : 'rec.net'; - const thumbPreview = video.thumbnailBlobName ? `https://img.${apex}/${encodeURIComponent(video.thumbnailBlobName)}?width=160&sig=p1` : null; - // The watch fetches videos under cdn.{apex}/video/ + const thumbPreview = imageCdnUrl(video.thumbnailBlobName, 'width=160&sig=p1'); + // The watch fetches videos under cdn./video/ // (RecNet/CommunityBoard.txt:1068 — `String.Concat("/video/", BlobName)`). // Match that path here so the inline admin preview hits the same // route the watch does; previously we built a bare `cdn.{apex}/` // URL which 404'd because CdnController's catch-all regex didn't // include video extensions. - const videoUrl = video.blobName ? `https://cdn.${apex}/video/${encodeURIComponent(video.blobName)}` : null; + const videoUrl = assetCdnUrl(video.blobName ? `video/${video.blobName}` : null); // Video file picker → POST /communityboard/video/upload (timeout // generous: 100 MB at 5 MB/s is 20s of body time alone, plus S3 diff --git a/DorkNet.Server/admin-ui/src/pages/LoadingTips.tsx b/DorkNet.Server/admin-ui/src/pages/LoadingTips.tsx index 69f76e2..3a0842f 100644 --- a/DorkNet.Server/admin-ui/src/pages/LoadingTips.tsx +++ b/DorkNet.Server/admin-ui/src/pages/LoadingTips.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef, useState } from 'react'; import { api, get } from '../lib/api'; +import { imageCdnUrl } from '../lib/types'; import { PageHeader } from '../components/PageHeader'; import { useToast } from '../components/Toast'; import { Plus, Trash } from '../components/Icons'; @@ -185,8 +186,7 @@ function TipEditor({ } }; - const apex = typeof window !== 'undefined' && window.location.host.endsWith('localhost') ? 'localhost' : 'rec.net'; - const preview = tip.imageName ? `https://img.${apex}/${encodeURIComponent(tip.imageName)}?width=160&sig=p1` : null; + const preview = imageCdnUrl(tip.imageName, 'width=160&sig=p1'); return (