feat: Added SocialIcons component with 9 platform SVG icons, rendered s…
- "frontend/src/components/SocialIcons.tsx" - "frontend/src/pages/CreatorDetail.tsx" - "frontend/src/App.css" GSD-Task: S02/T02
This commit is contained in:
parent
a9e3572573
commit
f4f21b6c36
4 changed files with 159 additions and 2 deletions
|
|
@ -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);
|
||||
|
|
|
|||
110
frontend/src/components/SocialIcons.tsx
Normal file
110
frontend/src/components/SocialIcons.tsx
Normal file
|
|
@ -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 (
|
||||
<svg viewBox="0 0 24 24" style={S}>
|
||||
<rect {...P} x="2" y="2" width="20" height="20" rx="5" />
|
||||
<circle {...P} cx="12" cy="12" r="5" />
|
||||
<circle fill="currentColor" stroke="none" cx="17.5" cy="6.5" r="1.2" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconYoutube() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" style={S}>
|
||||
<rect {...P} x="2" y="4" width="20" height="16" rx="4" />
|
||||
<polygon {...P} points="10,8.5 16,12 10,15.5" fill="currentColor" stroke="none" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconBandcamp() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" style={S}>
|
||||
<polygon {...P} points="6,16 10,8 18,8 14,16" strokeWidth={1.8} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconSoundcloud() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" style={S}>
|
||||
<path {...P} d="M2 16 v-3 M5 16 v-5 M8 16 v-7 M11 16 v-8 M14 16 v-6" strokeWidth={1.8} />
|
||||
<path {...P} d="M16 16 v-7 a4 4 0 0 1 4 0 v7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconTwitter() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" style={S}>
|
||||
<path {...P} d="M4 20 L10.5 12 M13.5 12 L20 4" strokeWidth={1.8} />
|
||||
<path {...P} d="M20 20 L13.5 12 M10.5 12 L4 4" strokeWidth={1.8} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconSpotify() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" style={S}>
|
||||
<circle {...P} cx="12" cy="12" r="10" />
|
||||
<path {...P} d="M7 9.5 q5-2 10 0" />
|
||||
<path {...P} d="M8 12.5 q4-1.5 8 0" />
|
||||
<path {...P} d="M9 15.5 q3-1 6 0" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconFacebook() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" style={S}>
|
||||
<rect {...P} x="2" y="2" width="20" height="20" rx="5" />
|
||||
<path {...P} d="M15 3 v4 h-2 a1 1 0 0 0-1 1 v3 h3 l-0.5 3 H12 v7" strokeWidth={1.8} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconTwitch() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" style={S}>
|
||||
<path {...P} d="M4 3 L4 19 H8 V22 L11 19 H15 L20 14 V3 Z" />
|
||||
<line {...P} x1="10" y1="8" x2="10" y2="12" />
|
||||
<line {...P} x1="15" y1="8" x2="15" y2="12" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconGlobe() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" style={S}>
|
||||
<circle {...P} cx="12" cy="12" r="10" />
|
||||
<ellipse {...P} cx="12" cy="12" rx="4" ry="10" />
|
||||
<line {...P} x1="2" y1="12" x2="22" y2="12" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const ICON_MAP: Record<string, () => 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 <Icon />;
|
||||
}
|
||||
|
|
@ -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 && (
|
||||
<p className="creator-hero__bio">{creator.bio}</p>
|
||||
)}
|
||||
{creator.social_links && Object.keys(creator.social_links).length > 0 && (
|
||||
<div className="creator-hero__socials">
|
||||
{Object.entries(creator.social_links).map(([platform, url]) => (
|
||||
<a key={platform} href={url} target="_blank" rel="noopener noreferrer"
|
||||
className="creator-hero__social-link" title={platform}>
|
||||
<SocialIcon platform={platform} />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{creator.genres && creator.genres.length > 0 && (
|
||||
<div className="creator-hero__genres">
|
||||
{creator.genres.map((g) => (
|
||||
|
|
@ -134,9 +145,17 @@ export default function CreatorDetail() {
|
|||
|
||||
{/* Stats */}
|
||||
<div className="creator-detail__stats-bar">
|
||||
<span className="creator-detail__stats">
|
||||
{creator.technique_count} technique{creator.technique_count !== 1 ? "s" : ""}
|
||||
</span>
|
||||
<span className="creator-detail__stats-sep">·</span>
|
||||
<span className="creator-detail__stats">
|
||||
{creator.video_count} video{creator.video_count !== 1 ? "s" : ""}
|
||||
</span>
|
||||
<span className="creator-detail__stats-sep">·</span>
|
||||
<span className="creator-detail__stats">
|
||||
{creator.moment_count} moment{creator.moment_count !== 1 ? "s" : ""}
|
||||
</span>
|
||||
{Object.keys(creator.genre_breakdown).length > 0 && (
|
||||
<span className="creator-detail__topic-pills">
|
||||
{Object.entries(creator.genre_breakdown)
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
{"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"}
|
||||
Loading…
Add table
Reference in a new issue