Skip to content

Commit 7bc9362

Browse files
committed
add animation to tags on posts cards
1 parent e0533f2 commit 7bc9362

10 files changed

Lines changed: 181 additions & 51 deletions

File tree

app/globals.css

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,29 @@
113113
--sidebar-ring: oklch(0.556 0 0);
114114
}
115115

116+
@layer utilities {
117+
.text-balance {
118+
text-wrap: balance;
119+
}
120+
121+
.animate-marquee {
122+
animation: marquee var(--duration, 15s) linear infinite;
123+
}
124+
125+
.pause-animation {
126+
animation-play-state: paused;
127+
}
128+
129+
@keyframes marquee {
130+
0% {
131+
transform: translateX(0);
132+
}
133+
100% {
134+
transform: translateX(-50%);
135+
}
136+
}
137+
}
138+
116139
@layer base {
117140
* {
118141
@apply border-border outline-ring/50;

app/page.tsx

Lines changed: 6 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { formatDate } from "@/lib/utils";
44

55
import { getPosts } from "./posts/page";
66
import { AsciiAnimation } from "@/components/ascii-animation";
7+
import { BlogPostCard } from "@/components/blog-post-card";
78

89
export default async function Home() {
910
const blogPosts = await getPosts();
@@ -31,7 +32,7 @@ export default async function Home() {
3132
</section> */}
3233

3334
{/* Featured Post */}
34-
<section className="min-h-[90vh] py-16 justify-end flex flex-col">
35+
<section className="min-h-[80vh] py-16 justify-end flex flex-col">
3536
<h2 className="mb-8 text-zinc-900 dark:text-zinc-50">Artículo Destacado</h2>
3637
<div className="grid grid-cols-1 lg:grid-cols-5 gap-8">
3738
<div className="lg:col-span-3 space-y-4">
@@ -43,7 +44,7 @@ export default async function Home() {
4344
<span className="text-zinc-300 dark:text-zinc-600"></span>
4445
<span className="text-sm text-zinc-500 dark:text-zinc-400">{featuredPost.frontmatter.tags[0]}</span>
4546
</div>
46-
<h3 className="text-zinc-900 dark:text-zinc-50 text-2xl font-bold">
47+
<h3 className="text-zinc-900 dark:text-zinc-50 text-4xl font-bold">
4748
<Link href={`/posts/${featuredPost.slug}`}>{featuredPost.frontmatter.title}</Link>
4849
</h3>
4950
</div>
@@ -66,7 +67,7 @@ export default async function Home() {
6667
</Link>
6768
</div>
6869
</div>
69-
<div className="lg:col-span-2 bg-zinc-100 dark:bg-zinc-900 rounded-lg p-6">
70+
{/* <div className="lg:col-span-2 bg-zinc-100 dark:bg-zinc-900 rounded-lg p-6">
7071
<h4 className="font-mono font-semibold mb-3 text-zinc-900 dark:text-zinc-50">Temas Relacionados</h4>
7172
<div className="flex flex-wrap gap-2">
7273
{featuredPost.frontmatter.tags.map((tag) => (
@@ -84,7 +85,7 @@ export default async function Home() {
8485
{featuredPost.frontmatter.description.split("\n").slice(0, 3).join(" ").substring(0, 300)}...
8586
</p>
8687
</div>
87-
</div>
88+
</div> */}
8889
</div>
8990
</section>
9091

@@ -93,43 +94,7 @@ export default async function Home() {
9394
<h2 className="mb-8 text-zinc-900 dark:text-zinc-50">Artículos Recientes</h2>
9495
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
9596
{recentPosts.map((post) => (
96-
<article
97-
key={post.frontmatter.title}
98-
className="border border-zinc-200 dark:border-zinc-800 rounded-lg overflow-hidden flex flex-col"
99-
>
100-
<div className="p-6 flex-1">
101-
<div className="flex items-center space-x-2 mb-3">
102-
<time className="text-sm text-zinc-500 dark:text-zinc-400">
103-
{formatDate(post.frontmatter.date)}
104-
</time>
105-
<span className="text-zinc-300 dark:text-zinc-600"></span>
106-
<span className="text-sm text-zinc-500 dark:text-zinc-400">{post.frontmatter.tags[0]}</span>
107-
</div>
108-
<h3 className="mb-2 text-zinc-900 dark:text-zinc-50">
109-
<Link href={`/posts/${post.slug}`} className="hover:underline">
110-
{post.frontmatter.title}
111-
</Link>
112-
</h3>
113-
<p className="mb-4 line-clamp-3 text-zinc-700 dark:text-zinc-300">{post.frontmatter.description}</p>
114-
</div>
115-
<div className="px-6 py-4 bg-zinc-50 dark:bg-zinc-900 border-t border-zinc-200 dark:border-zinc-800">
116-
<div className="flex flex-wrap gap-2">
117-
{post.frontmatter.tags.slice(0, 3).map((tag) => (
118-
<span
119-
key={tag}
120-
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-zinc-200 text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200"
121-
>
122-
{tag}
123-
</span>
124-
))}
125-
{post.frontmatter.tags.length > 3 && (
126-
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-zinc-200 text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200">
127-
+{post.frontmatter.tags.length - 3}
128-
</span>
129-
)}
130-
</div>
131-
</div>
132-
</article>
97+
<BlogPostCard key={post.slug} slug={post.slug} frontmatter={post.frontmatter} />
13398
))}
13499
</div>
135100
<div className="mt-12 text-center">

app/posts/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export default async function BlogPage() {
1515
return (
1616
<div className="min-h-screen bg-white dark:bg-zinc-950">
1717
<main className="container mx-auto px-4 py-12">
18-
<div className="max-w-4xl mx-auto">
18+
<div className="max-w-4xl mr-auto">
1919
<header className="mb-12">
2020
<h1 className="mb-4 text-zinc-900 dark:text-zinc-50">Artículos</h1>
2121
<p className="text-xl text-zinc-700 dark:text-zinc-300">

components/blog-post-card.tsx

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"use client";
2+
import { slugify } from "@/lib/utils";
3+
import Link from "next/link";
4+
import { useEffect, useRef, useState } from "react";
5+
6+
function formatDate(dateString: string): string {
7+
const date = new Date(dateString);
8+
9+
// Format: "1 de enero de 2023"
10+
return date.toLocaleDateString("es-ES", {
11+
day: "numeric",
12+
month: "long",
13+
year: "numeric",
14+
});
15+
}
16+
17+
interface PostFrontmatter {
18+
title: string;
19+
date: string;
20+
description: string;
21+
tags: string[];
22+
}
23+
24+
interface PostCardProps {
25+
slug: string;
26+
frontmatter: PostFrontmatter;
27+
}
28+
29+
export function BlogPostCard({ slug, frontmatter }: PostCardProps) {
30+
const tagsContainerRef = useRef<HTMLDivElement>(null);
31+
const tagsRef = useRef<HTMLDivElement>(null);
32+
const [shouldAnimate, setShouldAnimate] = useState(false);
33+
useEffect(() => {
34+
if (tagsContainerRef.current && tagsRef.current) {
35+
const containerWidth = tagsContainerRef.current.clientWidth;
36+
const tagsWidth = tagsRef.current.scrollWidth;
37+
38+
setShouldAnimate(tagsWidth > containerWidth);
39+
}
40+
}, [frontmatter.tags]);
41+
return (
42+
<article className="group h-full flex flex-col border border-zinc-200 dark:border-zinc-800 rounded-lg overflow-hidden bg-background/20 backdrop-blur-[2px] transition-all hover:shadow-md dark:hover:shadow-zinc-900/30">
43+
<div className="p-6 flex-1 flex flex-col">
44+
<div className="flex items-center space-x-2 mb-3 text-sm text-zinc-500 dark:text-zinc-400">
45+
<time>{formatDate(frontmatter.date)}</time>
46+
{/* <span className="text-zinc-300 dark:text-zinc-600">•</span> */}
47+
{/* <span>{frontmatter.tags[0]}</span> */}
48+
</div>
49+
50+
<h3 className="mb-3 text-xl font-semibold text-zinc-900 dark:text-zinc-50 line-clamp-3 group-hover:text-primary">
51+
<Link href={`/posts/${slug}`}>{frontmatter.title}</Link>
52+
</h3>
53+
54+
<p className="line-clamp-6 text-zinc-700 dark:text-zinc-300 flex-grow">{frontmatter.description}</p>
55+
</div>
56+
<div className="bg-zinc-50 dark:bg-zinc-900 border-t border-zinc-200 dark:border-zinc-800">
57+
<div ref={tagsContainerRef} className="h-8 flex items-center overflow-hidden relative">
58+
<div
59+
ref={tagsRef}
60+
className={shouldAnimate ? "flex gap-2 items-center animate-marquee" : "flex gap-2 items-center"}
61+
style={
62+
shouldAnimate
63+
? ({
64+
"--duration": `${frontmatter.tags.length * 3}s`,
65+
} as React.CSSProperties)
66+
: {}
67+
}
68+
onMouseEnter={
69+
shouldAnimate
70+
? (e) => {
71+
e.currentTarget.style.animationPlayState = "paused";
72+
}
73+
: undefined
74+
}
75+
onMouseLeave={
76+
shouldAnimate
77+
? (e) => {
78+
e.currentTarget.style.animationPlayState = "running";
79+
}
80+
: undefined
81+
}
82+
>
83+
{frontmatter.tags
84+
.map((tag) => slugify(tag.toLowerCase()))
85+
.map((tag) => (
86+
<span
87+
key={tag}
88+
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-zinc-200 text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200 whitespace-nowrap"
89+
>
90+
{tag}
91+
</span>
92+
))}
93+
94+
{/* Duplicate tags for seamless looping when animating */}
95+
{shouldAnimate &&
96+
frontmatter.tags
97+
.map((tag) => slugify(tag.toLowerCase()))
98+
.map((tag) => (
99+
<span
100+
key={`${tag}-duplicate`}
101+
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-zinc-200 text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200 whitespace-nowrap"
102+
>
103+
{tag}
104+
</span>
105+
))}
106+
</div>
107+
</div>
108+
</div>
109+
</article>
110+
);
111+
}

components/site-footer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { siteConfig } from "@/lib/site-config";
33

44
export function SiteFooter() {
55
return (
6-
<footer className="border-t border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-950 z-10">
6+
<footer className="border-t border-zinc-200 bg-background/50 backdrop-blur-[2px] dark:border-zinc-800 z-10">
77
<div className="container mx-auto px-4 py-8">
88
<div className="grid grid-cols-1 gap-8 md:grid-cols-3">
99
<div>

components/site-header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { siteConfig } from "@/lib/site-config";
44

55
export function SiteHeader() {
66
return (
7-
<header className="sticky top-0 z-40 w-full border-b border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-950">
7+
<header className="sticky top-0 z-40 w-full border-b border-zinc-200 bg-background/50 backdrop-blur-[2px] dark:border-zinc-800">
88
<div className="container mx-auto flex h-16 items-center justify-between px-4">
99
<div className="flex items-center gap-2">
1010
<Link href="/" className="flex items-center space-x-2">

lib/site-config.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
export const siteConfig = {
22
name: "cyberpunga",
3-
description:
4-
"explorando las intersecciones entre tecnología, sociedad y condición humana desde una perspectiva latinoamericana.",
3+
description: "si estás leyendo esto, significa que sigues con vida y nos alegra muchísimo.",
54
url: "https://cyberpun.ga", // Replace with your actual domain when you have one
65
ogImage: "https://cyberpun.ga/og.jpg", // For social media previews
76
links: {
@@ -13,16 +12,16 @@ export const siteConfig = {
1312
title: "Artículos",
1413
href: "/posts",
1514
},
16-
{
17-
title: "Acerca de",
18-
href: "/about",
19-
},
15+
// {
16+
// title: "Acerca de",
17+
// href: "/about",
18+
// },
2019
],
2120
footerNav: {
2221
resources: [
2322
{ title: "Inicio", href: "/" },
2423
{ title: "Artículos", href: "/posts" },
25-
{ title: "Acerca de", href: "/about" },
24+
// { title: "Acerca de", href: "/about" },
2625
],
2726
popularTags: [
2827
{ title: "tecnología", href: "/posts?tag=tecnología" },

lib/utils.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,22 @@ export function formatDate(dateString: string): string {
1515
year: "numeric",
1616
});
1717
}
18+
19+
export function slugify(str: string): string {
20+
str = str.replace(/^\s+|\s+$/g, ""); // trim
21+
str = str.toLowerCase();
22+
23+
// remove accents, swap ñ for n, etc
24+
var from = "àáäâèéëêìíïîòóöôùúüûñç·/_,:;";
25+
var to = "aaaaeeeeiiiioooouuuunc------";
26+
for (var i = 0, l = from.length; i < l; i++) {
27+
str = str.replace(new RegExp(from.charAt(i), "g"), to.charAt(i));
28+
}
29+
30+
str = str
31+
.replace(/[^a-z0-9 -]/g, "") // remove invalid chars
32+
.replace(/\s+/g, "-") // collapse whitespace and replace by -
33+
.replace(/-+/g, "-"); // collapse dashes
34+
35+
return str;
36+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"remark-gfm": "^4.0.1",
2929
"remark-mdx-frontmatter": "^5.1.0",
3030
"tailwind-merge": "^3.2.0",
31+
"tailwindcss-animate": "^1.0.7",
3132
"tw-animate-css": "^1.2.5"
3233
},
3334
"devDependencies": {

pnpm-lock.yaml

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)