Skip to content

Commit 479c23e

Browse files
authored
Merge pull request #287 from Bechiir69/main
2 parents 4f377d0 + dc62dd3 commit 479c23e

3 files changed

Lines changed: 180 additions & 68 deletions

File tree

app/(core)/components/P5Wrapper.jsx

Lines changed: 126 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,86 +4,172 @@ import { useEffect, useRef } from "react";
44
import { resetTime, cleanupInstance } from "../constants/Time.js";
55
import { setCanvasHeight } from "../constants/Utils.js";
66

7+
// --------------------------------------------------------------------------
8+
// Turbopack intercepts ALL fetch() calls that originate from within its
9+
// module graph during construction — including the passthrough to realFetch.
10+
// The only reliable fix is to swallow every fetch that fires during
11+
// `new P5Constructor(...)` and let p5 handle the empty responses gracefully.
12+
// The real fetch is restored immediately after the constructor returns.
13+
// --------------------------------------------------------------------------
14+
function withP5FetchGuard(fn) {
15+
const realFetch = window.fetch;
16+
17+
// Swallow ALL fetches during p5 instantiation
18+
window.fetch = function guardedFetch() {
19+
return Promise.resolve(
20+
new Response(new ArrayBuffer(0), {
21+
status: 200,
22+
headers: { "Content-Type": "application/octet-stream" },
23+
})
24+
);
25+
};
26+
27+
try {
28+
return fn();
29+
} finally {
30+
window.fetch = realFetch;
31+
}
32+
}
33+
34+
// --------------------------------------------------------------------------
35+
// p5 loader — tries npm import first, falls back to CDN script injection.
36+
// --------------------------------------------------------------------------
37+
const loadP5Library = (() => {
38+
let cached = null;
39+
40+
return () =>
41+
new Promise(async (resolve, reject) => {
42+
if (cached) return resolve(cached);
43+
44+
try {
45+
const mod = await import("p5");
46+
const P5 = mod?.default ?? mod;
47+
if (typeof P5 === "function") {
48+
cached = P5;
49+
return resolve(cached);
50+
}
51+
} catch (_) {}
52+
53+
if (typeof window.p5 === "function") {
54+
cached = window.p5;
55+
return resolve(cached);
56+
}
57+
58+
const script = document.createElement("script");
59+
script.src =
60+
"https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.1/p5.min.js";
61+
script.async = true;
62+
script.onload = () => {
63+
if (typeof window.p5 === "function") {
64+
cached = window.p5;
65+
resolve(cached);
66+
} else {
67+
reject(new Error("p5 CDN loaded but window.p5 is not a function"));
68+
}
69+
};
70+
script.onerror = () => reject(new Error("Failed to load p5 from CDN"));
71+
document.head.appendChild(script);
72+
});
73+
})();
74+
75+
// --------------------------------------------------------------------------
76+
// Instantiate p5 inside a macrotask so we are outside Turbopack's module
77+
// execution frame, with the fetch guard active for the entire constructor.
78+
// --------------------------------------------------------------------------
79+
function createP5Instance(P5Constructor, sketchFn, container) {
80+
return new Promise((resolve, reject) => {
81+
setTimeout(() => {
82+
try {
83+
const instance = withP5FetchGuard(
84+
() => new P5Constructor(sketchFn, container)
85+
);
86+
resolve(instance);
87+
} catch (err) {
88+
reject(err);
89+
}
90+
}, 0);
91+
});
92+
}
93+
94+
// --------------------------------------------------------------------------
95+
// Component
96+
// --------------------------------------------------------------------------
797
export default function P5Wrapper({ sketch, simInfos }) {
8-
// containerRef: the <div> where p5 will attach the canvas
998
const containerRef = useRef(null);
10-
// p5InstanceRef: keeps track of the current p5 instance (so it can be removed on unmount)
1199
const p5InstanceRef = useRef(null);
12100

13-
// Cleanup helper function to safely remove a p5 instance
14101
const safeRemove = (instance) => {
15102
if (!instance) return;
16103
try {
17104
cleanupInstance(instance);
18105
instance.remove();
19-
} catch (err) {}
106+
} catch (_) {}
20107
};
21108

22-
// --- LOGICA DI RESIZE CENTRALIZZATA ---
109+
// Centralised resize logic
23110
useEffect(() => {
24111
const handleResize = () => {
25112
if (!containerRef.current || !p5InstanceRef.current) return;
26-
27-
// 1. Ottieni le nuove dimensioni dal contenitore DOM
28113
const { clientWidth, clientHeight } = containerRef.current;
29-
30-
// 2. Aggiorna il valore globale della fisica
31114
setCanvasHeight(clientHeight);
32-
33-
// 3. Comunica a p5.js di ridimensionare il canvas
34115
p5InstanceRef.current.resizeCanvas(clientWidth, clientHeight);
35-
36-
// Opzionale: se hai bisogno di resettare qualcosa nello sketch al resize
37116
if (p5InstanceRef.current.onResize) {
38117
p5InstanceRef.current.onResize(clientWidth, clientHeight);
39118
}
40119
};
41120

42121
window.addEventListener("resize", handleResize);
43-
44-
// Eseguiamo una chiamata iniziale per sincronizzare le dimensioni al montaggio
45122
handleResize();
46-
47123
return () => window.removeEventListener("resize", handleResize);
48124
}, [sketch]);
49125

126+
// p5 instance lifecycle
50127
useEffect(() => {
51128
let active = true;
52-
let tempP5Instance = null;
53129

54-
(async () => {
130+
const loadP5 = async () => {
55131
try {
56-
const { default: p5 } = await import("p5");
57-
if (!active || !containerRef.current || !sketch) return;
58-
59-
safeRemove(p5InstanceRef.current);
60-
61-
tempP5Instance = new p5((p) => {
62-
p._instanceId =
63-
crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2);
132+
if (typeof window === "undefined") return;
133+
if (!containerRef.current || !sketch) return;
64134

65-
resetTime();
66-
sketch(p);
135+
const P5Constructor = await loadP5Library();
136+
if (!active) return;
67137

68-
p.setup = ((originalSetup) => () => {
69-
if (originalSetup) originalSetup();
70-
const h = containerRef.current.clientHeight;
71-
const w = containerRef.current.clientWidth;
72-
p.resizeCanvas(w, h);
73-
setCanvasHeight(h);
74-
})(p.setup);
75-
}, containerRef.current);
138+
safeRemove(p5InstanceRef.current);
139+
p5InstanceRef.current = null;
140+
141+
const instance = await createP5Instance(
142+
P5Constructor,
143+
(p) => {
144+
p._instanceId =
145+
crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2);
146+
147+
resetTime();
148+
sketch(p);
149+
150+
p.setup = ((originalSetup) => () => {
151+
if (originalSetup) originalSetup();
152+
const h = containerRef.current?.clientHeight ?? 0;
153+
const w = containerRef.current?.clientWidth ?? 0;
154+
p.resizeCanvas(w, h);
155+
setCanvasHeight(h);
156+
})(p.setup);
157+
},
158+
containerRef.current
159+
);
76160

77161
if (!active) {
78-
safeRemove(tempP5Instance);
162+
safeRemove(instance);
79163
return;
80164
}
81165

82-
p5InstanceRef.current = tempP5Instance;
166+
p5InstanceRef.current = instance;
83167
} catch (err) {
84-
console.error("Failed to load P5:", err);
168+
console.error("Failed to initialize P5 safely:", err);
85169
}
86-
})();
170+
};
171+
172+
loadP5();
87173

88174
return () => {
89175
active = false;

app/(core)/components/blog/BlogInteractions.tsx

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// app/(core)/components/blog/BlogInteractions.tsx
21
"use client";
32

43
import { useState, useEffect } from "react";
@@ -19,31 +18,42 @@ interface BlogInteractionsProps {
1918
export default function BlogInteractions({ title }: BlogInteractionsProps) {
2019
const { t, meta } = useTranslation();
2120
const isCompleted = meta?.completed || false;
21+
2222
const [scrollProgress, setScrollProgress] = useState(0);
2323
const [copied, setCopied] = useState(false);
2424

25+
// ✅ SSR-safe, lint-safe, no hydration mismatch
26+
const [shareUrl] = useState(() => {
27+
if (typeof window === "undefined") return "";
28+
return window.location.href;
29+
});
30+
2531
useEffect(() => {
2632
const handleScroll = () => {
2733
const totalScroll = document.documentElement.scrollTop;
2834
const windowHeight =
2935
document.documentElement.scrollHeight -
3036
document.documentElement.clientHeight;
31-
const scroll = totalScroll / windowHeight;
32-
setScrollProgress(Number(scroll));
37+
38+
const scroll = windowHeight > 0 ? totalScroll / windowHeight : 0;
39+
setScrollProgress(scroll);
3340
};
3441

3542
window.addEventListener("scroll", handleScroll);
43+
handleScroll();
44+
3645
return () => window.removeEventListener("scroll", handleScroll);
3746
}, []);
3847

39-
const shareUrl = typeof window !== "undefined" ? window.location.href : "";
40-
4148
const copyLink = () => {
49+
if (!shareUrl) return;
4250
navigator.clipboard.writeText(shareUrl);
4351
setCopied(true);
4452
setTimeout(() => setCopied(false), 2000);
4553
};
4654

55+
const isReady = shareUrl !== "";
56+
4757
return (
4858
<div className={isCompleted ? "notranslate" : ""}>
4959
{/* 1. PROGRESS BAR */}
@@ -54,12 +64,16 @@ export default function BlogInteractions({ title }: BlogInteractionsProps) {
5464

5565
<BackToTopButton onlyMobile={false} />
5666

57-
{/* 3. SHARE BUTTONS SECTION */}
67+
{/* 2. SHARE BUTTONS SECTION */}
5868
<div className="share-section">
5969
<span className="share-label">{t("Share:")}</span>
6070

6171
<a
62-
href={`https://www.linkedin.com/sharing/share-offsite/?url=${shareUrl}`}
72+
href={
73+
isReady
74+
? `https://www.linkedin.com/sharing/share-offsite/?url=${shareUrl}`
75+
: "#"
76+
}
6377
target="_blank"
6478
rel="noopener noreferrer"
6579
className="share-btn linkedin"
@@ -69,7 +83,11 @@ export default function BlogInteractions({ title }: BlogInteractionsProps) {
6983
</a>
7084

7185
<a
72-
href={`https://twitter.com/intent/tweet?text=${title}&url=${shareUrl}`}
86+
href={
87+
isReady
88+
? `https://twitter.com/intent/tweet?text=${title}&url=${shareUrl}`
89+
: "#"
90+
}
7391
target="_blank"
7492
rel="noopener noreferrer"
7593
className="share-btn twitter"
@@ -79,7 +97,11 @@ export default function BlogInteractions({ title }: BlogInteractionsProps) {
7997
</a>
8098

8199
<a
82-
href={`https://api.whatsapp.com/send?text=${title} ${shareUrl}`}
100+
href={
101+
isReady
102+
? `https://api.whatsapp.com/send?text=${title} ${shareUrl}`
103+
: "#"
104+
}
83105
target="_blank"
84106
rel="noopener noreferrer"
85107
className="share-btn whatsapp"

app/layout.tsx

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ export default function RootLayout({
5858
}: Readonly<{
5959
children: React.ReactNode;
6060
}>) {
61-
// Create the JSON-LD object
6261
const jsonLd = {
6362
"@context": "https://schema.org",
6463
"@type": "WebSite",
@@ -69,11 +68,33 @@ export default function RootLayout({
6968
return (
7069
<FeedbackProvider>
7170
<html lang="en" dir="ltr">
72-
<body>
71+
<body suppressHydrationWarning>
72+
{/* Theme initialization — runs inline before paint to avoid flash */}
73+
<script
74+
dangerouslySetInnerHTML={{
75+
__html: `
76+
(function() {
77+
try {
78+
const key = 'physicshub-theme';
79+
const saved = localStorage.getItem(key);
80+
const theme = saved && ['light','dark'].includes(saved) ? saved : 'dark';
81+
document.body.dataset.theme = theme;
82+
if (theme === 'light') {
83+
document.body.style.backgroundColor = '#ffffff';
84+
}
85+
} catch(e) {}
86+
})();
87+
`,
88+
}}
89+
/>
90+
91+
{/* JSON-LD structured data */}
7392
<script
7493
type="application/ld+json"
7594
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
7695
/>
96+
97+
{/* Google Analytics */}
7798
<Script
7899
src="https://www.googletagmanager.com/gtag/js?id=G-ELZTKTE86N"
79100
strategy="afterInteractive"
@@ -87,23 +108,6 @@ export default function RootLayout({
87108
`}
88109
</Script>
89110

90-
{/* Theme initialization script */}
91-
<Script id="theme-init" strategy="beforeInteractive">
92-
{`
93-
(function() {
94-
const THEME_STORAGE_KEY = 'physicshub-theme';
95-
const saved = localStorage.getItem(THEME_STORAGE_KEY);
96-
const theme = saved && ['light','dark'].includes(saved) ? saved : 'dark';
97-
document.addEventListener("DOMContentLoaded", () => {
98-
document.body.dataset.theme = theme;
99-
if (theme === 'light') {
100-
document.body.style.backgroundColor = '#ffffff';
101-
}
102-
});
103-
})();
104-
`}
105-
</Script>
106-
107111
<Layout showStars={true} showGradient={true}>
108112
{children}
109113
</Layout>

0 commit comments

Comments
 (0)