simplified layout logic
This commit is contained in:
88
src/app.tsx
88
src/app.tsx
@@ -5,9 +5,7 @@ import {
|
|||||||
ErrorBoundary,
|
ErrorBoundary,
|
||||||
onMount,
|
onMount,
|
||||||
onCleanup,
|
onCleanup,
|
||||||
Suspense,
|
Suspense
|
||||||
Show,
|
|
||||||
createSignal
|
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import "./app.css";
|
import "./app.css";
|
||||||
import { LeftBar, RightBar } from "./components/Bars";
|
import { LeftBar, RightBar } from "./components/Bars";
|
||||||
@@ -20,56 +18,30 @@ import { createWindowWidth, isMobile } from "~/lib/resize-utils";
|
|||||||
|
|
||||||
function AppLayout(props: { children: any }) {
|
function AppLayout(props: { children: any }) {
|
||||||
const {
|
const {
|
||||||
leftBarSize,
|
|
||||||
rightBarSize,
|
|
||||||
setCenterWidth,
|
|
||||||
centerWidth,
|
|
||||||
leftBarVisible,
|
leftBarVisible,
|
||||||
rightBarVisible,
|
rightBarVisible,
|
||||||
setLeftBarVisible,
|
setLeftBarVisible,
|
||||||
setRightBarVisible,
|
setRightBarVisible
|
||||||
barsInitialized
|
|
||||||
} = useBars();
|
} = useBars();
|
||||||
|
|
||||||
let lastScrollY = 0;
|
let lastScrollY = 0;
|
||||||
const SCROLL_THRESHOLD = 75;
|
const SCROLL_THRESHOLD = 75;
|
||||||
|
|
||||||
// Track if we're on the client (hydrated) - starts false on server
|
|
||||||
const [isClient, setIsClient] = createSignal(false);
|
|
||||||
|
|
||||||
// Use onMount to avoid hydration issues - window operations are client-only
|
// Use onMount to avoid hydration issues - window operations are client-only
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
setIsClient(true);
|
|
||||||
const windowWidth = createWindowWidth();
|
const windowWidth = createWindowWidth();
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const handleResize = () => {
|
const currentIsMobile = isMobile(windowWidth());
|
||||||
const currentIsMobile = isMobile(windowWidth());
|
|
||||||
|
|
||||||
// Show bars when switching to desktop
|
// Show bars when switching to desktop
|
||||||
if (!currentIsMobile) {
|
if (!currentIsMobile) {
|
||||||
setLeftBarVisible(true);
|
setLeftBarVisible(true);
|
||||||
setRightBarVisible(true);
|
setRightBarVisible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// On mobile, leftBarSize() is always 0 (overlay mode)
|
|
||||||
const newWidth = window.innerWidth - leftBarSize() - rightBarSize();
|
|
||||||
setCenterWidth(newWidth);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Call immediately and whenever dependencies change
|
|
||||||
handleResize();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Recalculate when bar sizes change (visibility or actual resize)
|
|
||||||
createEffect(() => {
|
|
||||||
if (typeof window === "undefined") return;
|
|
||||||
// On mobile, leftBarSize() is always 0 (overlay mode)
|
|
||||||
const newWidth = window.innerWidth - leftBarSize() - rightBarSize();
|
|
||||||
setCenterWidth(newWidth);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Auto-hide on scroll (mobile only)
|
// Auto-hide on scroll (mobile only)
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const windowWidth = createWindowWidth();
|
const windowWidth = createWindowWidth();
|
||||||
@@ -194,40 +166,20 @@ function AppLayout(props: { children: any }) {
|
|||||||
<>
|
<>
|
||||||
<div class="flex max-w-screen flex-row">
|
<div class="flex max-w-screen flex-row">
|
||||||
<LeftBar />
|
<LeftBar />
|
||||||
<Show
|
<div class="bg-base relative h-screen w-screen overflow-x-hidden overflow-y-scroll md:ml-62.5 md:w-[calc(100vw-500px)]">
|
||||||
when={!isClient() || barsInitialized()}
|
<noscript>
|
||||||
fallback={<TerminalSplash />}
|
<div class="bg-yellow text-crust border-text fixed top-0 z-150 ml-16 border-b-2 p-4 text-center font-semibold md:ml-64">
|
||||||
>
|
JavaScript is disabled. Features will be limited.
|
||||||
<div
|
|
||||||
class="bg-base relative h-screen overflow-x-hidden overflow-y-scroll"
|
|
||||||
style={
|
|
||||||
barsInitialized()
|
|
||||||
? {
|
|
||||||
width: `${centerWidth() ?? "calc(100vw - 500px)"}px`,
|
|
||||||
"margin-left": `${leftBarSize() ?? "250px"}px`
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
width: "calc(100vw - 500px)",
|
|
||||||
"margin-left": "250px"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<noscript>
|
|
||||||
<div class="bg-yellow text-crust border-text fixed top-0 z-150 ml-16 border-b-2 p-4 text-center font-semibold md:ml-64">
|
|
||||||
JavaScript is disabled. Features will be limited.
|
|
||||||
</div>
|
|
||||||
</noscript>
|
|
||||||
<div
|
|
||||||
class="py-16"
|
|
||||||
onMouseUp={handleCenterTapRelease}
|
|
||||||
onTouchEnd={handleCenterTapRelease}
|
|
||||||
>
|
|
||||||
<Suspense fallback={<TerminalSplash />}>
|
|
||||||
{props.children}
|
|
||||||
</Suspense>
|
|
||||||
</div>
|
</div>
|
||||||
|
</noscript>
|
||||||
|
<div
|
||||||
|
class="py-16"
|
||||||
|
onMouseUp={handleCenterTapRelease}
|
||||||
|
onTouchEnd={handleCenterTapRelease}
|
||||||
|
>
|
||||||
|
<Suspense fallback={<TerminalSplash />}>{props.children}</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</div>
|
||||||
<RightBar />
|
<RightBar />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -170,10 +170,8 @@ export function RightBarContent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function LeftBar() {
|
export function LeftBar() {
|
||||||
const { setLeftBarSize, leftBarSize, leftBarVisible, setLeftBarVisible } =
|
const { leftBarSize, leftBarVisible, setLeftBarVisible } = useBars();
|
||||||
useBars();
|
|
||||||
let ref: HTMLDivElement | undefined;
|
let ref: HTMLDivElement | undefined;
|
||||||
let actualWidth = 0;
|
|
||||||
|
|
||||||
const [recentPosts, setRecentPosts] = createSignal<any[] | undefined>(
|
const [recentPosts, setRecentPosts] = createSignal<any[] | undefined>(
|
||||||
undefined
|
undefined
|
||||||
@@ -208,21 +206,6 @@ export function LeftBar() {
|
|||||||
setIsMounted(true);
|
setIsMounted(true);
|
||||||
|
|
||||||
if (ref) {
|
if (ref) {
|
||||||
const updateSize = () => {
|
|
||||||
actualWidth = ref?.offsetWidth || 0;
|
|
||||||
setLeftBarSize(leftBarVisible() ? actualWidth : 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
updateSize();
|
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver((entries) => {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
actualWidth = ref?.offsetWidth || 0;
|
|
||||||
setLeftBarSize(leftBarVisible() ? actualWidth : 0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
resizeObserver.observe(ref);
|
|
||||||
|
|
||||||
// Focus trap for accessibility on mobile
|
// Focus trap for accessibility on mobile
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
const isMobile = window.innerWidth < 768;
|
const isMobile = window.innerWidth < 768;
|
||||||
@@ -260,7 +243,6 @@ export function LeftBar() {
|
|||||||
ref.addEventListener("keydown", handleKeyDown);
|
ref.addEventListener("keydown", handleKeyDown);
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
resizeObserver.disconnect();
|
|
||||||
ref?.removeEventListener("keydown", handleKeyDown);
|
ref?.removeEventListener("keydown", handleKeyDown);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -303,10 +285,6 @@ export function LeftBar() {
|
|||||||
}, 0);
|
}, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
setLeftBarSize(leftBarVisible() ? actualWidth : 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
id="navigation"
|
id="navigation"
|
||||||
@@ -320,7 +298,7 @@ export function LeftBar() {
|
|||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
"transition-timing-function": "cubic-bezier(0.4, 0, 0.2, 1)",
|
"transition-timing-function": "cubic-bezier(0.4, 0, 0.2, 1)",
|
||||||
"min-width": leftBarSize() > 0 ? `${leftBarSize()}px` : undefined,
|
"min-width": "250px",
|
||||||
"box-shadow": "inset -6px 0 16px -6px rgba(0, 0, 0, 0.1)",
|
"box-shadow": "inset -6px 0 16px -6px rgba(0, 0, 0, 0.1)",
|
||||||
"padding-top": "env(safe-area-inset-top)",
|
"padding-top": "env(safe-area-inset-top)",
|
||||||
"padding-bottom": "env(safe-area-inset-bottom)"
|
"padding-bottom": "env(safe-area-inset-bottom)"
|
||||||
@@ -484,36 +462,8 @@ export function LeftBar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function RightBar() {
|
export function RightBar() {
|
||||||
const { setRightBarSize, rightBarSize, rightBarVisible } = useBars();
|
const { rightBarSize, rightBarVisible } = useBars();
|
||||||
let ref: HTMLDivElement | undefined;
|
let ref: HTMLDivElement | undefined;
|
||||||
let actualWidth = 0;
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
if (ref) {
|
|
||||||
const updateSize = () => {
|
|
||||||
actualWidth = ref?.offsetWidth || 0;
|
|
||||||
setRightBarSize(rightBarVisible() ? actualWidth : 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
updateSize();
|
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver((entries) => {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
actualWidth = ref?.offsetWidth || 0;
|
|
||||||
setRightBarSize(rightBarVisible() ? actualWidth : 0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
resizeObserver.observe(ref);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
resizeObserver.disconnect();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
setRightBarSize(rightBarVisible() ? actualWidth : 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
@@ -525,8 +475,8 @@ export function RightBar() {
|
|||||||
"translate-x-0": rightBarVisible()
|
"translate-x-0": rightBarVisible()
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
"transition-timing-function": "cubic-bezier(0.4, 0, 0.2, 1)", // Smooth for both revealing and hiding
|
"transition-timing-function": "cubic-bezier(0.4, 0, 0.2, 1)",
|
||||||
"min-width": rightBarSize() > 0 ? `${rightBarSize()}px` : undefined,
|
"min-width": "250px",
|
||||||
"box-shadow": "inset 6px 0 16px -6px rgba(0, 0, 0, 0.1)",
|
"box-shadow": "inset 6px 0 16px -6px rgba(0, 0, 0, 0.1)",
|
||||||
"padding-top": "env(safe-area-inset-top)",
|
"padding-top": "env(safe-area-inset-top)",
|
||||||
"padding-bottom": "env(safe-area-inset-bottom)"
|
"padding-bottom": "env(safe-area-inset-bottom)"
|
||||||
|
|||||||
@@ -63,61 +63,6 @@ export default function PostForm(props: PostFormProps) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const autoSave = async () => {
|
|
||||||
const titleVal = title();
|
|
||||||
|
|
||||||
if (titleVal) {
|
|
||||||
try {
|
|
||||||
let bannerImageKey = "";
|
|
||||||
const bannerFile = bannerImageFile();
|
|
||||||
if (bannerFile) {
|
|
||||||
bannerImageKey = (await AddImageToS3(
|
|
||||||
bannerFile,
|
|
||||||
titleVal,
|
|
||||||
"blog"
|
|
||||||
)) as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.mode === "edit" || createdPostId()) {
|
|
||||||
// Update existing post (either in edit mode or if already created)
|
|
||||||
await api.database.updatePost.mutate({
|
|
||||||
id: createdPostId() || props.postId!,
|
|
||||||
title: titleVal.replaceAll(" ", "_"),
|
|
||||||
subtitle: subtitle() || "",
|
|
||||||
body: body() || "Hello, World!",
|
|
||||||
banner_photo:
|
|
||||||
bannerImageKey !== ""
|
|
||||||
? bannerImageKey
|
|
||||||
: requestedDeleteImage()
|
|
||||||
? "_DELETE_IMAGE_"
|
|
||||||
: null,
|
|
||||||
published: published(),
|
|
||||||
tags: tags().length > 0 ? tags() : null,
|
|
||||||
author_id: props.userID
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Create mode: only save once (first autosave)
|
|
||||||
const result = await api.database.createPost.mutate({
|
|
||||||
category: "blog",
|
|
||||||
title: titleVal.replaceAll(" ", "_"),
|
|
||||||
subtitle: subtitle() || null,
|
|
||||||
body: body() || "Hello, World!",
|
|
||||||
banner_photo: bannerImageKey !== "" ? bannerImageKey : null,
|
|
||||||
published: published(),
|
|
||||||
tags: tags().length > 0 ? tags() : null,
|
|
||||||
author_id: props.userID
|
|
||||||
});
|
|
||||||
setCreatedPostId(result.data as number);
|
|
||||||
setHasSaved(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
showAutoSaveTrigger();
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Autosave failed:", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const showAutoSaveTrigger = () => {
|
const showAutoSaveTrigger = () => {
|
||||||
setShowAutoSaveMessage(true);
|
setShowAutoSaveMessage(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -125,27 +70,247 @@ export default function PostForm(props: PostFormProps) {
|
|||||||
}, 5000);
|
}, 5000);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Debounced auto-save (1 second after last change)
|
// Helper to ensure post exists (create if needed)
|
||||||
const debouncedAutoSave = debounce(autoSave, 1000);
|
const ensurePostExists = async (): Promise<number> => {
|
||||||
|
const existingId = createdPostId() || props.postId;
|
||||||
|
if (existingId) return existingId;
|
||||||
|
|
||||||
// Track changes to trigger auto-save
|
// Create minimal post if it doesn't exist yet
|
||||||
|
const result = await api.database.createPost.mutate({
|
||||||
|
category: "blog",
|
||||||
|
title: title().replaceAll(" ", "_") || "Untitled",
|
||||||
|
subtitle: null,
|
||||||
|
body: "Hello, World!",
|
||||||
|
banner_photo: null,
|
||||||
|
published: false,
|
||||||
|
tags: null,
|
||||||
|
author_id: props.userID
|
||||||
|
});
|
||||||
|
const newId = result.data as number;
|
||||||
|
setCreatedPostId(newId);
|
||||||
|
setHasSaved(true);
|
||||||
|
return newId;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Individual autosave functions for each field
|
||||||
|
const autoSaveTitle = async () => {
|
||||||
|
const currentTitle = title();
|
||||||
|
if (!currentTitle || currentTitle === props.initialData?.title) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const postId = await ensurePostExists();
|
||||||
|
await api.database.updatePost.mutate({
|
||||||
|
id: postId,
|
||||||
|
title: currentTitle.replaceAll(" ", "_"),
|
||||||
|
subtitle: subtitle() || null,
|
||||||
|
body: body() || "Hello, World!",
|
||||||
|
banner_photo: null,
|
||||||
|
published: published(),
|
||||||
|
tags: tags().length > 0 ? tags() : null,
|
||||||
|
author_id: props.userID
|
||||||
|
});
|
||||||
|
showAutoSaveTrigger();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Title autosave failed:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const autoSaveSubtitle = async () => {
|
||||||
|
const currentSubtitle = subtitle();
|
||||||
|
if (currentSubtitle === props.initialData?.subtitle) return;
|
||||||
|
if (!title()) return; // Need title to save
|
||||||
|
|
||||||
|
try {
|
||||||
|
const postId = await ensurePostExists();
|
||||||
|
await api.database.updatePost.mutate({
|
||||||
|
id: postId,
|
||||||
|
title: title().replaceAll(" ", "_"),
|
||||||
|
subtitle: currentSubtitle || null,
|
||||||
|
body: body() || "Hello, World!",
|
||||||
|
banner_photo: null,
|
||||||
|
published: published(),
|
||||||
|
tags: tags().length > 0 ? tags() : null,
|
||||||
|
author_id: props.userID
|
||||||
|
});
|
||||||
|
showAutoSaveTrigger();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Subtitle autosave failed:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const autoSaveBody = async () => {
|
||||||
|
const currentBody = body();
|
||||||
|
if (currentBody === props.initialData?.body) return;
|
||||||
|
if (!title()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const postId = await ensurePostExists();
|
||||||
|
await api.database.updatePost.mutate({
|
||||||
|
id: postId,
|
||||||
|
title: title().replaceAll(" ", "_"),
|
||||||
|
subtitle: subtitle() || null,
|
||||||
|
body: currentBody || "Hello, World!",
|
||||||
|
banner_photo: null,
|
||||||
|
published: published(),
|
||||||
|
tags: tags().length > 0 ? tags() : null,
|
||||||
|
author_id: props.userID
|
||||||
|
});
|
||||||
|
showAutoSaveTrigger();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Body autosave failed:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const autoSaveTags = async () => {
|
||||||
|
const currentTags = tags();
|
||||||
|
const initialTags = props.initialData?.tags || [];
|
||||||
|
if (JSON.stringify(currentTags) === JSON.stringify(initialTags)) return;
|
||||||
|
if (!title()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const postId = await ensurePostExists();
|
||||||
|
await api.database.updatePost.mutate({
|
||||||
|
id: postId,
|
||||||
|
title: title().replaceAll(" ", "_"),
|
||||||
|
subtitle: subtitle() || null,
|
||||||
|
body: body() || "Hello, World!",
|
||||||
|
banner_photo: null,
|
||||||
|
published: published(),
|
||||||
|
tags: currentTags.length > 0 ? currentTags : null,
|
||||||
|
author_id: props.userID
|
||||||
|
});
|
||||||
|
showAutoSaveTrigger();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Tags autosave failed:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const autoSavePublished = async () => {
|
||||||
|
const currentPublished = published();
|
||||||
|
if (currentPublished === props.initialData?.published) return;
|
||||||
|
if (!title()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const postId = await ensurePostExists();
|
||||||
|
await api.database.updatePost.mutate({
|
||||||
|
id: postId,
|
||||||
|
title: title().replaceAll(" ", "_"),
|
||||||
|
subtitle: subtitle() || null,
|
||||||
|
body: body() || "Hello, World!",
|
||||||
|
banner_photo: null,
|
||||||
|
published: currentPublished,
|
||||||
|
tags: tags().length > 0 ? tags() : null,
|
||||||
|
author_id: props.userID
|
||||||
|
});
|
||||||
|
showAutoSaveTrigger();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Published autosave failed:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const autoSaveBanner = async () => {
|
||||||
|
const bannerFile = bannerImageFile();
|
||||||
|
if (!bannerFile && !requestedDeleteImage()) return;
|
||||||
|
if (!title()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let bannerImageKey = "";
|
||||||
|
if (bannerFile) {
|
||||||
|
bannerImageKey = (await AddImageToS3(
|
||||||
|
bannerFile,
|
||||||
|
title(),
|
||||||
|
"blog"
|
||||||
|
)) as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const postId = await ensurePostExists();
|
||||||
|
await api.database.updatePost.mutate({
|
||||||
|
id: postId,
|
||||||
|
title: title().replaceAll(" ", "_"),
|
||||||
|
subtitle: subtitle() || null,
|
||||||
|
body: body() || "Hello, World!",
|
||||||
|
banner_photo:
|
||||||
|
bannerImageKey !== ""
|
||||||
|
? bannerImageKey
|
||||||
|
: requestedDeleteImage()
|
||||||
|
? "_DELETE_IMAGE_"
|
||||||
|
: null,
|
||||||
|
published: published(),
|
||||||
|
tags: tags().length > 0 ? tags() : null,
|
||||||
|
author_id: props.userID
|
||||||
|
});
|
||||||
|
showAutoSaveTrigger();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Banner autosave failed:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Debounced versions
|
||||||
|
const debouncedAutoSaveTitle = debounce(autoSaveTitle, 2500);
|
||||||
|
const debouncedAutoSaveSubtitle = debounce(autoSaveSubtitle, 2500);
|
||||||
|
const debouncedAutoSaveBody = debounce(autoSaveBody, 2500);
|
||||||
|
const debouncedAutoSaveTags = debounce(autoSaveTags, 2500);
|
||||||
|
const debouncedAutoSavePublished = debounce(autoSavePublished, 1000);
|
||||||
|
const debouncedAutoSaveBanner = debounce(autoSaveBanner, 2500);
|
||||||
|
|
||||||
|
// Individual effects for each field
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const titleVal = title();
|
const titleVal = title();
|
||||||
const subtitleVal = subtitle();
|
if (isInitialLoad()) return;
|
||||||
const bodyVal = body();
|
if (titleVal && titleVal !== props.initialData?.title) {
|
||||||
const tagsVal = tags();
|
debouncedAutoSaveTitle();
|
||||||
const publishedVal = published();
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Only trigger auto-save if conditions are met
|
createEffect(() => {
|
||||||
if (props.mode === "edit" && !isInitialLoad() && titleVal) {
|
const subtitleVal = subtitle();
|
||||||
debouncedAutoSave();
|
if (isInitialLoad()) return;
|
||||||
} else if (props.mode === "create" && titleVal) {
|
if (subtitleVal !== props.initialData?.subtitle) {
|
||||||
debouncedAutoSave();
|
debouncedAutoSaveSubtitle();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const bodyVal = body();
|
||||||
|
if (isInitialLoad()) return;
|
||||||
|
if (bodyVal !== props.initialData?.body) {
|
||||||
|
debouncedAutoSaveBody();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const tagsVal = tags();
|
||||||
|
if (isInitialLoad()) return;
|
||||||
|
const initialTags = props.initialData?.tags || [];
|
||||||
|
if (JSON.stringify(tagsVal) !== JSON.stringify(initialTags)) {
|
||||||
|
debouncedAutoSaveTags();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const publishedVal = published();
|
||||||
|
if (isInitialLoad()) return;
|
||||||
|
if (publishedVal !== props.initialData?.published) {
|
||||||
|
debouncedAutoSavePublished();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const bannerFile = bannerImageFile();
|
||||||
|
const deleteRequested = requestedDeleteImage();
|
||||||
|
if (isInitialLoad()) return;
|
||||||
|
if (bannerFile || deleteRequested) {
|
||||||
|
debouncedAutoSaveBanner();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
debouncedAutoSave.cancel();
|
debouncedAutoSaveTitle.cancel();
|
||||||
|
debouncedAutoSaveSubtitle.cancel();
|
||||||
|
debouncedAutoSaveBody.cancel();
|
||||||
|
debouncedAutoSaveTags.cancel();
|
||||||
|
debouncedAutoSavePublished.cancel();
|
||||||
|
debouncedAutoSaveBanner.cancel();
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleBannerImageDrop = (acceptedFiles: File[]) => {
|
const handleBannerImageDrop = (acceptedFiles: File[]) => {
|
||||||
|
|||||||
@@ -1,37 +1,18 @@
|
|||||||
import {
|
import { Accessor, createContext, useContext } from "solid-js";
|
||||||
Accessor,
|
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
createMemo,
|
|
||||||
onMount
|
|
||||||
} from "solid-js";
|
|
||||||
import { createSignal } from "solid-js";
|
import { createSignal } from "solid-js";
|
||||||
import { isMobile, MOBILE_BREAKPOINT } from "~/lib/resize-utils";
|
|
||||||
|
export const STATIC_BAR_SIZE = 250;
|
||||||
|
|
||||||
const BarsContext = createContext<{
|
const BarsContext = createContext<{
|
||||||
leftBarSize: Accessor<number>;
|
|
||||||
setLeftBarSize: (size: number) => void;
|
|
||||||
rightBarSize: Accessor<number>;
|
|
||||||
setRightBarSize: (size: number) => void;
|
|
||||||
centerWidth: Accessor<number>;
|
|
||||||
setCenterWidth: (size: number) => void;
|
|
||||||
leftBarVisible: Accessor<boolean>;
|
leftBarVisible: Accessor<boolean>;
|
||||||
setLeftBarVisible: (visible: boolean) => void;
|
setLeftBarVisible: (visible: boolean) => void;
|
||||||
rightBarVisible: Accessor<boolean>;
|
rightBarVisible: Accessor<boolean>;
|
||||||
setRightBarVisible: (visible: boolean) => void;
|
setRightBarVisible: (visible: boolean) => void;
|
||||||
barsInitialized: Accessor<boolean>;
|
|
||||||
}>({
|
}>({
|
||||||
leftBarSize: () => 0,
|
|
||||||
setLeftBarSize: () => {},
|
|
||||||
rightBarSize: () => 0,
|
|
||||||
setRightBarSize: () => {},
|
|
||||||
centerWidth: () => 0,
|
|
||||||
setCenterWidth: () => {},
|
|
||||||
leftBarVisible: () => true,
|
leftBarVisible: () => true,
|
||||||
setLeftBarVisible: () => {},
|
setLeftBarVisible: () => {},
|
||||||
rightBarVisible: () => true,
|
rightBarVisible: () => true,
|
||||||
setRightBarVisible: () => {},
|
setRightBarVisible: () => {}
|
||||||
barsInitialized: () => false
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export function useBars() {
|
export function useBars() {
|
||||||
@@ -40,106 +21,16 @@ export function useBars() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function BarsProvider(props: { children: any }) {
|
export function BarsProvider(props: { children: any }) {
|
||||||
const [_leftBarNaturalSize, _setLeftBarNaturalSize] = createSignal(0);
|
|
||||||
const [_rightBarNaturalSize, _setRightBarNaturalSize] = createSignal(0);
|
|
||||||
const [syncedBarSize, setSyncedBarSize] = createSignal(0);
|
|
||||||
const [centerWidth, setCenterWidth] = createSignal(0);
|
|
||||||
const [windowWidth, setWindowWidth] = createSignal(
|
|
||||||
typeof window !== "undefined" ? window.innerWidth : 1024
|
|
||||||
);
|
|
||||||
const [leftBarVisible, setLeftBarVisible] = createSignal(true);
|
const [leftBarVisible, setLeftBarVisible] = createSignal(true);
|
||||||
const [rightBarVisible, setRightBarVisible] = createSignal(true);
|
const [rightBarVisible, setRightBarVisible] = createSignal(true);
|
||||||
const [barsInitialized, setBarsInitialized] = createSignal(false);
|
|
||||||
|
|
||||||
let leftBarSized = false;
|
|
||||||
let rightBarSized = false;
|
|
||||||
|
|
||||||
// Setup window width tracking and initial mobile detection on client only
|
|
||||||
onMount(() => {
|
|
||||||
// Immediately sync to actual window width
|
|
||||||
setWindowWidth(window.innerWidth);
|
|
||||||
|
|
||||||
const handleResize = () => {
|
|
||||||
setWindowWidth(window.innerWidth);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("resize", handleResize);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("resize", handleResize);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const wrappedSetLeftBarSize = (size: number) => {
|
|
||||||
if (!barsInitialized()) {
|
|
||||||
// Before initialization, capture natural size
|
|
||||||
_setLeftBarNaturalSize(size);
|
|
||||||
if (!leftBarSized && size > 0) {
|
|
||||||
leftBarSized = true;
|
|
||||||
checkAndSync();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// After initialization, just update the natural size for visibility handling
|
|
||||||
_setLeftBarNaturalSize(size);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkAndSync = () => {
|
|
||||||
const currentIsMobile = isMobile(windowWidth());
|
|
||||||
const bothBarsReady = leftBarSized && (currentIsMobile || rightBarSized);
|
|
||||||
|
|
||||||
if (bothBarsReady) {
|
|
||||||
const maxWidth = Math.max(_leftBarNaturalSize(), _rightBarNaturalSize());
|
|
||||||
setSyncedBarSize(maxWidth);
|
|
||||||
setBarsInitialized(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrappedSetRightBarSize = (size: number) => {
|
|
||||||
if (!barsInitialized()) {
|
|
||||||
// Before initialization, capture natural size
|
|
||||||
_setRightBarNaturalSize(size);
|
|
||||||
if (!rightBarSized && size > 0) {
|
|
||||||
rightBarSized = true;
|
|
||||||
checkAndSync();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// After initialization, just update the natural size for visibility handling
|
|
||||||
_setRightBarNaturalSize(size);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const leftBarSize = createMemo(() => {
|
|
||||||
// Return 0 if hidden (natural size is 0), otherwise return synced size when initialized
|
|
||||||
const naturalSize = _leftBarNaturalSize();
|
|
||||||
if (naturalSize === 0) return 0; // Hidden
|
|
||||||
// On mobile (<768px), always return 0 for layout (overlay mode)
|
|
||||||
const currentIsMobile = isMobile(windowWidth());
|
|
||||||
if (currentIsMobile) return 0;
|
|
||||||
return barsInitialized() ? syncedBarSize() : naturalSize;
|
|
||||||
});
|
|
||||||
|
|
||||||
const rightBarSize = createMemo(() => {
|
|
||||||
// Return 0 if hidden (natural size is 0), otherwise return synced size when initialized
|
|
||||||
const naturalSize = _rightBarNaturalSize();
|
|
||||||
if (naturalSize === 0) return 0; // Hidden
|
|
||||||
return barsInitialized() ? syncedBarSize() : naturalSize;
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BarsContext.Provider
|
<BarsContext.Provider
|
||||||
value={{
|
value={{
|
||||||
leftBarSize,
|
|
||||||
setLeftBarSize: wrappedSetLeftBarSize,
|
|
||||||
rightBarSize,
|
|
||||||
setRightBarSize: wrappedSetRightBarSize,
|
|
||||||
centerWidth,
|
|
||||||
setCenterWidth,
|
|
||||||
leftBarVisible,
|
leftBarVisible,
|
||||||
setLeftBarVisible,
|
setLeftBarVisible,
|
||||||
rightBarVisible,
|
rightBarVisible,
|
||||||
setRightBarVisible,
|
setRightBarVisible
|
||||||
barsInitialized
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import CommentIcon from "~/components/icons/CommentIcon";
|
|||||||
import CommentSectionWrapper from "~/components/blog/CommentSectionWrapper";
|
import CommentSectionWrapper from "~/components/blog/CommentSectionWrapper";
|
||||||
import PostBodyClient from "~/components/blog/PostBodyClient";
|
import PostBodyClient from "~/components/blog/PostBodyClient";
|
||||||
import type { Comment, CommentReaction, UserPublicData } from "~/types/comment";
|
import type { Comment, CommentReaction, UserPublicData } from "~/types/comment";
|
||||||
import { useBars } from "~/context/bars";
|
|
||||||
import { TerminalSplash } from "~/components/TerminalSplash";
|
import { TerminalSplash } from "~/components/TerminalSplash";
|
||||||
|
|
||||||
// Server function to fetch post by title
|
// Server function to fetch post by title
|
||||||
@@ -222,7 +221,6 @@ const getPostByTitle = query(
|
|||||||
export default function PostPage() {
|
export default function PostPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const { centerWidth, leftBarSize, barsInitialized } = useBars();
|
|
||||||
|
|
||||||
const data = createAsync(
|
const data = createAsync(
|
||||||
() => {
|
() => {
|
||||||
@@ -278,29 +276,19 @@ export default function PostPage() {
|
|||||||
src={p().banner_photo || "/blueprint.jpg"}
|
src={p().banner_photo || "/blueprint.jpg"}
|
||||||
alt="post-cover"
|
alt="post-cover"
|
||||||
class="blog-banner-image h-full object-cover select-none"
|
class="blog-banner-image h-full object-cover select-none"
|
||||||
style={
|
style={{
|
||||||
barsInitialized()
|
width: "calc(100vw - 500px)",
|
||||||
? {
|
"margin-left": "250px",
|
||||||
width: `${centerWidth()}px`,
|
"pointer-events": "none"
|
||||||
"margin-left": `${leftBarSize()}px`,
|
}}
|
||||||
"pointer-events": "none"
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
"pointer-events": "none"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="text-shadow text-text blog-banner-text absolute top-1/3 z-10 my-auto px-4 text-center tracking-widest brightness-150 select-text"
|
class="text-shadow text-text blog-banner-text absolute top-1/3 z-10 my-auto px-4 text-center tracking-widest brightness-150 select-text"
|
||||||
style={
|
style={{
|
||||||
barsInitialized()
|
width: "calc(100vw - 500px)",
|
||||||
? {
|
"margin-left": "250px"
|
||||||
width: `${centerWidth()}px`,
|
}}
|
||||||
"margin-left": `${leftBarSize()}px`
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div class="text-3xl font-light tracking-widest">
|
<div class="text-3xl font-light tracking-widest">
|
||||||
{p().title.replaceAll("_", " ")}
|
{p().title.replaceAll("_", " ")}
|
||||||
@@ -374,7 +362,7 @@ export default function PostPage() {
|
|||||||
<Show when={postData.privilegeLevel === "admin"}>
|
<Show when={postData.privilegeLevel === "admin"}>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<A
|
<A
|
||||||
class="border-blue bg-blue z-100 h-fit rounded border px-4 py-2 text-base shadow-md transition-all duration-300 ease-in-out hover:brightness-125 active:scale-90"
|
class="border-blue bg-blue z-10 h-fit rounded border px-4 py-2 text-base shadow-md transition-all duration-300 ease-in-out hover:brightness-125 active:scale-90"
|
||||||
href={`/blog/edit/${p().id}`}
|
href={`/blog/edit/${p().id}`}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export default function Home() {
|
|||||||
<div class="pb-4">Some of my recent projects:</div>
|
<div class="pb-4">Some of my recent projects:</div>
|
||||||
<div class="flex flex-col items-center gap-2 xl:flex-row xl:items-start xl:justify-center">
|
<div class="flex flex-col items-center gap-2 xl:flex-row xl:items-start xl:justify-center">
|
||||||
{/* FlexLöve */}
|
{/* FlexLöve */}
|
||||||
<div class="border-surface0 flex w-full max-w-md flex-col rounded-md border-2 p-4 text-center">
|
<div class="border-surface0 flex w-full max-w-3/4 flex-col rounded-md border-2 p-4 text-center">
|
||||||
<div>My LÖVE UI library</div>
|
<div>My LÖVE UI library</div>
|
||||||
<a
|
<a
|
||||||
href="https://github.com/mikefreno/flexlove"
|
href="https://github.com/mikefreno/flexlove"
|
||||||
|
|||||||
Reference in New Issue
Block a user