feat(profile): add social links section on public profile (B2)
This commit is contained in:
parent
9be0e6e14f
commit
09f9dc3de6
4 changed files with 113 additions and 0 deletions
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
|
|
|||
Loading…
Reference in a new issue