Skip to content

Commit a174d19

Browse files
authored
Develop (#4)
* fix: OG image file name * feat: implement blog, responsive navbar, and layout fixes (#3) - Add Blog system with index and slug pages - Add LatestPostsSection to homepage - Implement responsive mobile menu in Navbar - Add 'Notas' link to navigation - Fix horizontal scroll issues on mobile/tablet - Update formatting to 'VasLibre' in footer and hero - Optimize API data fetching
1 parent e660b0b commit a174d19

414 files changed

Lines changed: 14817 additions & 69 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export default function RootLayout({
8989

9090
return (
9191
<html lang="es" suppressHydrationWarning>
92-
<body className={`font-sans antialiased`}>
92+
<body className={`font-sans antialiased overflow-x-hidden`}>
9393
<Providers>
9494
<script
9595
type="application/ld+json"

app/notas/[slug]/page.tsx

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { PostBody } from "@/components/blog/post-body";
2+
import { Footer } from "@/components/footer";
3+
import { Navbar } from "@/components/navbar";
4+
import { getPostBySlug, getPostSlugs } from "@/lib/api";
5+
import { Container } from "@/components/container";
6+
import { markdownToHtml } from "@/lib/markdownToHtml";
7+
import { notFound } from "next/navigation";
8+
import { PostHeader } from "@/components/blog/post-header";
9+
import { Metadata } from "next";
10+
import { Post } from "@/interfaces/post";
11+
12+
type ParamsType = {
13+
params: Promise<{
14+
slug: string;
15+
}>;
16+
};
17+
18+
export async function generateStaticParams() {
19+
const slugs = getPostSlugs();
20+
21+
return slugs.map((filename: string) => ({
22+
slug: filename.replace(/\.md$/, ""),
23+
}));
24+
}
25+
26+
export default async function PostPage({
27+
params,
28+
}: {
29+
params: Promise<{ slug: string }>;
30+
}) {
31+
const { slug } = await params;
32+
const post = getPostBySlug(slug) as Post;
33+
34+
if (!post) {
35+
return notFound();
36+
}
37+
38+
const content = await markdownToHtml(post.content || "");
39+
40+
// JSON-LD for better SEO
41+
const jsonLd = {
42+
"@context": "https://schema.org",
43+
"@type": "BlogPosting",
44+
headline: post.title,
45+
image: post.coverImage ? [post.coverImage] : [],
46+
datePublished: post.date,
47+
dateModified: post.date,
48+
author: {
49+
"@type": "Person",
50+
name: post.author.name,
51+
image: post.author.picture,
52+
},
53+
description: post.excerpt,
54+
url: `https://vaslibre.org.ve/notas/${slug}/`,
55+
};
56+
57+
return (
58+
<main className="min-h-screen bg-background flex flex-col">
59+
<Navbar />
60+
61+
<script
62+
type="application/ld+json"
63+
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
64+
/>
65+
66+
{/* Premium Hero Section */}
67+
<section className="relative pt-32 pb-12 md:pt-40 md:pb-20 w-full backdrop-blur-3xl overflow-hidden">
68+
{/* Background grid pattern - Fixed Syntax */}
69+
<div className="absolute inset-0 bg-[linear-gradient(to_right,#1a1a2e_1px,transparent_1px),linear-gradient(to_bottom,#1a1a2e_1px,transparent_1px)] dark:bg-[linear-gradient(to_right,#4a4a5e_1px,transparent_1px),linear-gradient(to_bottom,#4a4a5e_1px,transparent_1px)] bg-size-[4rem_4rem] opacity-50 [mask-image:linear-gradient(to_bottom,black_40%,transparent_100%)] -z-10" />
70+
71+
{/* Glow effects */}
72+
<div className="absolute top-0 left-1/4 w-96 h-96 bg-primary/40 rounded-full blur-3xl animate-pulse -z-10" />
73+
<div
74+
className="absolute bottom-0 right-1/4 w-96 h-96 bg-accent/40 rounded-full blur-3xl animate-pulse -z-10"
75+
style={{ animationDelay: "1s" }}
76+
/>
77+
78+
<Container>
79+
<div className="max-w-4xl mx-auto text-center">
80+
<PostHeader
81+
title={post.title}
82+
coverImage={post.coverImage}
83+
date={post.date}
84+
author={post.author}
85+
/>
86+
</div>
87+
</Container>
88+
</section>
89+
90+
<div className="flex-1 pb-12 md:pb-16 pt-0">
91+
<Container>
92+
<PostBody content={content} />
93+
</Container>
94+
</div>
95+
96+
<Footer />
97+
</main>
98+
);
99+
}
100+
101+
export async function generateMetadata(props: ParamsType): Promise<Metadata> {
102+
const { slug } = await props.params;
103+
const post = getPostBySlug(slug) as Post;
104+
105+
console.log(post.excerpt);
106+
if (!post) {
107+
return notFound();
108+
}
109+
110+
const title = `${post.title} - VaSLibre`;
111+
const url = `https://vaslibre.org.ve/notas/${slug}/`;
112+
113+
return {
114+
title,
115+
description: post.excerpt,
116+
openGraph: {
117+
title,
118+
description: post.excerpt,
119+
images: [post.ogImage.url],
120+
url,
121+
},
122+
123+
alternates: {
124+
canonical: url,
125+
},
126+
};
127+
}

app/notas/page.tsx

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { PostPagination } from "@/components/blog/post-pagination";
2+
import { Footer } from "@/components/footer";
3+
import { Navbar } from "@/components/navbar";
4+
import { getAllPosts } from "@/lib/api";
5+
import { Suspense } from "react";
6+
7+
export default function Home() {
8+
// Get all posts statically at build time
9+
const allPosts = getAllPosts([
10+
"title",
11+
"date",
12+
"slug",
13+
"author",
14+
"coverImage",
15+
"excerpt",
16+
]);
17+
18+
return (
19+
<main className="min-h-screen bg-background flex flex-col">
20+
<Navbar />
21+
22+
{/* Hero Section - Full Width */}
23+
<section className="relative pt-32 pb-12 md:pt-40 md:pb-20 w-full backdrop-blur-3xl overflow-hidden">
24+
{/* Background grid pattern - Fixed Syntax */}
25+
<div className="absolute inset-0 bg-[linear-gradient(to_right,#1a1a2e_1px,transparent_1px),linear-gradient(to_bottom,#1a1a2e_1px,transparent_1px)] dark:bg-[linear-gradient(to_right,#4a4a5e_1px,transparent_1px),linear-gradient(to_bottom,#4a4a5e_1px,transparent_1px)] bg-size-[4rem_4rem] opacity-50 [mask-image:linear-gradient(to_bottom,black_40%,transparent_100%)] -z-10" />
26+
27+
{/* Glow effects */}
28+
<div className="absolute top-0 left-1/4 w-96 h-96 bg-primary/40 rounded-full blur-3xl animate-pulse -z-10" />
29+
<div
30+
className="absolute bottom-0 right-1/4 w-96 h-96 bg-accent/40 rounded-full blur-3xl animate-pulse -z-10"
31+
style={{ animationDelay: "1s" }}
32+
/>
33+
34+
<div className="container mx-auto px-4 relative z-10 text-center space-y-6 max-w-4xl">
35+
<div className="inline-flex items-center gap-2 bg-secondary/50 border border-border rounded-full px-4 py-1.5 text-sm backdrop-blur-sm animate-in fade-in zoom-in-50 duration-500 delay-150">
36+
<span className="relative flex h-2 w-2">
37+
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
38+
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary"></span>
39+
</span>
40+
<span className="text-muted-foreground font-medium">
41+
Blog & Recursos
42+
</span>
43+
</div>
44+
45+
<h1 className="text-4xl md:text-6xl lg:text-7xl font-bold tracking-tight animate-in fade-in slide-in-from-bottom-4 duration-500 delay-200">
46+
Nuestras{" "}
47+
<span className="bg-linear-to-r from-primary to-accent bg-clip-text text-transparent">
48+
Notas
49+
</span>
50+
</h1>
51+
52+
<p className="text-muted-foreground text-lg md:text-xl max-w-2xl mx-auto leading-relaxed animate-in fade-in slide-in-from-bottom-4 duration-500 delay-300">
53+
Explora el archivo de conocimientos, noticias y
54+
tutoriales de la comunidad VaSLibre.
55+
</p>
56+
</div>
57+
</section>
58+
59+
<div className="flex-1 container mx-auto px-4 pb-12 md:pb-16">
60+
<Suspense
61+
fallback={
62+
<div className="text-center py-20">
63+
Cargando notas...
64+
</div>
65+
}
66+
>
67+
<PostPagination allPosts={allPosts as any} />
68+
</Suspense>
69+
</div>
70+
<Footer />
71+
</main>
72+
);
73+
}

app/page.tsx

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
1-
import { HeroSection } from "@/components/hero-section"
2-
import { AboutSection } from "@/components/about-section"
3-
import { MembersSection } from "@/components/members-section"
4-
import { GallerySection } from "@/components/gallery-section"
5-
import { EventsSection } from "@/components/events-section"
6-
import { ContactSection } from "@/components/contact-section"
7-
import { Footer } from "@/components/footer"
8-
import { Navbar } from "@/components/navbar"
1+
import { HeroSection } from "@/components/hero-section";
2+
import { AboutSection } from "@/components/about-section";
3+
import { MembersSection } from "@/components/members-section";
4+
import { GallerySection } from "@/components/gallery-section";
5+
import { LatestPostsSection } from "@/components/latest-posts-section";
6+
import { EventsSection } from "@/components/events-section";
7+
import { ContactSection } from "@/components/contact-section";
8+
import { Footer } from "@/components/footer";
9+
import { Navbar } from "@/components/navbar";
910

1011
export default function Home() {
11-
return (
12-
<main className="min-h-screen">
13-
<Navbar />
14-
<HeroSection />
15-
<AboutSection />
16-
<MembersSection />
17-
<GallerySection />
18-
<EventsSection />
19-
<ContactSection />
20-
<Footer />
21-
</main>
22-
)
12+
return (
13+
<main className="min-h-screen">
14+
<Navbar />
15+
<HeroSection />
16+
<AboutSection />
17+
<MembersSection />
18+
<GallerySection />
19+
<LatestPostsSection />
20+
<EventsSection />
21+
<ContactSection />
22+
<Footer />
23+
</main>
24+
);
2325
}

app/sitemap.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,34 @@
11
// app/sitemap.ts
22
import type { MetadataRoute } from 'next';
33

4+
import { getAllPosts } from "@/lib/api";
5+
46
export const dynamic = "force-static";
57

68
export default function sitemap(): MetadataRoute.Sitemap {
9+
const posts = getAllPosts(["slug", "date"]);
10+
const baseUrl = "https://vaslibre.org.ve";
11+
12+
const postsUrls = posts.map((post) => ({
13+
url: `${baseUrl}/notas/${post.slug}`,
14+
lastModified: new Date(post.date!).toISOString(),
15+
changeFrequency: "monthly" as const,
16+
priority: 0.7,
17+
}));
18+
719
return [
820
{
9-
url: 'https://vaslibre.org.ve',
21+
url: baseUrl,
1022
lastModified: new Date(),
11-
changeFrequency: 'yearly',
23+
changeFrequency: "yearly",
1224
priority: 1,
1325
},
26+
{
27+
url: `${baseUrl}/notas`,
28+
lastModified: new Date(),
29+
changeFrequency: "weekly",
30+
priority: 0.8,
31+
},
32+
...postsUrls,
1433
];
1534
}

components/blog/avatar.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
type Props = {
2+
name: string;
3+
picture: string;
4+
};
5+
6+
export function Avatar({ name, picture }: Props) {
7+
return (
8+
<div className="flex items-center">
9+
<img
10+
src={picture}
11+
className="w-12 h-12 rounded-full mr-4"
12+
alt={name}
13+
/>
14+
<div className="text-xl font-bold">{name}</div>
15+
</div>
16+
);
17+
};

components/blog/blog-card.tsx

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import Link from "next/link";
2+
import { type Post } from "@/interfaces/post";
3+
import { Card, CardContent, CardFooter } from "@/components/ui/card";
4+
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
5+
import { Button } from "@/components/ui/button";
6+
import { DateFormatter } from "./date-formatter";
7+
import { cn } from "@/lib/utils";
8+
import { ArrowRight, Calendar } from "lucide-react";
9+
10+
type Props = {
11+
post: Post;
12+
className?: string;
13+
};
14+
15+
export function BlogCard({ post, className }: Props) {
16+
return (
17+
<Card
18+
className={cn(
19+
"group flex flex-col h-full overflow-hidden border-border/50 bg-card transition-all hover:shadow-lg hover:border-border",
20+
className
21+
)}
22+
>
23+
{/* Image Container - Switched from CardHeader to plain div to guarantee 0 padding */}
24+
<div className="w-full relative">
25+
<Link
26+
href={`/notas/${post.slug}`}
27+
className="block overflow-hidden active:scale-95 transition-transform duration-200"
28+
>
29+
<div className="relative aspect-video w-full overflow-hidden bg-muted">
30+
{/* bg-muted acts as placeholder if transparency exists */}
31+
<img
32+
src={post.coverImage}
33+
alt={`Cover for ${post.title}`}
34+
className="absolute inset-0 w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
35+
/>
36+
</div>
37+
</Link>
38+
</div>
39+
40+
<CardContent className="flex-1 p-6">
41+
<div className="flex items-center gap-2 text-sm text-muted-foreground mb-3">
42+
<Calendar className="w-4 h-4" />
43+
<DateFormatter dateString={post.date} />
44+
</div>
45+
<Link
46+
href={`/notas/${post.slug}`}
47+
className="block group-hover:text-primary transition-colors"
48+
>
49+
<h3 className="text-xl font-bold leading-tight mb-2 line-clamp-2">
50+
{post.title}
51+
</h3>
52+
</Link>
53+
<p className="text-muted-foreground text-sm line-clamp-3 leading-relaxed">
54+
{post.excerpt}
55+
</p>
56+
</CardContent>
57+
<CardFooter className="p-6 pt-0 flex items-center justify-between mt-auto">
58+
<div className="flex items-center gap-2">
59+
<Avatar className="w-8 h-8 border">
60+
<AvatarImage
61+
src={post.author.picture}
62+
alt={post.author.name}
63+
/>
64+
<AvatarFallback>{post.author.name[0]}</AvatarFallback>
65+
</Avatar>
66+
<span className="text-sm font-medium opacity-80">
67+
{post.author.name}
68+
</span>
69+
</div>
70+
71+
<Button
72+
variant="ghost"
73+
size="sm"
74+
className="gap-1 px-4 hover:bg-primary hover:text-white transition-all duration-300 group/btn"
75+
asChild
76+
>
77+
<Link href={`/notas/${post.slug}`}>
78+
Leer más{" "}
79+
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover/btn:translate-x-1" />
80+
</Link>
81+
</Button>
82+
</CardFooter>
83+
</Card>
84+
);
85+
}

0 commit comments

Comments
 (0)