Skip to content

Commit 4576c64

Browse files
add blog section
1 parent 9f6254b commit 4576c64

File tree

8 files changed

+2044
-54
lines changed

8 files changed

+2044
-54
lines changed

package-lock.json

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

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,22 @@
77
"@radix-ui/react-dialog": "^1.1.2",
88
"@radix-ui/react-icons": "^1.3.0",
99
"@radix-ui/react-slot": "^1.0.2",
10+
"@tailwindcss/typography": "^0.5.16",
11+
"@tanstack/react-query": "^5.80.6",
12+
"@tanstack/react-query-devtools": "^5.80.6",
1013
"class-variance-authority": "^0.7.0",
1114
"clsx": "^2.1.0",
1215
"d3": "^7.9.0",
1316
"jest-environment-jsdom": "^29.6.2",
1417
"lucide-react": "^0.510.0",
1518
"react": "^18.3.1",
1619
"react-dom": "^18.3.1",
20+
"react-markdown": "^10.1.0",
1721
"react-modal": "^3.16.1",
1822
"react-router-dom": "^6.26.2",
1923
"react-toggle": "^4.1.3",
2024
"recharts": "^2.7.3",
25+
"remark-gfm": "^4.0.1",
2126
"tailwind-merge": "^2.2.0",
2227
"tailwindcss-animate": "^1.0.7",
2328
"typescript": "^5.6.3",

src/App.tsx

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,38 @@
11
import React from "react";
22
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
3+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4+
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
35
import Navbar from "./components/Navbar";
46
import Home from "./components/Hero";
57
import Footer from "./components/Footer";
68
import Blog from "./components/Blog";
79
import Dog from "./components/Dog";
810

11+
// Create a client
12+
const queryClient = new QueryClient({
13+
defaultOptions: {
14+
queries: {
15+
staleTime: 5 * 60 * 1000, // 5 minutes
16+
gcTime: 10 * 60 * 1000, // 10 minutes (formerly cacheTime)
17+
},
18+
},
19+
});
20+
921
function App() {
1022
return (
11-
<Router>
12-
<Navbar />
13-
<Routes>
14-
<Route path="/" element={<Home />} />
15-
<Route path="/blog" element={<Blog />} />
16-
<Route path="/dog" element={<Dog />} />
17-
</Routes>
18-
<Footer />
19-
</Router>
23+
<QueryClientProvider client={queryClient}>
24+
<Router>
25+
<Navbar />
26+
<Routes>
27+
<Route path="/" element={<Home />} />
28+
<Route path="/blog" element={<Blog />} />
29+
<Route path="/blog/:slug" element={<Blog />} />
30+
<Route path="/dog" element={<Dog />} />
31+
</Routes>
32+
<Footer />
33+
</Router>
34+
<ReactQueryDevtools initialIsOpen={false} />
35+
</QueryClientProvider>
2036
);
2137
}
2238

src/components/Blog.tsx

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,31 @@
1-
import React from "react";
1+
import { useState, useEffect } from "react";
2+
import { useParams, useNavigate } from "react-router-dom";
3+
import BlogList from "./BlogList";
4+
import BlogPost from "./BlogPost";
25

36
const Blog = () => {
4-
return (
5-
<div className="m-auto w-screen h-screen text-center items-center flex flex-col">
6-
<div className="relative flex flex-col items-center my-4 p-4 rounded-xl bg-gray-200 shadow-md opacity-80">
7-
🚧 Beep boop under construction 🚧
8-
</div>
9-
</div>
10-
);
7+
const { slug } = useParams<{ slug: string }>();
8+
const navigate = useNavigate();
9+
const [selectedPostSlug, setSelectedPostSlug] = useState<string | null>(null);
10+
11+
// Sync URL params with local state
12+
useEffect(() => {
13+
setSelectedPostSlug(slug || null);
14+
}, [slug]);
15+
16+
const handlePostSelect = (postSlug: string) => {
17+
navigate(`/blog/${postSlug}`);
18+
};
19+
20+
const handleBackToList = () => {
21+
navigate("/blog");
22+
};
23+
24+
if (selectedPostSlug) {
25+
return <BlogPost slug={selectedPostSlug} onBack={handleBackToList} />;
26+
}
27+
28+
return <BlogList onPostSelect={handlePostSelect} />;
1129
};
1230

1331
export default Blog;

src/components/BlogList.tsx

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import React from "react";
2+
import { useNavigate } from "react-router-dom";
3+
import { useBlogPosts, usePrefetchBlogPost } from "../hooks/useBlog";
4+
import type { ModelsBlogPostMeta } from "../lib/blog-api-client/src";
5+
6+
interface BlogListProps {
7+
onPostSelect: (slug: string) => void;
8+
}
9+
10+
const BlogList: React.FC<BlogListProps> = ({ onPostSelect }) => {
11+
const navigate = useNavigate();
12+
const prefetchBlogPost = usePrefetchBlogPost();
13+
const {
14+
data: posts = [],
15+
isLoading: loading,
16+
error,
17+
refetch,
18+
} = useBlogPosts();
19+
20+
const formatDate = (dateString?: string) => {
21+
if (!dateString) return "";
22+
try {
23+
return new Date(dateString).toLocaleDateString("en-US", {
24+
year: "numeric",
25+
month: "long",
26+
day: "numeric",
27+
});
28+
} catch {
29+
return dateString;
30+
}
31+
};
32+
33+
return (
34+
<div className="min-h-screen py-8">
35+
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
36+
<div className="text-center mb-12">
37+
<h1 className="text-xl text-gray-600">
38+
Thoughts, insights, and updates from my journey
39+
</h1>
40+
</div>
41+
42+
{loading ? (
43+
<div className="text-center py-12">
44+
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
45+
<p className="mt-4 text-gray-600">Loading posts...</p>
46+
</div>
47+
) : error ? (
48+
<div className="text-center py-12">
49+
<div className="bg-red-50 border border-red-200 rounded-lg p-6 max-w-md mx-auto">
50+
<div className="flex items-center justify-center w-12 h-12 mx-auto bg-red-100 rounded-full mb-4">
51+
<svg
52+
className="w-6 h-6 text-red-600"
53+
fill="none"
54+
stroke="currentColor"
55+
viewBox="0 0 24 24"
56+
>
57+
<path
58+
strokeLinecap="round"
59+
strokeLinejoin="round"
60+
strokeWidth={2}
61+
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
62+
/>
63+
</svg>
64+
</div>
65+
<h3 className="text-lg font-medium text-red-800 mb-2">
66+
Error Loading Posts
67+
</h3>
68+
<p className="text-red-700">
69+
{error?.message || "Failed to load posts"}
70+
</p>
71+
<button
72+
onClick={() => refetch()}
73+
className="mt-4 px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
74+
>
75+
Try Again
76+
</button>
77+
</div>
78+
</div>
79+
) : posts.length === 0 ? (
80+
<div className="text-center py-12">
81+
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6 max-w-md mx-auto">
82+
<div className="flex items-center justify-center w-12 h-12 mx-auto bg-blue-100 rounded-full mb-4">
83+
<svg
84+
className="w-6 h-6 text-blue-600"
85+
fill="none"
86+
stroke="currentColor"
87+
viewBox="0 0 24 24"
88+
>
89+
<path
90+
strokeLinecap="round"
91+
strokeLinejoin="round"
92+
strokeWidth={2}
93+
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.746 0 3.332.477 4.5 1.253v13C19.832 18.477 18.246 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
94+
/>
95+
</svg>
96+
</div>
97+
<h3 className="text-lg font-medium text-blue-800 mb-2">
98+
No Posts Yet
99+
</h3>
100+
<p className="text-blue-700">Check back soon for new content!</p>
101+
</div>
102+
</div>
103+
) : (
104+
<div className="max-w-3xl mx-auto space-y-6">
105+
{posts.map((post, index) => (
106+
<article
107+
key={post.slug || index}
108+
className="bg-post rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300 cursor-pointer"
109+
onClick={() => post.slug && navigate(`/blog/${post.slug}`)}
110+
onMouseEnter={() => post.slug && prefetchBlogPost(post.slug)}
111+
>
112+
<div className="p-6">
113+
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
114+
<div className="flex-1">
115+
<h2 className="text-xl font-semibold text-gray-900 mb-2">
116+
{post.title || "Untitled Post"}
117+
</h2>
118+
119+
{post.excerpt && (
120+
<p className="text-gray-600 mb-3 line-clamp-2">
121+
{post.excerpt}
122+
</p>
123+
)}
124+
</div>
125+
126+
<div className="flex flex-col sm:items-end gap-2 sm:min-w-0 sm:flex-shrink-0">
127+
<time
128+
dateTime={post.publishDate}
129+
className="text-sm text-gray-500"
130+
>
131+
{formatDate(post.publishDate)}
132+
</time>
133+
134+
{post.tags && post.tags.length > 0 && (
135+
<div className="flex flex-wrap gap-1 sm:justify-end">
136+
{post.tags.slice(0, 3).map((tag, tagIndex) => (
137+
<span
138+
key={tagIndex}
139+
className="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full"
140+
>
141+
{tag}
142+
</span>
143+
))}
144+
{post.tags.length > 3 && (
145+
<span className="text-xs text-gray-500">
146+
+{post.tags.length - 3} more
147+
</span>
148+
)}
149+
</div>
150+
)}
151+
</div>
152+
</div>
153+
154+
<div className="mt-4 pt-4 border-t border-gray-100">
155+
<div className="flex items-center text-blue-600 hover:text-blue-800 text-sm font-medium">
156+
Read more
157+
<svg
158+
className="w-4 h-4 ml-1"
159+
fill="none"
160+
stroke="currentColor"
161+
viewBox="0 0 24 24"
162+
>
163+
<path
164+
strokeLinecap="round"
165+
strokeLinejoin="round"
166+
strokeWidth={2}
167+
d="M9 5l7 7-7 7"
168+
/>
169+
</svg>
170+
</div>
171+
</div>
172+
</div>
173+
</article>
174+
))}
175+
</div>
176+
)}
177+
</div>
178+
</div>
179+
);
180+
};
181+
182+
export default BlogList;

0 commit comments

Comments
 (0)