Personal Website for @jaspermayone.com jaspermayone.com
at main 198 lines 7.7 kB view raw
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&apos;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}