diff --git a/frontend/src/App.css b/frontend/src/App.css index 1d95c39..6fb5bfb 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -2698,14 +2698,42 @@ a.app-footer__repo:hover { gap: 0.375rem; } +.creator-hero__socials { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin: 0.5rem 0; +} + +.creator-hero__social-link { + color: var(--color-text-muted); + transition: color 0.15s ease; + display: inline-flex; + align-items: center; +} + +.creator-hero__social-link:hover { + color: var(--color-accent); +} + +.creator-hero__social-link svg { + width: 1.25rem; + height: 1.25rem; +} + .creator-detail__stats-bar { display: flex; align-items: center; - gap: 0.75rem; + gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.5rem; } +.creator-detail__stats-sep { + color: var(--color-text-muted); + margin: 0 0.125rem; +} + .creator-detail__stats { font-size: 0.875rem; color: var(--color-text-secondary); diff --git a/frontend/src/components/SocialIcons.tsx b/frontend/src/components/SocialIcons.tsx new file mode 100644 index 0000000..c9c8435 --- /dev/null +++ b/frontend/src/components/SocialIcons.tsx @@ -0,0 +1,110 @@ +/** + * Inline SVG icons for social media platforms. + * Monoline stroke style matching CategoryIcons.tsx. + */ + +const S = { width: "1.25em", height: "1.25em", verticalAlign: "-0.15em" } as const; +const P = { fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round" as const, strokeLinejoin: "round" as const }; + +export function IconInstagram() { + return ( + + + + + + ); +} + +export function IconYoutube() { + return ( + + + + + ); +} + +export function IconBandcamp() { + return ( + + + + ); +} + +export function IconSoundcloud() { + return ( + + + + + ); +} + +export function IconTwitter() { + return ( + + + + + ); +} + +export function IconSpotify() { + return ( + + + + + + + ); +} + +export function IconFacebook() { + return ( + + + + + ); +} + +export function IconTwitch() { + return ( + + + + + + ); +} + +export function IconGlobe() { + return ( + + + + + + ); +} + +const ICON_MAP: Record JSX.Element> = { + instagram: IconInstagram, + youtube: IconYoutube, + bandcamp: IconBandcamp, + soundcloud: IconSoundcloud, + twitter: IconTwitter, + x: IconTwitter, + spotify: IconSpotify, + facebook: IconFacebook, + twitch: IconTwitch, +}; + +/** Resolves a platform name to the matching icon, falling back to globe. */ +export function SocialIcon({ platform }: { platform: string }) { + const Icon = ICON_MAP[platform.toLowerCase()] ?? IconGlobe; + return ; +} diff --git a/frontend/src/pages/CreatorDetail.tsx b/frontend/src/pages/CreatorDetail.tsx index 6388d8e..b58a978 100644 --- a/frontend/src/pages/CreatorDetail.tsx +++ b/frontend/src/pages/CreatorDetail.tsx @@ -12,6 +12,7 @@ import { type CreatorDetailResponse, } from "../api/public-client"; import CreatorAvatar from "../components/CreatorAvatar"; +import { SocialIcon } from "../components/SocialIcons"; import SortDropdown from "../components/SortDropdown"; import { catSlug } from "../utils/catSlug"; import { useDocumentTitle } from "../hooks/useDocumentTitle"; @@ -122,6 +123,16 @@ export default function CreatorDetail() { {creator.bio && ( {creator.bio} )} + {creator.social_links && Object.keys(creator.social_links).length > 0 && ( + + {Object.entries(creator.social_links).map(([platform, url]) => ( + + + + ))} + + )} {creator.genres && creator.genres.length > 0 && ( {creator.genres.map((g) => ( @@ -134,9 +145,17 @@ export default function CreatorDetail() { {/* Stats */} + + {creator.technique_count} technique{creator.technique_count !== 1 ? "s" : ""} + + · {creator.video_count} video{creator.video_count !== 1 ? "s" : ""} + · + + {creator.moment_count} moment{creator.moment_count !== 1 ? "s" : ""} + {Object.keys(creator.genre_breakdown).length > 0 && ( {Object.entries(creator.genre_breakdown) diff --git a/frontend/tsconfig.app.tsbuildinfo b/frontend/tsconfig.app.tsbuildinfo index a04a926..db3a8d3 100644 --- a/frontend/tsconfig.app.tsbuildinfo +++ b/frontend/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/public-client.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/CategoryIcons.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ReportIssueModal.tsx","./src/components/SearchAutocomplete.tsx","./src/components/SortDropdown.tsx","./src/components/TableOfContents.tsx","./src/components/TagList.tsx","./src/hooks/useCountUp.ts","./src/hooks/useDocumentTitle.ts","./src/hooks/useSortPreference.ts","./src/pages/About.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/AdminTechniquePages.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/Home.tsx","./src/pages/SearchResults.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/utils/catSlug.ts","./src/utils/citations.tsx"],"version":"5.6.3"} \ No newline at end of file +{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/public-client.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/CategoryIcons.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ReportIssueModal.tsx","./src/components/SearchAutocomplete.tsx","./src/components/SocialIcons.tsx","./src/components/SortDropdown.tsx","./src/components/TableOfContents.tsx","./src/components/TagList.tsx","./src/hooks/useCountUp.ts","./src/hooks/useDocumentTitle.ts","./src/hooks/useSortPreference.ts","./src/pages/About.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/AdminTechniquePages.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/Home.tsx","./src/pages/SearchResults.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/utils/catSlug.ts","./src/utils/citations.tsx"],"version":"5.6.3"} \ No newline at end of file
{creator.bio}