feat(profile): add social links section on public profile (B2)

This commit is contained in:
senke 2026-02-20 15:06:20 +01:00
parent 9be0e6e14f
commit 09f9dc3de6
4 changed files with 113 additions and 0 deletions

View file

@ -0,0 +1,37 @@
import type { Meta, StoryObj } from '@storybook/react';
import { ProfileSocialLinksSection } from './ProfileSocialLinksSection';
const meta: Meta<typeof ProfileSocialLinksSection> = {
component: ProfileSocialLinksSection,
tags: ['autodocs'],
parameters: {
layout: 'centered',
},
};
export default meta;
type Story = StoryObj<typeof ProfileSocialLinksSection>;
export const Default: Story = {
args: {
socialLinks: {
twitter: 'https://twitter.com/veza_user',
youtube: 'https://youtube.com/@veza_channel',
instagram: 'https://instagram.com/veza_profile',
},
},
};
export const Empty: Story = {
args: {
socialLinks: null,
},
};
export const SingleLink: Story = {
args: {
socialLinks: {
website: 'https://veza.example.com',
},
},
};

View file

@ -0,0 +1,65 @@
/**
* B2: Section Liens sociaux sur le profil public.
* Affiche Twitter, YouTube, Instagram, etc. avec icônes et URLs cliquables.
*/
import {
Twitter,
Instagram,
Facebook,
Youtube,
Link as LinkIcon,
} from 'lucide-react';
const SOCIAL_KEYS = [
{ key: 'twitter', label: 'Twitter', Icon: Twitter },
{ key: 'instagram', label: 'Instagram', Icon: Instagram },
{ key: 'facebook', label: 'Facebook', Icon: Facebook },
{ key: 'youtube', label: 'YouTube', Icon: Youtube },
{ key: 'website', label: 'Website', Icon: LinkIcon },
] as const;
interface ProfileSocialLinksSectionProps {
socialLinks?: Record<string, unknown> | null;
}
function isValidUrl(value: unknown): value is string {
if (typeof value !== 'string') return false;
if (!value.trim()) return false;
return value.startsWith('http://') || value.startsWith('https://');
}
export function ProfileSocialLinksSection({ socialLinks }: ProfileSocialLinksSectionProps) {
const entries = SOCIAL_KEYS.filter(
({ key }) => socialLinks?.[key] != null && isValidUrl(socialLinks[key]),
).map(({ key, label, Icon }) => ({
key,
label,
Icon,
href: String(socialLinks![key]),
}));
if (entries.length === 0) return null;
return (
<div className="mt-6 pt-6 border-t border-border">
<h3 className="text-sm font-bold uppercase tracking-widest text-muted-foreground mb-3">
Links
</h3>
<div className="flex flex-wrap gap-4">
{entries.map(({ key, label, Icon, href }) => (
<a
key={key}
href={href}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors duration-[var(--duration-fast)]"
aria-label={label}
>
<Icon className="h-4 w-4" aria-hidden />
<span>{label}</span>
</a>
))}
</div>
</div>
);
}

View file

@ -1,4 +1,5 @@
import { MapPin, Calendar, User, Music, Library, Users } from 'lucide-react';
import { ProfileSocialLinksSection } from '../../components/ProfileSocialLinksSection';
import { Avatar } from '@/components/ui/avatar';
import { Card, CardContent } from '@/components/ui/card';
import { FollowButton } from '../../components/FollowButton';
@ -108,6 +109,7 @@ export function UserProfilePageHeader({
</span>
)}
</p>
<ProfileSocialLinksSection socialLinks={profile.social_links} />
</div>
{/* Stats pills */}

View file

@ -224,6 +224,7 @@ export const handlersMisc = [
first_name: 'Story',
last_name: 'User',
avatar_url: `https://i.pravatar.cc/150?u=${params.username}`,
banner_url: params.username === 'demo' ? 'https://picsum.photos/1200/400?random=1' : null,
bio: 'Music enthusiast',
location: 'Paris, France',
birthdate: null,
@ -231,6 +232,14 @@ export const handlersMisc = [
created_at: '2024-01-01T00:00:00Z',
followers_count: 42,
following_count: 10,
social_links:
params.username === 'demo'
? {
twitter: 'https://twitter.com/veza_demo',
youtube: 'https://youtube.com/@veza_channel',
instagram: 'https://instagram.com/veza_profile',
}
: undefined,
},
});
}),