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{" "}
+