Skip to content

Commit a8a07b9

Browse files
committed
feat(resources): add iframe of 2 websites for content
1 parent 32391e1 commit a8a07b9

File tree

3 files changed

+152
-4
lines changed

3 files changed

+152
-4
lines changed

src/app/resources/page.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Metadata } from "next";
2-
import { ComingSoon } from "@/components/sections/coming-soon";
2+
import { ResourceIframe } from "@/components/sections/resource-iframe";
33
import { siteConfig } from "@/lib/config";
44

55
const title = "Resources";
@@ -31,8 +31,23 @@ export const metadata: Metadata = {
3131

3232
export default function Resources() {
3333
return (
34-
<>
35-
<ComingSoon />
36-
</>
34+
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
35+
<div className="mb-8 text-center">
36+
<p className="text-lg text-muted-foreground">
37+
Here are some recommended websites. They offer more details and different perspectives on our
38+
chapter's topics.
39+
</p>
40+
</div>
41+
<div className="space-y-8">
42+
<ResourceIframe
43+
title="Where She Stood - WWII"
44+
websiteUrl="https://whereshestoodwwii.wixsite.com/where-she-stood"
45+
/>
46+
<ResourceIframe
47+
title="The Spine of the Nation"
48+
websiteUrl="https://thespineofthenation.wordpress.com"
49+
/>
50+
</div>
51+
</div>
3752
);
3853
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"use client";
2+
3+
import { useState, useRef, useEffect, useCallback, RefObject } from "react";
4+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
5+
import { Button } from "@/components/ui/button";
6+
import { cn, noReturnDebounce } from "@/lib/utils";
7+
8+
const IFRAME_DEFAULTS = {
9+
WIDTH: 1338,
10+
HEIGHT: 600,
11+
} as const;
12+
13+
function calculateResponsiveScale(containerWidth: number, sourceWidth: number): number {
14+
if (!containerWidth || !sourceWidth) return 1;
15+
return Math.min(containerWidth / sourceWidth, 1);
16+
}
17+
18+
function createScaleTransformStyle(scale: number) {
19+
return {
20+
transform: `scale(${scale})`,
21+
transformOrigin: "top left",
22+
width: `${100 / scale}%`,
23+
height: `${100 / scale}%`,
24+
};
25+
}
26+
27+
/**
28+
* A hook to calculate the responsive scale of an element based on its container's width.
29+
*/
30+
function useResponsiveScale(targetRef: RefObject<HTMLElement | null>, sourceWidth: number): number {
31+
const [scale, setScale] = useState(1);
32+
33+
const updateScale = useCallback(() => {
34+
if (!targetRef.current) return;
35+
const newScale = calculateResponsiveScale(targetRef.current.clientWidth, sourceWidth);
36+
setScale(newScale);
37+
}, [targetRef, sourceWidth]);
38+
39+
useEffect(() => {
40+
updateScale();
41+
const debouncedUpdate = noReturnDebounce(updateScale);
42+
window.addEventListener("resize", debouncedUpdate);
43+
return () => window.removeEventListener("resize", debouncedUpdate);
44+
}, [updateScale]);
45+
46+
return scale;
47+
}
48+
49+
interface ResourceIframeProps {
50+
websiteUrl: string;
51+
title: string;
52+
className?: string;
53+
scale?: number;
54+
desktopWidth?: number;
55+
containerHeight?: number;
56+
}
57+
58+
export function ResourceIframe({
59+
websiteUrl,
60+
title,
61+
className,
62+
scale: forcedScale,
63+
desktopWidth = IFRAME_DEFAULTS.WIDTH,
64+
containerHeight = IFRAME_DEFAULTS.HEIGHT,
65+
}: ResourceIframeProps) {
66+
const [isLoading, setIsLoading] = useState(true);
67+
const containerRef = useRef<HTMLDivElement>(null);
68+
const calculatedScale = useResponsiveScale(containerRef, desktopWidth);
69+
70+
const finalScale = forcedScale ?? calculatedScale;
71+
const scaleWrapperStyle = createScaleTransformStyle(finalScale);
72+
73+
return (
74+
<Card className={cn("w-full", className)}>
75+
<CardHeader>
76+
<div className="flex flex-wrap items-center justify-between gap-4">
77+
<CardTitle>{title}</CardTitle>
78+
<Button
79+
asChild
80+
variant="outline"
81+
>
82+
<a
83+
href={websiteUrl}
84+
target="_blank"
85+
rel="noopener"
86+
>
87+
Visit Full Website →
88+
</a>
89+
</Button>
90+
</div>
91+
</CardHeader>
92+
<CardContent>
93+
<div
94+
ref={containerRef}
95+
className="relative hidden w-full overflow-hidden rounded-lg border md:block"
96+
style={{ height: containerHeight }}
97+
>
98+
{isLoading && (
99+
<div className="absolute inset-0 z-10 flex items-center justify-center bg-muted">
100+
<p className="text-muted-foreground">Loading website...</p>
101+
</div>
102+
)}
103+
<div
104+
className="h-full w-full"
105+
style={scaleWrapperStyle}
106+
>
107+
<iframe
108+
src={websiteUrl}
109+
title={title}
110+
allowFullScreen
111+
className="h-full w-full border-0"
112+
onLoad={() => setIsLoading(false)}
113+
/>
114+
</div>
115+
</div>
116+
</CardContent>
117+
</Card>
118+
);
119+
}

src/lib/utils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,17 @@ import { twMerge } from "tailwind-merge";
44
export function cn(...inputs: ClassValue[]) {
55
return twMerge(clsx(inputs));
66
}
7+
8+
export function noReturnDebounce<T extends unknown[]>(
9+
func: (...args: T) => void,
10+
delay: number = 100
11+
): (...args: T) => void {
12+
let timeoutId: ReturnType<typeof setTimeout>;
13+
14+
return (...args: T) => {
15+
clearTimeout(timeoutId);
16+
timeoutId = setTimeout(() => {
17+
func(...args);
18+
}, delay);
19+
};
20+
}

0 commit comments

Comments
 (0)