Skip to content

Commit f2315c6

Browse files
committed
feat(resources): add dynamic full-page view for embedded resources
Refactors the resources section to be data-driven and adds a dedicated full-page, mobile-friendly view for each resource.
1 parent 094702e commit f2315c6

File tree

5 files changed

+125
-35
lines changed

5 files changed

+125
-35
lines changed

src/app/resources/[slug]/page.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { ResourceIframe } from "@/components/sections/resource-iframe";
2+
import { buildPageMetadata } from "@/lib/config";
3+
import { findResourceBySlug, resources } from "@/lib/resources";
4+
import type { Metadata } from "next";
5+
import { notFound } from "next/navigation";
6+
7+
export async function generateStaticParams() {
8+
return resources.map((r) => ({ slug: r.slug }));
9+
}
10+
11+
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
12+
const { slug } = await params;
13+
const resource = findResourceBySlug(slug);
14+
if (!resource) return buildPageMetadata("/resources");
15+
return buildPageMetadata(`/resources/${resource.slug}`, {
16+
title: resource.title,
17+
description: `Embedded resource: ${resource.title}`,
18+
});
19+
}
20+
21+
export default async function ResourceEmbedPage({ params }: { params: Promise<{ slug: string }> }) {
22+
const { slug } = await params;
23+
const resource = findResourceBySlug(slug);
24+
if (!resource) return notFound();
25+
26+
return (
27+
<ResourceIframe
28+
title={resource.title}
29+
websiteUrl={resource.websiteUrl}
30+
hideTopPx={resource.hideTopPx}
31+
hideHeader
32+
showOnMobile
33+
containerHeight="100dvh"
34+
/>
35+
);
36+
}

src/app/resources/page.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ResourceIframe } from "@/components/sections/resource-iframe";
22
import { buildPageMetadata } from "@/lib/config";
3+
import { resources } from "@/lib/resources";
34
import { Metadata } from "next";
45

56
const description =
@@ -16,16 +17,15 @@ export default function Resources() {
1617
</p>
1718
</div>
1819
<div className="space-y-8">
19-
<ResourceIframe
20-
title="Where She Stood - WWII"
21-
websiteUrl="https://whereshestoodwwii.wixsite.com/where-she-stood"
22-
hideTopPx={50}
23-
/>
24-
<ResourceIframe
25-
title="The Spine of the Nation"
26-
websiteUrl="https://thespineofthenation.wordpress.com"
27-
hideTopPx={49}
28-
/>
20+
{resources.map((r) => (
21+
<ResourceIframe
22+
key={r.slug}
23+
title={r.title}
24+
websiteUrl={r.websiteUrl}
25+
hideTopPx={r.hideTopPx}
26+
fullPageHref={`/resources/${r.slug}`}
27+
/>
28+
))}
2929
</div>
3030
</div>
3131
);

src/components/sections/resource-iframe.tsx

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { Button } from "@/components/ui/button";
44
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
55
import { cn, noReturnDebounce } from "@/lib/utils";
6-
import { RefObject, useCallback, useEffect, useRef, useState } from "react";
6+
import { CSSProperties, RefObject, useCallback, useEffect, useRef, useState } from "react";
77

88
const IFRAME_DEFAULTS = {
99
WIDTH: 1440,
@@ -51,16 +51,23 @@ function useResponsiveScale(targetRef: RefObject<HTMLElement | null>, sourceWidt
5151
return scale;
5252
}
5353

54-
interface ResourceIframeProps {
54+
type ResourceIframeProps = {
5555
websiteUrl: string;
5656
title: string;
5757
className?: string;
5858
scale?: number;
5959
desktopWidth?: number;
60-
containerHeight?: number;
60+
/** Can be a number (px) or CSS height string (e.g. "100dvh"). */
61+
containerHeight?: number | CSSProperties["height"];
6162
/** Number of pixels to hide from the top of the iframe content (pre-scale units). */
6263
hideTopPx?: number;
63-
}
64+
/** Internal link to a full-page view. If provided, the header button links here instead of external site. */
65+
fullPageHref?: string;
66+
/** If true, omits the header (useful for full-page variant). */
67+
hideHeader?: boolean;
68+
/** If true, shows the iframe on mobile as well (not just md+). */
69+
showOnMobile?: boolean;
70+
};
6471

6572
export function ResourceIframe({
6673
websiteUrl,
@@ -70,37 +77,59 @@ export function ResourceIframe({
7077
desktopWidth = IFRAME_DEFAULTS.WIDTH,
7178
containerHeight = IFRAME_DEFAULTS.HEIGHT,
7279
hideTopPx = 0,
80+
fullPageHref,
81+
hideHeader = false,
82+
showOnMobile = false,
7383
}: ResourceIframeProps) {
7484
const [isLoading, setIsLoading] = useState(true);
7585
const containerRef = useRef<HTMLDivElement>(null);
7686
const calculatedScale = useResponsiveScale(containerRef, desktopWidth);
7787

7888
const finalScale = forcedScale ?? calculatedScale;
7989
const scaleWrapperStyle = createScaleTransformStyle(finalScale, hideTopPx);
90+
const linkHref = fullPageHref ?? websiteUrl;
91+
const isExternal = !fullPageHref;
92+
const buttonLabel = isExternal ? "Open Original Site →" : "Open Full Page →";
8093

8194
return (
82-
<Card className={cn("w-full gap-0 md:gap-6", className)}>
83-
<CardHeader>
84-
<div className="flex flex-wrap items-center justify-between gap-4">
85-
<CardTitle>{title}</CardTitle>
86-
<Button
87-
asChild
88-
variant="outline"
89-
>
90-
<a
91-
href={websiteUrl}
92-
target="_blank"
93-
rel="noopener"
95+
<Card className={cn("w-full gap-0 md:gap-6", hideHeader && "rounded-none border-0 p-0", className)}>
96+
{!hideHeader && (
97+
<CardHeader>
98+
<div className="flex flex-wrap items-center justify-between gap-4">
99+
<div className="flex flex-col">
100+
<CardTitle>{title}</CardTitle>
101+
<a
102+
href={websiteUrl}
103+
target="_blank"
104+
rel="noopener"
105+
className="text-muted-foreground text-xs underline underline-offset-4"
106+
>
107+
{websiteUrl}
108+
</a>
109+
</div>
110+
<Button
111+
asChild
112+
variant="outline"
94113
>
95-
Visit Full Website →
96-
</a>
97-
</Button>
98-
</div>
99-
</CardHeader>
100-
<CardContent>
114+
<a
115+
href={linkHref}
116+
target={isExternal ? "_blank" : undefined}
117+
rel="noopener"
118+
>
119+
{buttonLabel}
120+
</a>
121+
</Button>
122+
</div>
123+
</CardHeader>
124+
)}
125+
<CardContent className={cn(hideHeader && "p-0")}>
101126
<div
102127
ref={containerRef}
103-
className="relative hidden w-full overflow-hidden rounded-lg border md:block"
128+
className={cn(
129+
"relative overflow-hidden",
130+
!showOnMobile && "hidden md:block",
131+
hideHeader ? "rounded-none border-0" : "rounded-lg border"
132+
)}
104133
style={{ height: containerHeight }}
105134
>
106135
{isLoading && (

src/lib/exco.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,11 @@ export const excoMembers: ExcoMember[] = [
3636
},
3737
{
3838
name: "SeoJin Moon",
39-
position: "co-Directory of Publicity",
39+
position: "Directory of Publicity",
4040
},
4141
{
4242
name: "Razzaq Kinza",
43-
position: "co-Directory of Publicity",
43+
position: "Directory of Publicity",
4444
},
4545
{
4646
name: "Cheng Ho Ming",

src/lib/resources.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export type Resource = {
2+
slug: string;
3+
title: string;
4+
websiteUrl: string;
5+
hideTopPx: number;
6+
};
7+
8+
export const resources: Resource[] = [
9+
{
10+
slug: "where-she-stood-wwii",
11+
title: "Where She Stood - WWII",
12+
websiteUrl: "https://whereshestoodwwii.wixsite.com/where-she-stood",
13+
hideTopPx: 50,
14+
},
15+
{
16+
slug: "the-spine-of-the-nation",
17+
title: "The Spine of the Nation",
18+
websiteUrl: "https://thespineofthenation.wordpress.com",
19+
hideTopPx: 49,
20+
},
21+
];
22+
23+
export function findResourceBySlug(slug: string): Resource | undefined {
24+
return resources.find((r) => r.slug === slug);
25+
}

0 commit comments

Comments
 (0)