diff --git a/src/app.tsx b/src/app.tsx index f225330..873c1f6 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -14,6 +14,7 @@ import { TerminalSplash } from "./components/TerminalSplash"; import { MetaProvider } from "@solidjs/meta"; import ErrorBoundaryFallback from "./components/ErrorBoundaryFallback"; import { BarsProvider, useBars } from "./context/bars"; +import { createWindowWidth, isMobile } from "~/lib/resize-utils"; function AppLayout(props: { children: any }) { const { @@ -28,15 +29,16 @@ function AppLayout(props: { children: any }) { barsInitialized } = useBars(); + const windowWidth = createWindowWidth(); let lastScrollY = 0; const SCROLL_THRESHOLD = 100; createEffect(() => { const handleResize = () => { - const isMobile = window.innerWidth < 768; // md breakpoint + const currentIsMobile = isMobile(windowWidth()); // Show bars when switching to desktop - if (!isMobile) { + if (!currentIsMobile) { setLeftBarVisible(true); setRightBarVisible(true); } @@ -48,10 +50,6 @@ function AppLayout(props: { children: any }) { // Call immediately and whenever dependencies change handleResize(); - - window.addEventListener("resize", handleResize); - - return () => window.removeEventListener("resize", handleResize); }); // Recalculate when bar sizes change (visibility or actual resize) @@ -65,9 +63,9 @@ function AppLayout(props: { children: any }) { onMount(() => { const handleScroll = () => { const currentScrollY = window.scrollY; - const isMobile = window.innerWidth < 768; // md breakpoint + const currentIsMobile = isMobile(windowWidth()); - if (isMobile && currentScrollY > SCROLL_THRESHOLD) { + if (currentIsMobile && currentScrollY > SCROLL_THRESHOLD) { // Scrolling down past threshold - hide left bar on mobile if (currentScrollY > lastScrollY) { setLeftBarVisible(false); @@ -87,9 +85,9 @@ function AppLayout(props: { children: any }) { // ESC key to close sidebars on mobile onMount(() => { const handleKeyDown = (e: KeyboardEvent) => { - const isMobile = window.innerWidth < 768; // md breakpoint + const currentIsMobile = isMobile(windowWidth()); - if (e.key === "Escape" && isMobile) { + if (e.key === "Escape" && currentIsMobile) { if (leftBarVisible()) { setLeftBarVisible(false); } @@ -122,12 +120,12 @@ function AppLayout(props: { children: any }) { const touchEndY = e.changedTouches[0].clientY; const deltaX = touchEndX - touchStartX; const deltaY = touchEndY - touchStartY; - const isMobile = window.innerWidth < 768; // md breakpoint + const currentIsMobile = isMobile(windowWidth()); // Only trigger if horizontal swipe is dominant if (Math.abs(deltaX) > Math.abs(deltaY)) { // Mobile: Only left bar - if (isMobile) { + if (currentIsMobile) { // Swipe right anywhere - reveal left bar if (deltaX > SWIPE_THRESHOLD) { setLeftBarVisible(true); @@ -160,10 +158,10 @@ function AppLayout(props: { children: any }) { }); const handleCenterTapRelease = (e: MouseEvent | TouchEvent) => { - const isMobile = window.innerWidth < 768; + const currentIsMobile = isMobile(windowWidth()); // Only hide left bar on mobile when it's visible - if (isMobile && leftBarVisible()) { + if (currentIsMobile && leftBarVisible()) { const target = e.target as HTMLElement; const isInteractive = target.closest( "a, button, input, select, textarea, [onclick]" diff --git a/src/components/SimpleParallax.tsx b/src/components/SimpleParallax.tsx index 531b75c..e1c9d4d 100644 --- a/src/components/SimpleParallax.tsx +++ b/src/components/SimpleParallax.tsx @@ -1,5 +1,15 @@ -import { createSignal, createEffect, onMount, onCleanup, children as resolveChildren, type ParentComponent, createMemo, For } from "solid-js"; +import { + createSignal, + createEffect, + onMount, + onCleanup, + children as resolveChildren, + type ParentComponent, + createMemo, + For +} from "solid-js"; import { animate } from "motion"; +import { createWindowWidth } from "~/lib/resize-utils"; type ParallaxBackground = { imageSet: { [key: number]: string }; @@ -21,11 +31,17 @@ type ParallaxLayerProps = { function ParallaxLayer(props: ParallaxLayerProps) { let containerRef: HTMLDivElement | undefined; - - const layerDepthFactor = createMemo(() => props.layer / (Object.keys(props.caveParallax.imageSet).length - 1)); - const layerVerticalOffset = createMemo(() => props.verticalOffsetPixels * layerDepthFactor()); + + const layerDepthFactor = createMemo( + () => props.layer / (Object.keys(props.caveParallax.imageSet).length - 1) + ); + const layerVerticalOffset = createMemo( + () => props.verticalOffsetPixels * layerDepthFactor() + ); const speed = createMemo(() => (120 - props.layer * 10) * 1000); - const targetX = createMemo(() => props.direction * -props.caveParallax.size.width * props.imagesNeeded); + const targetX = createMemo( + () => props.direction * -props.caveParallax.size.width * props.imagesNeeded + ); const containerStyle = createMemo(() => ({ width: `${props.caveParallax.size.width * props.imagesNeeded * 3}px`, @@ -33,19 +49,19 @@ function ParallaxLayer(props: ParallaxLayerProps) { left: `${(props.dimensions.width - props.scaledWidth) / 2}px`, top: `${(props.dimensions.height - props.scaledHeight) / 2 + layerVerticalOffset()}px`, "transform-origin": "center center", - "will-change": "transform", + "will-change": "transform" })); // Set up animation when component mounts or when direction/speed changes createEffect(() => { if (!containerRef) return; - + const target = targetX(); const duration = speed() / 1000; - + const controls = animate( containerRef, - { + { transform: [ `translateX(0px) scale(${props.scale})`, `translateX(${target}px) scale(${props.scale})` @@ -54,7 +70,7 @@ function ParallaxLayer(props: ParallaxLayerProps) { { duration, easing: "linear", - repeat: Infinity, + repeat: Infinity } ); @@ -68,7 +84,7 @@ function ParallaxLayer(props: ParallaxLayerProps) { style={{ left: `${groupOffset * props.caveParallax.size.width * props.imagesNeeded}px`, width: `${props.caveParallax.size.width * props.imagesNeeded}px`, - height: `${props.caveParallax.size.height}px`, + height: `${props.caveParallax.size.height}px` }} > {Array.from({ length: props.imagesNeeded }).map((_, index) => ( @@ -77,7 +93,7 @@ function ParallaxLayer(props: ParallaxLayerProps) { style={{ width: `${props.caveParallax.size.width}px`, height: `${props.caveParallax.size.height}px`, - left: `${index * props.caveParallax.size.width}px`, + left: `${index * props.caveParallax.size.width}px` }} > Object.keys(props.caveParallax.imageSet).length - 3 ? "eager" : "lazy"} + loading={ + props.layer > + Object.keys(props.caveParallax.imageSet).length - 3 + ? "eager" + : "lazy" + } /> ))} @@ -95,11 +116,7 @@ function ParallaxLayer(props: ParallaxLayerProps) { }); return ( -
+
{imageGroups()}
); @@ -107,9 +124,17 @@ function ParallaxLayer(props: ParallaxLayerProps) { const SimpleParallax: ParentComponent = (props) => { let containerRef: HTMLDivElement | undefined; - const [dimensions, setDimensions] = createSignal({ width: 0, height: 0 }); + const windowWidth = createWindowWidth(100); + const [windowHeight, setWindowHeight] = createSignal( + typeof window !== "undefined" ? window.innerHeight : 800 + ); const [direction, setDirection] = createSignal(1); + const dimensions = createMemo(() => ({ + width: windowWidth(), + height: windowHeight() + })); + const caveParallax = createMemo(() => ({ imageSet: { 0: "/Cave/0.png", @@ -119,33 +144,27 @@ const SimpleParallax: ParentComponent = (props) => { 4: "/Cave/4.png", 5: "/Cave/5.png", 6: "/Cave/6.png", - 7: "/Cave/7.png", + 7: "/Cave/7.png" }, size: { width: 384, height: 216 }, - verticalOffset: 0.4, + verticalOffset: 0.4 })); - const layerCount = createMemo(() => Object.keys(caveParallax().imageSet).length - 1); + const layerCount = createMemo( + () => Object.keys(caveParallax().imageSet).length - 1 + ); const imagesNeeded = 3; - const updateDimensions = () => { - if (containerRef) { - setDimensions({ - width: window.innerWidth, - height: window.innerHeight, - }); - } - }; - onMount(() => { let timeoutId: ReturnType; const handleResize = () => { clearTimeout(timeoutId); - timeoutId = setTimeout(updateDimensions, 100); + timeoutId = setTimeout(() => { + setWindowHeight(window.innerHeight); + }, 100); }; - updateDimensions(); window.addEventListener("resize", handleResize); const intervalId = setInterval(() => { @@ -166,7 +185,7 @@ const SimpleParallax: ParentComponent = (props) => { scale: 0, scaledWidth: 0, scaledHeight: 0, - verticalOffsetPixels: 0, + verticalOffsetPixels: 0 }; } @@ -179,7 +198,7 @@ const SimpleParallax: ParentComponent = (props) => { scale, scaledWidth: cave.size.width * scale, scaledHeight: cave.size.height * scale, - verticalOffsetPixels: cave.verticalOffset * dims.height, + verticalOffsetPixels: cave.verticalOffset * dims.height }; }); @@ -214,13 +233,13 @@ const SimpleParallax: ParentComponent = (props) => { return (
{parallaxLayers()} diff --git a/src/components/blog/CommentBlock.tsx b/src/components/blog/CommentBlock.tsx index bf3927b..4a86b98 100644 --- a/src/components/blog/CommentBlock.tsx +++ b/src/components/blog/CommentBlock.tsx @@ -12,7 +12,7 @@ import type { CommentReaction, UserPublicData } from "~/types/comment"; -import { debounce } from "es-toolkit"; +import { createWindowWidth } from "~/lib/resize-utils"; import UserDefaultImage from "~/components/icons/UserDefaultImage"; import ReplyIcon from "~/components/icons/ReplyIcon"; import TrashIcon from "~/components/icons/TrashIcon"; @@ -32,7 +32,7 @@ export default function CommentBlock(props: CommentBlockProps) { const [replyBoxShowing, setReplyBoxShowing] = createSignal(false); const [toggleHeight, setToggleHeight] = createSignal(0); const [reactions, setReactions] = createSignal([]); - const [windowWidth, setWindowWidth] = createSignal(0); + const windowWidth = createWindowWidth(200); const [deletionLoading, setDeletionLoading] = createSignal(false); const [userData, setUserData] = createSignal(null); @@ -45,19 +45,6 @@ export default function CommentBlock(props: CommentBlockProps) { setCommentCollapsed(props.level >= 4); }); - // Window resize handler - onMount(() => { - const handleResize = debounce(() => { - setWindowWidth(window.innerWidth); - }, 200); - - window.addEventListener("resize", handleResize); - - onCleanup(() => { - window.removeEventListener("resize", handleResize); - }); - }); - // Find user data from comment map createEffect(() => { if (props.userCommentMap) { diff --git a/src/context/bars.tsx b/src/context/bars.tsx index b9b8719..a5706cb 100644 --- a/src/context/bars.tsx +++ b/src/context/bars.tsx @@ -1,12 +1,6 @@ -import { - Accessor, - createContext, - useContext, - createMemo, - onMount, - onCleanup -} from "solid-js"; +import { Accessor, createContext, useContext, createMemo } from "solid-js"; import { createSignal } from "solid-js"; +import { createWindowWidth, isMobile } from "~/lib/resize-utils"; const BarsContext = createContext<{ leftBarSize: Accessor; @@ -45,30 +39,15 @@ export function BarsProvider(props: { children: any }) { const [_rightBarNaturalSize, _setRightBarNaturalSize] = createSignal(0); const [syncedBarSize, setSyncedBarSize] = createSignal(0); const [centerWidth, setCenterWidth] = createSignal(0); - const initialWindowWidth = - typeof window !== "undefined" ? window.innerWidth : 1024; - const isMobile = initialWindowWidth < 768; - const [leftBarVisible, setLeftBarVisible] = createSignal(!isMobile); + const windowWidth = createWindowWidth(); + const initialIsMobile = isMobile(windowWidth()); + const [leftBarVisible, setLeftBarVisible] = createSignal(!initialIsMobile); const [rightBarVisible, setRightBarVisible] = createSignal(true); const [barsInitialized, setBarsInitialized] = createSignal(false); - const [windowWidth, setWindowWidth] = createSignal(initialWindowWidth); let leftBarSized = false; let rightBarSized = false; - // Track window width reactively for mobile/desktop detection - onMount(() => { - const handleResize = () => { - setWindowWidth(window.innerWidth); - }; - - window.addEventListener("resize", handleResize); - - onCleanup(() => { - window.removeEventListener("resize", handleResize); - }); - }); - const wrappedSetLeftBarSize = (size: number) => { if (!barsInitialized()) { // Before initialization, capture natural size @@ -84,14 +63,10 @@ export function BarsProvider(props: { children: any }) { }; // Initialize immediately on mobile if left bar starts hidden - onMount(() => { - const isMobile = typeof window !== "undefined" && window.innerWidth < 768; - if (isMobile && !leftBarVisible()) { - // Skip waiting for left bar size on mobile when it starts hidden - leftBarSized = true; - checkAndSync(); - } - }); + if (initialIsMobile && !leftBarVisible()) { + // Skip waiting for left bar size on mobile when it starts hidden + leftBarSized = true; + } const wrappedSetRightBarSize = (size: number) => { if (!barsInitialized()) { @@ -108,8 +83,8 @@ export function BarsProvider(props: { children: any }) { }; const checkAndSync = () => { - const isMobile = typeof window !== "undefined" && window.innerWidth < 768; - const bothBarsReady = leftBarSized && (isMobile || rightBarSized); + const currentIsMobile = isMobile(windowWidth()); + const bothBarsReady = leftBarSized && (currentIsMobile || rightBarSized); if (bothBarsReady) { const maxWidth = Math.max(_leftBarNaturalSize(), _rightBarNaturalSize()); @@ -123,8 +98,8 @@ export function BarsProvider(props: { children: any }) { const naturalSize = _leftBarNaturalSize(); if (naturalSize === 0) return 0; // Hidden // On mobile (<768px), always return 0 for layout (overlay mode) - const isMobile = windowWidth() < 768; - if (isMobile) return 0; + const currentIsMobile = isMobile(windowWidth()); + if (currentIsMobile) return 0; return barsInitialized() ? syncedBarSize() : naturalSize; }); diff --git a/src/lib/resize-utils.ts b/src/lib/resize-utils.ts new file mode 100644 index 0000000..927b440 --- /dev/null +++ b/src/lib/resize-utils.ts @@ -0,0 +1,57 @@ +import { createSignal, onMount, onCleanup, Accessor } from "solid-js"; + +export const MOBILE_BREAKPOINT = 768; + +/** + * Creates a reactive window width signal that updates on resize + * @param debounceMs Optional debounce delay in milliseconds + * @returns Accessor for current window width + */ +export function createWindowWidth(debounceMs?: number): Accessor { + const initialWidth = typeof window !== "undefined" ? window.innerWidth : 1024; + const [width, setWidth] = createSignal(initialWidth); + + onMount(() => { + let timeoutId: ReturnType | undefined; + + const handleResize = () => { + if (debounceMs) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + setWidth(window.innerWidth); + }, debounceMs); + } else { + setWidth(window.innerWidth); + } + }; + + window.addEventListener("resize", handleResize); + + onCleanup(() => { + clearTimeout(timeoutId); + window.removeEventListener("resize", handleResize); + }); + }); + + return width; +} + +/** + * Checks if the current window width is in mobile viewport + * @param width Current window width + * @returns true if mobile viewport + */ +export function isMobile(width: number): boolean { + return width < MOBILE_BREAKPOINT; +} + +/** + * Creates a derived signal for mobile state + * @param windowWidth Window width accessor + * @returns Accessor for mobile state + */ +export function createIsMobile( + windowWidth: Accessor +): Accessor { + return () => isMobile(windowWidth()); +} diff --git a/src/routes/index.tsx b/src/routes/index.tsx index f8ed636..41b0a79 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,4 +1,5 @@ import { Title, Meta } from "@solidjs/meta"; +import { DarkModeToggle } from "~/components/DarkModeToggle"; import { Typewriter } from "~/components/Typewriter"; export default function Home() { @@ -7,10 +8,10 @@ export default function Home() { Home | Michael Freno -
+
Hey!
@@ -147,9 +148,12 @@ export default function Home() {
-
- And if you love the color schemes of this site (which of course you - do), you can see{" "} + + And if you love the color schemes of this site +
+ +
+ (which of course you do), you can see{" "} +