mobile optimizations

This commit is contained in:
2026-04-06 13:57:15 -07:00
parent 873090310a
commit 336c652bf7
6 changed files with 85 additions and 45 deletions

View File

@@ -1,18 +1,14 @@
import { RssIcon, TagIcon, TrendingUpIcon } from "lucide-react";
import { AnimateIn } from "@/components/animate-in";
import { TypedText } from "@/components/typed-text";
export const BlogHeader = () => {
return (
<div className="w-full max-w-6xl mx-auto px-4 pt-12 md:pt-24">
<div className="mb-3 text-center px-4">
<TypedText
text="Latest Thoughts & Writings"
as="h1"
className="text-2xl sm:text-3xl font-bold text-purple leading-relaxed"
speed={20}
/>
</div>
<AnimateIn>
<h1 className="text-2xl sm:text-3xl font-bold text-purple mb-3 text-center px-4 leading-relaxed">
Latest Thoughts & Writings
</h1>
</AnimateIn>
<AnimateIn delay={100}>
<div className="flex flex-wrap justify-center gap-4 mb-12 text-sm sm:text-base">
<a

View File

@@ -1,5 +1,4 @@
import { AnimateIn } from "@/components/animate-in";
import { TypedText } from "@/components/typed-text";
import { RssIcon, TagIcon, TrendingUpIcon } from "lucide-react";
type BlogPost = {
@@ -32,14 +31,11 @@ const TaggedPosts = ({ tag, posts }: TaggedPostsProps) => {
return (
<div className="w-full max-w-6xl mx-auto">
<div className="w-full px-4 pt-12 md:pt-24">
<div className="mb-3 text-center px-4">
<TypedText
text={`#${tag}`}
as="h1"
className="text-2xl sm:text-3xl font-bold text-purple leading-relaxed"
speed={20}
/>
</div>
<AnimateIn>
<h1 className="text-2xl sm:text-3xl font-bold text-purple mb-3 text-center px-4 leading-relaxed">
#{tag}
</h1>
</AnimateIn>
<AnimateIn delay={100}>
<div className="flex flex-wrap justify-center gap-4 mb-12 text-sm sm:text-base">
<a

View File

@@ -59,7 +59,7 @@ export default function MobileNav({ transparent = false }: { transparent?: boole
} ${
transparent
? "bg-transparent"
: "bg-background/30 backdrop-blur-sm"
: "bg-background border-t border-foreground/10"
}`}
style={{
paddingBottom: "env(safe-area-inset-bottom, 0px)",

View File

@@ -1,6 +1,5 @@
import type { CollectionEntry } from "astro:content";
import { AnimateIn } from "@/components/animate-in";
import { TypedText } from "@/components/typed-text";
interface ProjectListProps {
projects: CollectionEntry<"projects">[];
@@ -9,14 +8,11 @@ interface ProjectListProps {
export function ProjectList({ projects }: ProjectListProps) {
return (
<div className="w-full max-w-6xl mx-auto pt-12 md:pt-24 lg:pt-32 px-4">
<div className="mb-12 text-center">
<TypedText
text="Here's what I've been building lately"
as="h1"
className="text-2xl sm:text-3xl font-bold text-blue leading-relaxed"
speed={20}
/>
</div>
<AnimateIn>
<h1 className="text-2xl sm:text-3xl font-bold text-blue mb-12 text-center leading-relaxed">
Here's what I've been building lately
</h1>
</AnimateIn>
<ul className="space-y-6 md:space-y-10">
{projects.map((project, i) => (

View File

@@ -9,29 +9,34 @@ function typeInHeading(el: HTMLElement): Promise<void> {
const textLength = text.length;
if (textLength === 0) { resolve(); return; }
// Preserve height
const rect = el.getBoundingClientRect();
el.style.minHeight = `${rect.height}px`;
// Speed scales with length
const speed = Math.max(8, Math.min(25, 600 / textLength));
// Store original HTML, clear visible text but keep element structure
const originalHTML = el.innerHTML;
el.textContent = "";
// Wrap each character in a span with opacity:0
// The full text stays in the DOM so layout/wrapping is correct from the start
el.innerHTML = "";
const chars: HTMLSpanElement[] = [];
for (const char of text) {
const span = document.createElement("span");
span.textContent = char;
span.style.opacity = "0";
chars.push(span);
el.appendChild(span);
}
el.style.opacity = "1";
el.style.transform = "translate3d(0,0,0)";
let i = 0;
const step = () => {
if (i >= text.length) {
if (i >= chars.length) {
// Restore original HTML to clean up spans
el.innerHTML = originalHTML;
el.style.minHeight = "";
resolve();
return;
}
chars[i].style.opacity = "1";
i++;
el.textContent = text.slice(0, i);
setTimeout(step, speed);
};
step();

View File

@@ -79,17 +79,64 @@ export function TypedText({
speed = 12,
cursor = true,
}: TypedTextProps) {
const containerRef = useRef<HTMLDivElement>(null);
const tagRef = useRef<HTMLElement>(null);
const { ref, visible } = useScrollVisible();
const { displayed, done } = useTypewriter(text, visible, speed);
const [done, setDone] = useState(false);
const [started, setStarted] = useState(false);
useEffect(() => {
if (!visible || started) return;
setStarted(true);
if (prefersReducedMotion()) {
setDone(true);
return;
}
const el = tagRef.current;
if (!el) return;
// Wrap each character in an invisible span — layout stays correct
el.textContent = "";
const chars: HTMLSpanElement[] = [];
for (const char of text) {
const span = document.createElement("span");
span.textContent = char;
span.style.opacity = "0";
chars.push(span);
el.appendChild(span);
}
const textLength = text.length;
const charSpeed = Math.max(8, Math.min(speed, 600 / textLength));
let i = 0;
const step = () => {
if (i >= chars.length) {
el.textContent = text;
setDone(true);
return;
}
chars[i].style.opacity = "1";
i++;
setTimeout(step, charSpeed);
};
step();
}, [visible, started, text, speed]);
return (
<div ref={ref}>
<Tag className={className} style={{ minHeight: "1.2em" }}>
{visible ? displayed : "\u00A0"}
{cursor && visible && !done && (
<Tag
ref={tagRef as any}
className={className}
style={{ minHeight: "1.2em" }}
>
{!started ? "\u00A0" : done ? text : null}
</Tag>
{cursor && started && !done && (
<span className="animate-pulse text-foreground/40">|</span>
)}
</Tag>
</div>
);
}