|
1 | 1 | "use client"; |
2 | 2 |
|
3 | 3 | import { useState, useEffect, useRef } from "react"; |
| 4 | +import type { ReactNode } from "react"; |
4 | 5 | import Image from "next/image"; |
5 | | -import { MapPin, Mail, Github } from "lucide-react"; |
| 6 | +import { MapPin, Mail, Github, Linkedin, Globe, Link2 } from "lucide-react"; |
6 | 7 | import { cn } from "@/lib/utils"; |
7 | 8 | import authors from "@/data/authors"; |
| 9 | +import type { AuthorLink } from "@/data/authors"; |
8 | 10 | import { getAuthorImageSrc } from "@/lib/utils"; |
| 11 | +import { MastodonIcon } from "@/components/icons"; |
| 12 | + |
| 13 | +function linkIcon(kind: AuthorLink["kind"], size: number): ReactNode { |
| 14 | + switch (kind) { |
| 15 | + case "linkedin": |
| 16 | + return <Linkedin size={size} className="text-muted-foreground" />; |
| 17 | + case "website": |
| 18 | + return <Globe size={size} className="text-muted-foreground" />; |
| 19 | + case "mastodon": |
| 20 | + return ( |
| 21 | + <MastodonIcon |
| 22 | + className="text-muted-foreground" |
| 23 | + style={{ width: size, height: size }} |
| 24 | + /> |
| 25 | + ); |
| 26 | + default: |
| 27 | + return <Link2 size={size} className="text-muted-foreground" />; |
| 28 | + } |
| 29 | +} |
9 | 30 |
|
10 | 31 | interface Props { |
11 | 32 | authorKey: string; |
@@ -43,6 +64,48 @@ export default function AuthorCard({ authorKey, className }: Props) { |
43 | 64 |
|
44 | 65 | const imgSrc = getAuthorImageSrc(author.image); |
45 | 66 |
|
| 67 | + const extraLinks = (author.links ?? []).filter( |
| 68 | + (l) => typeof l?.href === "string" && l.href.trim() !== "" |
| 69 | + ); |
| 70 | + |
| 71 | + const linkItems: { |
| 72 | + label: string; |
| 73 | + href: string; |
| 74 | + iconSm: ReactNode; |
| 75 | + iconMd: ReactNode; |
| 76 | + external: boolean; |
| 77 | + }[] = [ |
| 78 | + ...(author.email |
| 79 | + ? [ |
| 80 | + { |
| 81 | + label: "Email", |
| 82 | + href: author.email, |
| 83 | + iconSm: <Mail size={14} className="text-muted-foreground" />, |
| 84 | + iconMd: <Mail size={16} />, |
| 85 | + external: false, |
| 86 | + }, |
| 87 | + ] |
| 88 | + : []), |
| 89 | + ...(author.github |
| 90 | + ? [ |
| 91 | + { |
| 92 | + label: "GitHub", |
| 93 | + href: author.github, |
| 94 | + iconSm: <Github size={14} className="text-muted-foreground" />, |
| 95 | + iconMd: <Github size={16} />, |
| 96 | + external: true, |
| 97 | + }, |
| 98 | + ] |
| 99 | + : []), |
| 100 | + ...extraLinks.map((l) => ({ |
| 101 | + label: l.label, |
| 102 | + href: l.href, |
| 103 | + iconSm: linkIcon(l.kind, 14), |
| 104 | + iconMd: linkIcon(l.kind, 16), |
| 105 | + external: true, |
| 106 | + })), |
| 107 | + ]; |
| 108 | + |
46 | 109 | return ( |
47 | 110 | <> |
48 | 111 | <div className="lg:hidden flex items-center gap-3 px-4 py-4"> |
@@ -83,27 +146,18 @@ export default function AuthorCard({ authorKey, className }: Props) { |
83 | 146 | </div> |
84 | 147 | )} |
85 | 148 |
|
86 | | - {author.email && ( |
87 | | - <a |
88 | | - href={author.email} |
89 | | - className="flex items-center gap-2 px-3 py-2 text-muted-foreground hover:text-foreground hover:bg-accent" |
90 | | - > |
91 | | - <Mail size={14} className="text-muted-foreground" /> |
92 | | - <span className="text-sm">Email</span> |
93 | | - </a> |
94 | | - )} |
95 | | - |
96 | | - {author.github && ( |
| 149 | + {linkItems.map((item) => ( |
97 | 150 | <a |
98 | | - href={author.github} |
99 | | - target="_blank" |
100 | | - rel="noopener noreferrer" |
| 151 | + key={`${item.label}-${item.href}`} |
| 152 | + href={item.href} |
| 153 | + target={item.external ? "_blank" : undefined} |
| 154 | + rel={item.external ? "noopener noreferrer" : undefined} |
101 | 155 | className="flex items-center gap-2 px-3 py-2 text-muted-foreground hover:text-foreground hover:bg-accent" |
102 | 156 | > |
103 | | - <Github size={14} className="text-muted-foreground" /> |
104 | | - <span className="text-sm">GitHub</span> |
| 157 | + {item.iconSm} |
| 158 | + <span className="text-sm">{item.label}</span> |
105 | 159 | </a> |
106 | | - )} |
| 160 | + ))} |
107 | 161 | </div> |
108 | 162 | )} |
109 | 163 | </div> |
@@ -151,27 +205,18 @@ export default function AuthorCard({ authorKey, className }: Props) { |
151 | 205 | )} |
152 | 206 |
|
153 | 207 | <div className="flex flex-col items-start pl-2 gap-3"> |
154 | | - {author.email && ( |
155 | | - <a |
156 | | - href={author.email} |
157 | | - className="inline-flex items-center gap-3 text-muted-foreground hover:text-foreground transition-colors duration-200" |
158 | | - > |
159 | | - <Mail size={16} /> |
160 | | - <span className="text-sm">Email</span> |
161 | | - </a> |
162 | | - )} |
163 | | - |
164 | | - {author.github && ( |
| 208 | + {linkItems.map((item) => ( |
165 | 209 | <a |
166 | | - href={author.github} |
167 | | - target="_blank" |
168 | | - rel="noopener noreferrer" |
| 210 | + key={`${item.label}-${item.href}-desktop`} |
| 211 | + href={item.href} |
| 212 | + target={item.external ? "_blank" : undefined} |
| 213 | + rel={item.external ? "noopener noreferrer" : undefined} |
169 | 214 | className="inline-flex items-center gap-3 text-muted-foreground hover:text-foreground transition-colors duration-200" |
170 | 215 | > |
171 | | - <Github size={16} /> |
172 | | - <span className="text-sm">GitHub</span> |
| 216 | + {item.iconMd} |
| 217 | + <span className="text-sm">{item.label}</span> |
173 | 218 | </a> |
174 | | - )} |
| 219 | + ))} |
175 | 220 | </div> |
176 | 221 | </div> |
177 | 222 | </div> |
|
0 commit comments