diff --git a/src/components/Typewriter.tsx b/src/components/Typewriter.tsx
index 09e9d2e..6e42da1 100644
--- a/src/components/Typewriter.tsx
+++ b/src/components/Typewriter.tsx
@@ -12,9 +12,7 @@ export function Typewriter(props: {
let cursorRef: HTMLDivElement | undefined;
const [isTyping, setIsTyping] = createSignal(false);
const [isDelaying, setIsDelaying] = createSignal(delay > 0);
- const [keepAliveCountdown, setKeepAliveCountdown] = createSignal(
- typeof keepAlive === "number" ? keepAlive : -1
- );
+ const [shouldHide, setShouldHide] = createSignal(false);
const resolved = children(() => props.children);
onMount(() => {
@@ -67,6 +65,12 @@ export function Typewriter(props: {
cursorRef.style.height = `${firstChar.offsetHeight}px`;
}
+ // Listen for animation end to hide cursor
+ const handleAnimationEnd = () => {
+ setShouldHide(true);
+ cursorRef?.removeEventListener("animationend", handleAnimationEnd);
+ };
+
const startReveal = () => {
setIsTyping(true); // Switch to typing cursor
@@ -102,17 +106,17 @@ export function Typewriter(props: {
// Typing finished, switch to block cursor
setIsTyping(false);
- // Start keepAlive countdown if it's a number
+ // Start keepAlive timer if it's a number
if (typeof keepAlive === "number") {
- const keepAliveInterval = setInterval(() => {
- setKeepAliveCountdown((prev) => {
- if (prev <= 1000) {
- clearInterval(keepAliveInterval);
- return 0;
- }
- return prev - 1000;
- });
- }, 1000);
+ // Attach animation end listener
+ cursorRef?.addEventListener("animationend", handleAnimationEnd);
+
+ // Trigger the animation with finite iteration count
+ const durationSeconds = keepAlive / 1000;
+ const iterations = Math.ceil(durationSeconds);
+ if (cursorRef) {
+ cursorRef.style.animation = `blink 1s ${iterations}`;
+ }
}
}
};
@@ -132,22 +136,16 @@ export function Typewriter(props: {
});
const getCursorClass = () => {
- if (isDelaying()) return "cursor-block"; // Blinking block during delay
- if (isTyping()) return "cursor-typing"; // Thin line while typing
-
- // After typing is done
- if (typeof keepAlive === "number") {
- return keepAliveCountdown() > 0 ? "cursor-block" : "hidden";
- }
+ if (isDelaying()) return "cursor-block";
+ if (isTyping()) return "cursor-typing";
+ if (shouldHide()) return "hidden";
return keepAlive ? "cursor-block" : "hidden";
};
return (
{resolved()}
-
- {" "}
-
+
);
}
diff --git a/src/lib/client-utils.ts b/src/lib/client-utils.ts
index 12c0d9a..b4803db 100644
--- a/src/lib/client-utils.ts
+++ b/src/lib/client-utils.ts
@@ -80,3 +80,34 @@ export function insertSoftHyphens(
})
.join(" ");
}
+
+/**
+ * Creates a debounced function that delays execution until after specified delay
+ * @param fn - The function to debounce
+ * @param delay - Delay in milliseconds
+ * @returns Debounced function with cancel method
+ */
+export function debounce any>(
+ fn: T,
+ delay: number
+): T & { cancel: () => void } {
+ let timeoutId: ReturnType | undefined;
+
+ const debounced = function (this: any, ...args: Parameters) {
+ if (timeoutId !== undefined) {
+ clearTimeout(timeoutId);
+ }
+ timeoutId = setTimeout(() => {
+ fn.apply(this, args);
+ }, delay);
+ } as T & { cancel: () => void };
+
+ debounced.cancel = () => {
+ if (timeoutId !== undefined) {
+ clearTimeout(timeoutId);
+ timeoutId = undefined;
+ }
+ };
+
+ return debounced;
+}
diff --git a/src/routes/blog/create/index.tsx b/src/routes/blog/create/index.tsx
index 9d1a9b9..2c47e39 100644
--- a/src/routes/blog/create/index.tsx
+++ b/src/routes/blog/create/index.tsx
@@ -1,10 +1,11 @@
-import { Show, createSignal, onCleanup } from "solid-js";
+import { Show, createSignal, createEffect, onCleanup } from "solid-js";
import { useNavigate, query } from "@solidjs/router";
import { Title } from "@solidjs/meta";
import { createAsync } from "@solidjs/router";
import { getRequestEvent } from "solid-js/web";
import { getPrivilegeLevel, getUserID } from "~/server/utils";
import { api } from "~/lib/api";
+import { debounce } from "~/lib/client-utils";
import Dropzone from "~/components/blog/Dropzone";
import TextEditor from "~/components/blog/TextEditor";
import TagMaker from "~/components/blog/TagMaker";
@@ -42,8 +43,6 @@ export default function CreatePost() {
const [showAutoSaveMessage, setShowAutoSaveMessage] = createSignal(false);
const [hasSaved, setHasSaved] = createSignal(false);
- let autosaveInterval: number | undefined;
-
const autoSave = async () => {
const titleVal = title();
const bodyVal = body();
@@ -90,18 +89,26 @@ export default function CreatePost() {
}, 5000);
};
- // Set up autosave interval (2 minutes)
- autosaveInterval = setInterval(
- () => {
- autoSave();
- },
- 2 * 60 * 1000
- ) as unknown as number;
+ // Debounced auto-save (1 second after last change)
+ const debouncedAutoSave = debounce(autoSave, 1000);
+
+ // Track changes to trigger auto-save
+ createEffect(() => {
+ // Track all relevant fields
+ const titleVal = title();
+ const subtitleVal = subtitle();
+ const bodyVal = body();
+ const tagsVal = tags();
+ const publishedVal = published();
+
+ // Only trigger auto-save if we have at least title and body
+ if (titleVal && bodyVal) {
+ debouncedAutoSave();
+ }
+ });
onCleanup(() => {
- if (autosaveInterval) {
- clearInterval(autosaveInterval);
- }
+ debouncedAutoSave.cancel();
});
const handleBannerImageDrop = (acceptedFiles: File[]) => {
diff --git a/src/routes/blog/edit/[id]/index.tsx b/src/routes/blog/edit/[id]/index.tsx
index 1d85ae6..48a6798 100644
--- a/src/routes/blog/edit/[id]/index.tsx
+++ b/src/routes/blog/edit/[id]/index.tsx
@@ -5,6 +5,7 @@ import { createAsync } from "@solidjs/router";
import { getRequestEvent } from "solid-js/web";
import { getPrivilegeLevel, getUserID } from "~/server/utils";
import { api } from "~/lib/api";
+import { debounce } from "~/lib/client-utils";
import { ConnectionFactory } from "~/server/utils";
import Dropzone from "~/components/blog/Dropzone";
import TextEditor from "~/components/blog/TextEditor";
@@ -60,8 +61,7 @@ export default function EditPost() {
const [loading, setLoading] = createSignal(false);
const [error, setError] = createSignal("");
const [showAutoSaveMessage, setShowAutoSaveMessage] = createSignal(false);
-
- let autosaveInterval: number | undefined;
+ const [isInitialLoad, setIsInitialLoad] = createSignal(true);
// Populate form when data loads
createEffect(() => {
@@ -78,6 +78,9 @@ export default function EditPost() {
const tagValues = (postData.tags as any[]).map((t) => t.value);
setTags(tagValues);
}
+
+ // Mark initial load as complete after data is loaded
+ setIsInitialLoad(false);
}
});
@@ -127,18 +130,26 @@ export default function EditPost() {
}, 5000);
};
- // Set up autosave interval (2 minutes)
- autosaveInterval = setInterval(
- () => {
- autoSave();
- },
- 2 * 60 * 1000
- ) as unknown as number;
+ // Debounced auto-save (1 second after last change)
+ const debouncedAutoSave = debounce(autoSave, 1000);
+
+ // Track changes to trigger auto-save (but not on initial load)
+ createEffect(() => {
+ // Track all relevant fields
+ const titleVal = title();
+ const subtitleVal = subtitle();
+ const bodyVal = body();
+ const tagsVal = tags();
+ const publishedVal = published();
+
+ // Only trigger auto-save if not initial load and we have title
+ if (!isInitialLoad() && titleVal) {
+ debouncedAutoSave();
+ }
+ });
onCleanup(() => {
- if (autosaveInterval) {
- clearInterval(autosaveInterval);
- }
+ debouncedAutoSave.cancel();
});
const handleBannerImageDrop = (acceptedFiles: File[]) => {
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index 6b1d492..7e2d87c 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -40,7 +40,7 @@ export default function Home() {
My Collection of By-the-ways:
-
+
- I use Neovim
- I use Arch Linux
- I use Rust