Personal Website for @jaspermayone.com
jaspermayone.com
1"use client";
2
3import { useMemo } from "react";
4import { useGitHubStats } from "@/hooks/useGitHubStats";
5import { OpenSourceCard } from "@/components/OpenSourceCard";
6import {
7 MaintainedProject,
8 OpenSourceContribution,
9 HostedService,
10} from "@/lib/types";
11import ExternalLink from "@/components/ExternalLink";
12import { SiGithub } from "react-icons/si";
13import { ArrowUpRight } from "lucide-react";
14
15interface OpenSourceContentProps {
16 projects: MaintainedProject[];
17 contributions: OpenSourceContribution[];
18 services: HostedService[];
19}
20
21export function OpenSourceContent({
22 projects,
23 contributions,
24 services,
25}: OpenSourceContentProps) {
26 // Collect all GitHub repos for stats fetching
27 const allRepos = useMemo(() => {
28 const repos: string[] = [];
29
30 projects.forEach((p) => repos.push(p.repo));
31 contributions.forEach((c) => repos.push(c.repo));
32 services.filter((s) => s.repo).forEach((s) => repos.push(s.repo!));
33
34 return [...new Set(repos)]; // Deduplicate
35 }, [projects, contributions, services]);
36
37 const { stats, loading } = useGitHubStats(allRepos);
38
39 // Sort projects: featured first, then by stars
40 const sortedProjects = useMemo(() => {
41 return [...projects].sort((a, b) => {
42 if (a.featured && !b.featured) return -1;
43 if (!a.featured && b.featured) return 1;
44 const starsA = stats[a.repo]?.stars || 0;
45 const starsB = stats[b.repo]?.stars || 0;
46 return starsB - starsA;
47 });
48 }, [projects, stats]);
49
50 return (
51 <div className="mx-5 mt-4 mb-8">
52 {/* Page intro */}
53 <div className="mb-8">
54 <p className="text-gray-600 dark:text-white/70">
55 I believe in open source software. Here are the projects I maintain,
56 my contributions to others, and the services I host.
57 </p>
58 </div>
59
60 {/* Section 1: Maintained Projects */}
61 <section className="mb-12">
62 <h2
63 className="mb-4 text-xl font-bold text-gray-900 dark:text-white"
64 style={{ fontFamily: "var(--font-balgin)" }}
65 >
66 Maintained Projects
67 </h2>
68 <p className="mb-4 text-sm text-gray-600 dark:text-white/70">
69 Projects I actively maintain and develop.
70 </p>
71 <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
72 {sortedProjects.map((project) => (
73 <OpenSourceCard
74 key={project.repo}
75 name={project.name}
76 description={project.description}
77 repo={project.repo}
78 stats={stats[project.repo]}
79 loading={loading}
80 featured={project.featured}
81 />
82 ))}
83 </div>
84 </section>
85
86 {/* Section 2: Contributions */}
87 {contributions.length > 0 && (
88 <section className="mb-12">
89 <h2
90 className="mb-4 text-xl font-bold text-gray-900 dark:text-white"
91 style={{ fontFamily: "var(--font-balgin)" }}
92 >
93 Contributions to Other Projects
94 </h2>
95 <p className="mb-4 text-sm text-gray-600 dark:text-white/70">
96 Open source projects I've contributed to.
97 </p>
98 <div className="space-y-3">
99 {contributions.map((contribution, index) => (
100 <ExternalLink
101 key={index}
102 href={`https://github.com/${contribution.repo}`}
103 className="flex items-center justify-between rounded-lg border border-gray-200 bg-white p-4 transition-colors hover:border-gray-300 hover:bg-gray-50 dark:border-gray-700 dark:bg-slate-800 dark:hover:border-gray-600 dark:hover:bg-slate-700"
104 >
105 <div className="flex-1">
106 <div className="flex items-center gap-2">
107 <span className="font-medium text-gray-900 dark:text-white">
108 {contribution.project}
109 </span>
110 <span
111 className="rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-600 dark:bg-gray-700 dark:text-gray-300"
112 style={{ fontFamily: "var(--font-balgin)" }}
113 >
114 {contribution.contributionType}
115 </span>
116 </div>
117 {contribution.description && (
118 <p className="mt-1 text-sm text-gray-600 dark:text-white/70">
119 {contribution.description}
120 </p>
121 )}
122 </div>
123 <SiGithub className="h-4 w-4 text-gray-400" />
124 </ExternalLink>
125 ))}
126 </div>
127 </section>
128 )}
129
130 {/* Section 3: Hosted Services */}
131 {services.length > 0 && (
132 <section>
133 <h2
134 className="mb-4 text-xl font-bold text-gray-900 dark:text-white"
135 style={{ fontFamily: "var(--font-balgin)" }}
136 >
137 Hosted Services
138 </h2>
139 <p className="mb-4 text-sm text-gray-600 dark:text-white/70">
140 Live services and applications I host and maintain.
141 </p>
142 <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
143 {services.map((service, index) => (
144 <div
145 key={index}
146 className="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-slate-800"
147 >
148 <div className="mb-2 flex items-start justify-between">
149 <div className="flex items-center gap-2">
150 <h3 className="font-semibold text-gray-900 dark:text-white">
151 {service.name}
152 </h3>
153 {service.status === "alpha" && (
154 <span className="rounded-full bg-purple-100 px-2 py-0.5 text-xs text-purple-800 dark:bg-purple-900/30 dark:text-purple-300">
155 Alpha
156 </span>
157 )}
158 {service.status === "beta" && (
159 <span className="rounded-full bg-yellow-100 px-2 py-0.5 text-xs text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300">
160 Beta
161 </span>
162 )}
163 {service.status === "deprecated" && (
164 <span className="rounded-full bg-red-100 px-2 py-0.5 text-xs text-red-800 dark:bg-red-900/30 dark:text-red-300">
165 Deprecated
166 </span>
167 )}
168 </div>
169 <div className="flex gap-2">
170 <ExternalLink
171 href={service.url}
172 className="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
173 aria-label="Visit service"
174 >
175 <ArrowUpRight className="h-4 w-4" />
176 </ExternalLink>
177 {service.repo && (
178 <ExternalLink
179 href={`https://github.com/${service.repo}`}
180 className="text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
181 aria-label="View source"
182 >
183 <SiGithub className="h-4 w-4" />
184 </ExternalLink>
185 )}
186 </div>
187 </div>
188 <p className="text-sm text-gray-600 dark:text-white/70">
189 {service.description}
190 </p>
191 </div>
192 ))}
193 </div>
194 </section>
195 )}
196 </div>
197 );
198}