mirror of
https://github.com/timmypidashev/web.git
synced 2026-04-14 11:03:50 +00:00
mobile optimizations
This commit is contained in:
@@ -1,18 +1,14 @@
|
|||||||
import { RssIcon, TagIcon, TrendingUpIcon } from "lucide-react";
|
import { RssIcon, TagIcon, TrendingUpIcon } from "lucide-react";
|
||||||
import { AnimateIn } from "@/components/animate-in";
|
import { AnimateIn } from "@/components/animate-in";
|
||||||
import { TypedText } from "@/components/typed-text";
|
|
||||||
|
|
||||||
export const BlogHeader = () => {
|
export const BlogHeader = () => {
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-6xl mx-auto px-4 pt-12 md:pt-24">
|
<div className="w-full max-w-6xl mx-auto px-4 pt-12 md:pt-24">
|
||||||
<div className="mb-3 text-center px-4">
|
<AnimateIn>
|
||||||
<TypedText
|
<h1 className="text-2xl sm:text-3xl font-bold text-purple mb-3 text-center px-4 leading-relaxed">
|
||||||
text="Latest Thoughts & Writings"
|
Latest Thoughts & Writings
|
||||||
as="h1"
|
</h1>
|
||||||
className="text-2xl sm:text-3xl font-bold text-purple leading-relaxed"
|
</AnimateIn>
|
||||||
speed={20}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<AnimateIn delay={100}>
|
<AnimateIn delay={100}>
|
||||||
<div className="flex flex-wrap justify-center gap-4 mb-12 text-sm sm:text-base">
|
<div className="flex flex-wrap justify-center gap-4 mb-12 text-sm sm:text-base">
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { AnimateIn } from "@/components/animate-in";
|
import { AnimateIn } from "@/components/animate-in";
|
||||||
import { TypedText } from "@/components/typed-text";
|
|
||||||
import { RssIcon, TagIcon, TrendingUpIcon } from "lucide-react";
|
import { RssIcon, TagIcon, TrendingUpIcon } from "lucide-react";
|
||||||
|
|
||||||
type BlogPost = {
|
type BlogPost = {
|
||||||
@@ -32,14 +31,11 @@ const TaggedPosts = ({ tag, posts }: TaggedPostsProps) => {
|
|||||||
return (
|
return (
|
||||||
<div className="w-full max-w-6xl mx-auto">
|
<div className="w-full max-w-6xl mx-auto">
|
||||||
<div className="w-full px-4 pt-12 md:pt-24">
|
<div className="w-full px-4 pt-12 md:pt-24">
|
||||||
<div className="mb-3 text-center px-4">
|
<AnimateIn>
|
||||||
<TypedText
|
<h1 className="text-2xl sm:text-3xl font-bold text-purple mb-3 text-center px-4 leading-relaxed">
|
||||||
text={`#${tag}`}
|
#{tag}
|
||||||
as="h1"
|
</h1>
|
||||||
className="text-2xl sm:text-3xl font-bold text-purple leading-relaxed"
|
</AnimateIn>
|
||||||
speed={20}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<AnimateIn delay={100}>
|
<AnimateIn delay={100}>
|
||||||
<div className="flex flex-wrap justify-center gap-4 mb-12 text-sm sm:text-base">
|
<div className="flex flex-wrap justify-center gap-4 mb-12 text-sm sm:text-base">
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export default function MobileNav({ transparent = false }: { transparent?: boole
|
|||||||
} ${
|
} ${
|
||||||
transparent
|
transparent
|
||||||
? "bg-transparent"
|
? "bg-transparent"
|
||||||
: "bg-background/30 backdrop-blur-sm"
|
: "bg-background border-t border-foreground/10"
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
paddingBottom: "env(safe-area-inset-bottom, 0px)",
|
paddingBottom: "env(safe-area-inset-bottom, 0px)",
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { CollectionEntry } from "astro:content";
|
import type { CollectionEntry } from "astro:content";
|
||||||
import { AnimateIn } from "@/components/animate-in";
|
import { AnimateIn } from "@/components/animate-in";
|
||||||
import { TypedText } from "@/components/typed-text";
|
|
||||||
|
|
||||||
interface ProjectListProps {
|
interface ProjectListProps {
|
||||||
projects: CollectionEntry<"projects">[];
|
projects: CollectionEntry<"projects">[];
|
||||||
@@ -9,14 +8,11 @@ interface ProjectListProps {
|
|||||||
export function ProjectList({ projects }: ProjectListProps) {
|
export function ProjectList({ projects }: ProjectListProps) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-6xl mx-auto pt-12 md:pt-24 lg:pt-32 px-4">
|
<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">
|
<AnimateIn>
|
||||||
<TypedText
|
<h1 className="text-2xl sm:text-3xl font-bold text-blue mb-12 text-center leading-relaxed">
|
||||||
text="Here's what I've been building lately"
|
Here's what I've been building lately
|
||||||
as="h1"
|
</h1>
|
||||||
className="text-2xl sm:text-3xl font-bold text-blue leading-relaxed"
|
</AnimateIn>
|
||||||
speed={20}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul className="space-y-6 md:space-y-10">
|
<ul className="space-y-6 md:space-y-10">
|
||||||
{projects.map((project, i) => (
|
{projects.map((project, i) => (
|
||||||
|
|||||||
@@ -9,29 +9,34 @@ function typeInHeading(el: HTMLElement): Promise<void> {
|
|||||||
const textLength = text.length;
|
const textLength = text.length;
|
||||||
if (textLength === 0) { resolve(); return; }
|
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));
|
const speed = Math.max(8, Math.min(25, 600 / textLength));
|
||||||
|
|
||||||
// Store original HTML, clear visible text but keep element structure
|
|
||||||
const originalHTML = el.innerHTML;
|
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.opacity = "1";
|
||||||
el.style.transform = "translate3d(0,0,0)";
|
el.style.transform = "translate3d(0,0,0)";
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
const step = () => {
|
const step = () => {
|
||||||
if (i >= text.length) {
|
if (i >= chars.length) {
|
||||||
|
// Restore original HTML to clean up spans
|
||||||
el.innerHTML = originalHTML;
|
el.innerHTML = originalHTML;
|
||||||
el.style.minHeight = "";
|
|
||||||
resolve();
|
resolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
chars[i].style.opacity = "1";
|
||||||
i++;
|
i++;
|
||||||
el.textContent = text.slice(0, i);
|
|
||||||
setTimeout(step, speed);
|
setTimeout(step, speed);
|
||||||
};
|
};
|
||||||
step();
|
step();
|
||||||
|
|||||||
@@ -79,17 +79,64 @@ export function TypedText({
|
|||||||
speed = 12,
|
speed = 12,
|
||||||
cursor = true,
|
cursor = true,
|
||||||
}: TypedTextProps) {
|
}: TypedTextProps) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const tagRef = useRef<HTMLElement>(null);
|
||||||
const { ref, visible } = useScrollVisible();
|
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 (
|
return (
|
||||||
<div ref={ref}>
|
<div ref={ref}>
|
||||||
<Tag className={className} style={{ minHeight: "1.2em" }}>
|
<Tag
|
||||||
{visible ? displayed : "\u00A0"}
|
ref={tagRef as any}
|
||||||
{cursor && visible && !done && (
|
className={className}
|
||||||
<span className="animate-pulse text-foreground/40">|</span>
|
style={{ minHeight: "1.2em" }}
|
||||||
)}
|
>
|
||||||
|
{!started ? "\u00A0" : done ? text : null}
|
||||||
</Tag>
|
</Tag>
|
||||||
|
{cursor && started && !done && (
|
||||||
|
<span className="animate-pulse text-foreground/40">|</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user