Skip to content

Commit 211189c

Browse files
committed
feat: Add authors page to list all contributors
1 parent 41fdcbc commit 211189c

File tree

2 files changed

+202
-0
lines changed

2 files changed

+202
-0
lines changed

src/pages/News/AuthorsPage.tsx

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { useNavigate } from 'react-router-dom';
3+
import { motion } from 'framer-motion';
4+
import { User, Building, BookOpen } from 'lucide-react';
5+
6+
import Header from '@/sections/Header';
7+
import Footer from '@/sections/Footer';
8+
import { fetchAllAuthors, Author } from '@/utils/author-utils';
9+
import { getPostsByAuthor } from '@/utils/posts-utils';
10+
11+
type AuthorWithPostCount = Author & {
12+
postCount: number;
13+
};
14+
15+
const AuthorsPage: React.FC = () => {
16+
const navigate = useNavigate();
17+
const [authors, setAuthors] = useState<AuthorWithPostCount[]>([]);
18+
const [isLoading, setIsLoading] = useState(true);
19+
const [error, setError] = useState<string | null>(null);
20+
21+
useEffect(() => {
22+
const loadAllAuthors = async () => {
23+
setIsLoading(true);
24+
document.title = 'Our Authors - SugarLabs';
25+
try {
26+
const allAuthors = await fetchAllAuthors();
27+
28+
if (!allAuthors || allAuthors.length === 0) {
29+
setAuthors([]);
30+
return;
31+
}
32+
console.log(allAuthors);
33+
const authorsWithData = await Promise.all(
34+
allAuthors.map(async (author) => {
35+
try {
36+
const posts = await getPostsByAuthor(author.slug);
37+
return { ...author, postCount: posts.length };
38+
} catch (postError) {
39+
console.error(
40+
`Failed to get post count for ${author.name}`,
41+
postError,
42+
);
43+
return { ...author, postCount: 0 };
44+
}
45+
}),
46+
);
47+
48+
setAuthors(authorsWithData);
49+
} catch (err) {
50+
console.error('Error loading authors:', err);
51+
setError('Failed to load author information');
52+
} finally {
53+
setIsLoading(false);
54+
}
55+
};
56+
57+
loadAllAuthors();
58+
}, []);
59+
60+
const handleAuthorClick = (slug: string) => {
61+
navigate(`/authors/${slug}`);
62+
};
63+
64+
const renderContent = () => {
65+
if (isLoading) {
66+
return (
67+
<div className="container mx-auto px-4 py-16 flex justify-center items-center min-h-[60vh]">
68+
<div className="flex flex-col items-center">
69+
<div className="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-blue-600 mb-4"></div>
70+
<p className="text-gray-600">Loading authors...</p>
71+
</div>
72+
</div>
73+
);
74+
}
75+
76+
if (error) {
77+
return (
78+
<div className="container mx-auto px-4 py-16 text-center min-h-[60vh] flex flex-col justify-center">
79+
<h1 className="text-3xl font-bold mb-4 text-red-600">
80+
Something went wrong
81+
</h1>
82+
<p className="mb-8 text-gray-600">{error}</p>
83+
</div>
84+
);
85+
}
86+
87+
if (authors.length === 0) {
88+
return (
89+
<div className="container mx-auto px-4 py-16 text-center min-h-[60vh] flex flex-col justify-center">
90+
<h1 className="text-3xl font-bold mb-4 text-blue-600">No Authors</h1>
91+
<p className="mb-8 text-gray-600">
92+
There are no authors to display at this time.
93+
</p>
94+
</div>
95+
);
96+
}
97+
98+
return (
99+
<div className="space-y-8">
100+
{authors.map((author, index) => (
101+
<motion.div
102+
key={author.slug}
103+
onClick={() => handleAuthorClick(author.slug)}
104+
className="cursor-pointer group"
105+
initial={{ opacity: 0, y: 20 }}
106+
animate={{ opacity: 1, y: 0 }}
107+
transition={{ duration: 0.5, delay: index * 0.1 }}
108+
whileHover={{ scale: 1.02 }}
109+
>
110+
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-xl dark:shadow-2xl dark:shadow-black/20 p-4 sm:p-6 lg:p-8 transition-all duration-300 group-hover:shadow-blue-200 dark:group-hover:shadow-blue-500/30">
111+
<div className="flex flex-col sm:flex-row items-center sm:items-start gap-6 lg:gap-8">
112+
{/* Avatar */}
113+
<div className="flex-shrink-0">
114+
{author.avatar ? (
115+
<img
116+
src={author.avatar}
117+
alt={author.name}
118+
className="w-24 h-24 sm:w-28 sm:h-28 lg:w-32 lg:h-32 rounded-full object-cover border-4 border-blue-100 dark:border-blue-900"
119+
/>
120+
) : (
121+
<div className="w-24 h-24 sm:w-28 sm:h-28 lg:w-32 lg:h-32 bg-blue-100 dark:bg-gray-700/50 rounded-full flex items-center justify-center">
122+
<User className="w-12 h-12 sm:w-14 sm:h-14 lg:w-16 lg:h-16 text-blue-600 dark:text-blue-400" />
123+
</div>
124+
)}
125+
</div>
126+
127+
{/* Author Info */}
128+
<div className="flex-1 text-center sm:text-left">
129+
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold text-gray-900 dark:text-white mb-2 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
130+
{author.name}
131+
</h1>
132+
<div className="flex flex-col sm:flex-row sm:items-center gap-2 mb-3">
133+
<span className="text-lg lg:text-xl text-blue-600 dark:text-blue-400 font-medium">
134+
{author.title}
135+
</span>
136+
{author.organization && (
137+
<>
138+
<span className="hidden sm:inline text-gray-400 dark:text-gray-500">
139+
at
140+
</span>
141+
<div className="flex items-center justify-center sm:justify-start gap-1 text-gray-700 dark:text-gray-300">
142+
<Building className="w-4 h-4" />
143+
<span className="font-medium">
144+
{author.organization}
145+
</span>
146+
</div>
147+
</>
148+
)}
149+
</div>
150+
<p className="text-gray-600 dark:text-gray-300 text-base lg:text-lg mb-4 max-w-2xl line-clamp-2">
151+
{author.description}
152+
</p>
153+
154+
{/* Quick Stats */}
155+
<div className="flex flex-wrap justify-center sm:justify-start gap-4 text-sm text-gray-600 dark:text-gray-300">
156+
<div className="flex items-center gap-1 bg-blue-50 dark:bg-blue-900/30 px-3 py-1 rounded-full">
157+
<BookOpen className="w-4 h-4" />
158+
<span>
159+
{author.postCount}{' '}
160+
{author.postCount === 1 ? 'Article' : 'Articles'}
161+
</span>
162+
</div>
163+
{author.organization && (
164+
<div className="flex items-center gap-1 bg-gray-50 dark:bg-gray-700/50 px-3 py-1 rounded-full">
165+
<Building className="w-4 h-4" />
166+
<span>{author.organization}</span>
167+
</div>
168+
)}
169+
</div>
170+
</div>
171+
</div>
172+
</div>
173+
</motion.div>
174+
))}
175+
</div>
176+
);
177+
};
178+
179+
return (
180+
<>
181+
<Header />
182+
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-green-50 dark:from-gray-900 dark:via-gray-900 dark:to-gray-800">
183+
<div className="container mx-auto px-4 py-12 max-w-5xl">
184+
<motion.h1
185+
className="text-4xl sm:text-5xl font-bold text-center mb-10 text-gray-900 dark:text-white"
186+
initial={{ opacity: 0, y: -20 }}
187+
animate={{ opacity: 1, y: 0 }}
188+
transition={{ duration: 0.5 }}
189+
>
190+
Meet Our Authors
191+
</motion.h1>
192+
{renderContent()}
193+
</div>
194+
</div>
195+
<Footer />
196+
</>
197+
);
198+
};
199+
200+
export default AuthorsPage;

src/routes.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import FlatHubPage from '@/pages/TryNow/FlatHub';
2525
import Matrix from '@/pages/Matrix';
2626
import NotFoundPage from '@/pages/NotFoundPage';
2727
import Contributors from '@/pages/Contributors';
28+
import AuthorsPage from './pages/News/AuthorsPage';
2829

2930
const router = createBrowserRouter([
3031
...redirectRoutes,
@@ -37,6 +38,7 @@ const router = createBrowserRouter([
3738
{ path: '/news/:category', element: <NewsPage /> },
3839
{ path: '/news/:category/:slug', element: <NewsDetailPage /> },
3940
{ path: '/authors/:slug', element: <AuthorPage /> },
41+
{ path: '/authors', element: <AuthorsPage /> },
4042
{ path: '/tags/:tag', element: <TagPage /> },
4143
{ path: '/more', element: <MorePage /> },
4244
{ path: '/more/:slug', element: <MorePage /> },

0 commit comments

Comments
 (0)