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 { 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 && (
|
||||
<span className="animate-pulse text-foreground/40">|</span>
|
||||
)}
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user