Skip to content

Commit 1affcca

Browse files
authored
feat: Add authors page to list all contributors (#517)
1 parent 7847431 commit 1affcca

File tree

3 files changed

+238
-0
lines changed

3 files changed

+238
-0
lines changed

src/constants/Header.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const navigationData = {
55
items: [
66
{ label: 'About Us', path: '/about-us' },
77
{ label: 'Leadership', path: '/leadership' },
8+
{ label: 'Authors', path: '/authors' },
89
{ label: 'Contact Us', path: '/contact-us' },
910
{ label: 'FAQs', path: '/faqs' },
1011
],

src/pages/News/AuthorsPage.tsx

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
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+
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="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
100+
{authors.map((author, index) => (
101+
<motion.div
102+
key={author.slug}
103+
onClick={() => handleAuthorClick(author.slug)}
104+
className="bg-white dark:bg-gray-800 rounded-2xl shadow-xl dark:shadow-2xl dark:shadow-black/20
105+
transition-shadow duration-300 group-hover:shadow-blue-200 dark:group-hover:shadow-blue-500/30
106+
cursor-pointer group flex flex-col"
107+
initial={{ opacity: 0, y: 20 }}
108+
animate={{ opacity: 1, y: 0 }}
109+
transition={{
110+
duration: 0.5,
111+
delay: index * 0.1,
112+
scale: {
113+
type: 'spring',
114+
stiffness: 300,
115+
damping: 15,
116+
},
117+
}}
118+
whileHover={{ scale: 1.05 }}
119+
>
120+
<div className="flex flex-col items-center p-6 text-center flex-1">
121+
{/* Avatar */}
122+
<div className="flex-shrink-0 mb-4">
123+
{author.avatar ? (
124+
<img
125+
src={author.avatar}
126+
alt={author.name}
127+
className="w-24 h-24 rounded-full object-cover border-4 border-blue-100 dark:border-blue-900"
128+
/>
129+
) : (
130+
<div className="w-24 h-24 bg-blue-100 dark:bg-gray-700/50 rounded-full flex items-center justify-center">
131+
<User className="w-12 h-12 text-blue-600 dark:text-blue-400" />
132+
</div>
133+
)}
134+
</div>
135+
136+
{/* Author Info*/}
137+
<div className="flex flex-col flex-1">
138+
<h1 className="text-xl font-bold text-gray-900 dark:text-white mb-1 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
139+
{author.name}
140+
</h1>
141+
142+
{/* Title and Org */}
143+
<div className="flex flex-col items-center gap-1 mb-3">
144+
<span className="text-md text-blue-600 dark:text-blue-400 font-medium">
145+
{author.title}
146+
</span>
147+
{author.organization && (
148+
<div className="flex items-center justify-center gap-1 text-sm text-gray-700 dark:text-gray-300">
149+
<Building className="w-4 h-4" />
150+
<span className="font-medium">{author.organization}</span>
151+
</div>
152+
)}
153+
</div>
154+
155+
{/* Description */}
156+
<p className="text-gray-600 dark:text-gray-300 text-sm mb-4 max-w-xl line-clamp-3">
157+
{author.description}
158+
</p>
159+
160+
{/* Quick Stats */}
161+
<div className="flex flex-wrap justify-center gap-2 text-xs text-gray-600 dark:text-gray-300 mt-auto">
162+
<div className="flex items-center gap-1 bg-blue-50 dark:bg-blue-900/30 px-3 py-1 rounded-full">
163+
<BookOpen className="w-4 h-4" />
164+
<span>
165+
{author.postCount}{' '}
166+
{author.postCount === 1 ? 'Article' : 'Articles'}
167+
</span>
168+
</div>
169+
{author.organization && (
170+
<div className="flex items-center gap-1 bg-gray-50 dark:bg-gray-700/50 px-3 py-1 rounded-full">
171+
<Building className="w-4 h-4" />
172+
<span>{author.organization}</span>
173+
</div>
174+
)}
175+
</div>
176+
</div>
177+
</div>
178+
</motion.div>
179+
))}
180+
</div>
181+
);
182+
};
183+
184+
return (
185+
<div className="bg-white dark:bg-gray-900">
186+
<Header />
187+
<section className="py-24 px-4 bg-white dark:bg-gray-900">
188+
<div className="container mx-auto max-w-6xl">
189+
<div className="text-center mb-16">
190+
<motion.h2
191+
className="text-4xl md:text-5xl font-bold text-gray-900 dark:text-white mb-4"
192+
initial={{ opacity: 0, y: -20 }}
193+
animate={{ opacity: 1, y: 0 }}
194+
transition={{ duration: 0.5 }}
195+
>
196+
<motion.span
197+
className="text-red-500 font-Pacifico"
198+
initial={{ opacity: 0, y: -20 }}
199+
animate={{ opacity: 1, y: 0 }}
200+
transition={{ duration: 0.5, delay: 0.1 }}
201+
>
202+
Meet{' '}
203+
</motion.span>
204+
Our Authors
205+
</motion.h2>
206+
207+
<div className="flex justify-center">
208+
<motion.div
209+
className="h-1 bg-red-500 mb-8 w-24"
210+
initial={{ scaleX: 0 }}
211+
animate={{ scaleX: 1 }}
212+
transition={{ duration: 0.5, delay: 0.2 }}
213+
></motion.div>
214+
</div>
215+
216+
<motion.p
217+
className="text-lg text-gray-600 dark:text-gray-300 max-w-3xl mx-auto"
218+
initial={{ opacity: 0, y: 20 }}
219+
animate={{ opacity: 1, y: 0 }}
220+
transition={{ duration: 0.5, delay: 0.3 }}
221+
>
222+
Meet the talented authors, mentors, and contributors who share
223+
their insights and drive the Sugar Labs mission forward. Explore
224+
their work and connect with the community.
225+
</motion.p>
226+
</div>
227+
{renderContent()}
228+
</div>
229+
</section>
230+
<Footer />
231+
</div>
232+
);
233+
};
234+
235+
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)