diff --git a/src/components/organisms/index.ts b/src/components/organisms/index.ts index 14ccb2c..f3426a3 100644 --- a/src/components/organisms/index.ts +++ b/src/components/organisms/index.ts @@ -3,6 +3,7 @@ export { IngredientChecklist } from './ingredient-checklist'; export { NutritionFacts } from './nutrition-facts'; +export { ProfileCard } from './profile-card'; export { RecipeCollectionSaver } from './recipe-collection-saver'; export { Dialog } from './dialog'; export { LoginForm } from './login-form'; @@ -25,6 +26,7 @@ export { MealPlanCard } from './meal-plan-card'; // Re-export types export type { IngredientChecklistProps } from './ingredient-checklist'; export type { NutritionFactsProps } from './nutrition-facts'; +export type { ProfileCardProps } from './profile-card'; export type { RecipeCollectionSaverProps } from './recipe-collection-saver'; export type { DialogProps } from './dialog'; export type { LoginFormProps } from './login-form'; diff --git a/src/components/organisms/profile-card/ProfileCard.stories.tsx b/src/components/organisms/profile-card/ProfileCard.stories.tsx new file mode 100644 index 0000000..1f5e70d --- /dev/null +++ b/src/components/organisms/profile-card/ProfileCard.stories.tsx @@ -0,0 +1,405 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +// Simple mock function for stories +const fn = () => () => {}; +import { ProfileCard } from './profile-card'; +import { LocaleProvider } from '@/providers/LocaleProvider'; +import type { Profile } from '@/types'; + +// Mock profile data +const mockChefProfile: Profile = { + id: 'chef-maria-rossi', + name: 'Chef Maria Rossi', + avatar: 'https://picsum.photos/seed/chef-maria/200/200', + title: 'Executive Chef & Culinary Director', + bio: 'Award-winning chef specializing in authentic Italian cuisine with over 15 years of experience in Michelin-starred restaurants.', + stats: { + followers: 24500, + recipes: 127, + avgRating: 4.8 + }, + badges: ['Michelin Star', 'James Beard Award', 'Top Chef Winner'], + specialties: ['Italian', 'Mediterranean', 'Pasta Expert', 'Sustainable Cooking'], + location: 'Milan, Italy', + socialLinks: [ + { platform: 'instagram', url: 'https://instagram.com/chefmariarossi', handle: '@chefmariarossi' }, + { platform: 'twitter', url: 'https://twitter.com/chefmariarossi', handle: '@chefmariarossi' }, + { platform: 'youtube', url: 'https://youtube.com/@chefmariarossi', handle: 'Chef Maria Rossi' }, + { platform: 'website', url: 'https://mariarossi.chef', handle: 'mariarossi.chef' }, + ], + isVerified: true, + joinedDate: '2019-03-15' +}; + +const mockHomeBakerProfile: Profile = { + id: 'home-baker-sarah', + name: 'Sarah Johnson', + avatar: 'https://picsum.photos/seed/baker-sarah/200/200', + title: 'Home Baker & Recipe Developer', + bio: 'Passionate home baker sharing family recipes and creative dessert ideas for everyday cooking.', + stats: { + followers: 8200, + recipes: 89, + avgRating: 4.6 + }, + badges: ['Community Favorite', 'Rising Star'], + specialties: ['Baking', 'Desserts', 'American', 'Family-Friendly'], + location: 'Austin, Texas, USA', + socialLinks: [ + { platform: 'instagram', url: 'https://instagram.com/sarahbakes', handle: '@sarahbakes' }, + { platform: 'facebook', url: 'https://facebook.com/sarahbakesaustin', handle: 'Sarah Bakes Austin' }, + ], + isVerified: false, + joinedDate: '2021-08-20' +}; + +const mockInternationalChefProfile: Profile = { + id: 'chef-kenji-tanaka', + name: 'Chef Kenji Tanaka', + avatar: 'https://picsum.photos/seed/chef-kenji/200/200', + title: 'Traditional Japanese Cuisine Master', + bio: 'Third-generation sushi master bringing authentic Japanese flavors to modern kitchens worldwide.', + stats: { + followers: 156000, + recipes: 203, + avgRating: 4.9 + }, + badges: ['Master Sushi Chef', 'UNESCO Heritage Ambassador', 'Top 50 Chefs'], + specialties: ['Japanese', 'Sushi', 'Kaiseki', 'Traditional', 'Seafood'], + location: 'Tokyo, Japan', + socialLinks: [ + { platform: 'instagram', url: 'https://instagram.com/chefkenjitanaka', handle: '@chefkenjitanaka' }, + { platform: 'twitter', url: 'https://twitter.com/chefkenjitanaka', handle: '@chefkenjitanaka' }, + { platform: 'youtube', url: 'https://youtube.com/@chefkenjitanaka', handle: 'Chef Kenji Tanaka' }, + { platform: 'website', url: 'https://kenjitanaka.jp', handle: 'kenjitanaka.jp' }, + ], + isVerified: true, + joinedDate: '2018-01-10' +}; + +const meta: Meta = { + title: 'Organisms/ProfileCard', + component: ProfileCard, + parameters: { + layout: 'centered', + docs: { + description: { + component: 'ProfileCard displays chef and user profile information with social interaction functionality. Supports multiple layout variants for different contexts and includes full internationalization support.' + } + } + }, + decorators: [ + (Story) => ( + +
+ +
+
+ ), + ], + argTypes: { + variant: { + control: 'select', + options: ['compact', 'standard', 'detailed'], + description: 'Visual layout variant for different contexts', + }, + showFollowButton: { + control: 'boolean', + description: 'Whether to show follow/unfollow button', + }, + isFollowing: { + control: 'boolean', + description: 'Whether user is currently following this profile', + }, + onFollow: { + action: 'follow', + description: 'Callback when follow button is clicked', + }, + onProfileClick: { + action: 'profileClick', + description: 'Callback when profile card is clicked', + }, + }, +}; + +export default meta; +type Story = StoryObj; + +// Standard variant with professional chef +export const Standard: Story = { + args: { + profile: mockChefProfile, + variant: 'standard', + showFollowButton: true, + isFollowing: false, + onFollow: fn, + onProfileClick: fn, + }, + parameters: { + docs: { + description: { + story: 'Standard layout showing professional chef profile with follow functionality, stats, and specialties. Ideal for recipe discovery and search results.' + } + } + } +}; + +// Compact variant for sidebars +export const Compact: Story = { + args: { + profile: mockHomeBakerProfile, + variant: 'compact', + showFollowButton: true, + isFollowing: false, + onFollow: fn, + onProfileClick: fn, + }, + parameters: { + docs: { + description: { + story: 'Compact layout for sidebar placement or recipe attribution. Shows essential information in minimal space.' + } + } + } +}; + +// Detailed variant for featured showcases +export const Detailed: Story = { + args: { + profile: mockInternationalChefProfile, + variant: 'detailed', + showFollowButton: true, + isFollowing: false, + onFollow: fn, + onProfileClick: fn, + }, + parameters: { + docs: { + description: { + story: 'Detailed layout for featured chef showcases with complete profile information, achievements, and social links.' + } + } + } +}; + +// Following state +export const Following: Story = { + args: { + profile: mockChefProfile, + variant: 'standard', + showFollowButton: true, + isFollowing: true, + onFollow: fn, + onProfileClick: fn, + }, + parameters: { + docs: { + description: { + story: 'Shows the following state with updated button styling and text.' + } + } + } +}; + +// Without follow button +export const ReadOnly: Story = { + args: { + profile: mockHomeBakerProfile, + variant: 'standard', + showFollowButton: false, + onProfileClick: fn, + }, + parameters: { + docs: { + description: { + story: 'Read-only profile card without follow functionality, suitable for display contexts.' + } + } + } +}; + +// Verified chef with achievements +export const VerifiedChef: Story = { + args: { + profile: { + ...mockInternationalChefProfile, + badges: ['Master Sushi Chef', 'UNESCO Heritage Ambassador', 'Top 50 Chefs', 'Verified Professional', 'Excellence Award'], + }, + variant: 'detailed', + showFollowButton: true, + isFollowing: false, + onFollow: fn, + onProfileClick: fn, + }, + parameters: { + docs: { + description: { + story: 'Verified chef profile showcasing multiple achievements and professional credentials.' + } + } + } +}; + +// Minimal social presence +export const MinimalSocial: Story = { + args: { + profile: { + ...mockHomeBakerProfile, + socialLinks: [ + { platform: 'instagram', url: 'https://instagram.com/sarahbakes', handle: '@sarahbakes' }, + ], + specialties: ['Baking', 'Desserts'], + badges: ['Community Favorite'], + }, + variant: 'standard', + showFollowButton: true, + isFollowing: false, + onFollow: fn, + onProfileClick: fn, + }, + parameters: { + docs: { + description: { + story: 'Profile with minimal social media presence and fewer specialties/badges.' + } + } + } +}; + +// Without location +export const NoLocation: Story = { + args: { + profile: { + ...mockChefProfile, + location: undefined, + }, + variant: 'standard', + showFollowButton: true, + isFollowing: false, + onFollow: fn, + onProfileClick: fn, + }, + parameters: { + docs: { + description: { + story: 'Profile card without location information.' + } + } + } +}; + +// RTL Layout Test +export const RTLTest: Story = { + args: { + profile: { + id: 'chef-ahmed-hassan', + name: 'Chef Ahmed Hassan', + avatar: 'https://picsum.photos/seed/chef-ahmed/200/200', + title: 'Traditional Middle Eastern Cuisine Expert', + bio: 'خبير في الطبخ التقليدي الشرق أوسطي مع خبرة أكثر من 20 عامًا في المطاعم الفاخرة.', + stats: { + followers: 45300, + recipes: 184, + avgRating: 4.7 + }, + badges: ['Master Chef', 'Heritage Cooking Expert', 'Award Winner'], + specialties: ['Middle Eastern', 'Lebanese', 'Traditional', 'Halal'], + location: 'Dubai, UAE', + socialLinks: [ + { platform: 'instagram', url: 'https://instagram.com/chefahmadhassan', handle: '@chefahmadhassan' }, + { platform: 'twitter', url: 'https://twitter.com/chefahmadhassan', handle: '@chefahmadhassan' }, + ], + isVerified: true, + joinedDate: '2017-11-05' + }, + variant: 'standard', + showFollowButton: true, + isFollowing: false, + onFollow: fn, + onProfileClick: fn, + }, + decorators: [ + (Story) => ( +
+ +
+ +
+
+
+ ), + ], + parameters: { + docs: { + description: { + story: 'Profile card in RTL layout for Arabic locale with proper text direction and layout adjustments.' + } + } + } +}; + +// Large follower count formatting +export const HighStats: Story = { + args: { + profile: { + ...mockInternationalChefProfile, + stats: { + followers: 1250000, + recipes: 847, + avgRating: 4.95 + }, + }, + variant: 'detailed', + showFollowButton: true, + isFollowing: false, + onFollow: fn, + onProfileClick: fn, + }, + parameters: { + docs: { + description: { + story: 'Profile with high statistics demonstrating number formatting for large follower counts.' + } + } + } +}; + +// Grid Layout Example +export const GridExample: Story = { + render: () => ( + +
+ + + +
+
+ ), + parameters: { + docs: { + description: { + story: 'Multiple profile cards in a responsive grid layout showing compact variant usage.' + } + } + } +}; \ No newline at end of file diff --git a/src/components/organisms/profile-card/ProfileCard.test.tsx b/src/components/organisms/profile-card/ProfileCard.test.tsx new file mode 100644 index 0000000..cb8b331 --- /dev/null +++ b/src/components/organisms/profile-card/ProfileCard.test.tsx @@ -0,0 +1,148 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, fireEvent } from '@/test-utils'; +import { ProfileCard } from './profile-card'; +import type { Profile } from '@/types'; + +// Mock profile data for testing +const mockProfile: Profile = { + id: 'test-chef', + name: 'Test Chef', + avatar: 'https://example.com/avatar.jpg', + title: 'Executive Chef', + bio: 'A passionate chef with years of experience', + stats: { + followers: 1000, + recipes: 50, + avgRating: 4.5 + }, + badges: ['Verified', 'Top Chef'], + specialties: ['Italian', 'Mediterranean'], + location: 'New York, USA', + socialLinks: [ + { platform: 'instagram', url: 'https://instagram.com/testchef', handle: '@testchef' }, + { platform: 'twitter', url: 'https://twitter.com/testchef', handle: '@testchef' }, + ], + isVerified: true, + joinedDate: '2020-01-01' +}; + +describe('ProfileCard', () => { + describe('Basic Rendering', () => { + it('renders profile name correctly', () => { + render(); + expect(screen.getByText('Test Chef')).toBeInTheDocument(); + }); + + it('renders profile title correctly', () => { + render(); + expect(screen.getByText('Executive Chef')).toBeInTheDocument(); + }); + + it('renders profile bio correctly', () => { + render(); + expect(screen.getByText('A passionate chef with years of experience')).toBeInTheDocument(); + }); + + it('renders profile location correctly', () => { + render(); + expect(screen.getByText('New York, USA')).toBeInTheDocument(); + }); + + it('renders avatar with correct alt text', () => { + render(); + const avatar = screen.getByRole('img', { name: 'Test Chef' }); + expect(avatar).toBeInTheDocument(); + expect(avatar).toHaveAttribute('src', 'https://example.com/avatar.jpg'); + }); + }); + + describe('Variants', () => { + it('renders compact variant', () => { + render(); + expect(screen.getByText('Test Chef')).toBeInTheDocument(); + }); + + it('renders standard variant', () => { + render(); + expect(screen.getByText('Test Chef')).toBeInTheDocument(); + }); + + it('renders detailed variant', () => { + render(); + expect(screen.getByText('Test Chef')).toBeInTheDocument(); + }); + }); + + describe('Follow Functionality', () => { + it('shows follow button when enabled', () => { + const onFollow = vi.fn(); + render( + + ); + expect(screen.getByRole('button')).toBeInTheDocument(); + }); + + it('hides follow button when disabled', () => { + render( + + ); + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + }); + + it('calls onFollow callback when follow button clicked', () => { + const onFollow = vi.fn(); + render( + + ); + + const followButton = screen.getByRole('button'); + fireEvent.click(followButton); + expect(onFollow).toHaveBeenCalledWith('test-chef'); + }); + }); + + describe('Statistics Display', () => { + it('displays follower count', () => { + render(); + expect(screen.getByText('1,000')).toBeInTheDocument(); + }); + + it('displays recipe count', () => { + render(); + expect(screen.getByText('50')).toBeInTheDocument(); + }); + + it('displays rating', () => { + render(); + expect(screen.getByText('4.5')).toBeInTheDocument(); + }); + }); + + describe('Edge Cases', () => { + it('handles profile without location', () => { + const profileWithoutLocation = { ...mockProfile, location: undefined }; + render(); + expect(screen.getByText('Test Chef')).toBeInTheDocument(); + expect(screen.queryByText('New York, USA')).not.toBeInTheDocument(); + }); + + it('handles profile without title', () => { + const profileWithoutTitle = { ...mockProfile, title: undefined }; + render(); + expect(screen.getByText('Test Chef')).toBeInTheDocument(); + expect(screen.queryByText('Executive Chef')).not.toBeInTheDocument(); + }); + }); +}); \ No newline at end of file diff --git a/src/components/organisms/profile-card/index.ts b/src/components/organisms/profile-card/index.ts new file mode 100644 index 0000000..d494f06 --- /dev/null +++ b/src/components/organisms/profile-card/index.ts @@ -0,0 +1,2 @@ +export { ProfileCard } from './profile-card'; +export type { ProfileCardProps } from './profile-card'; \ No newline at end of file diff --git a/src/components/organisms/profile-card/profile-card.tsx b/src/components/organisms/profile-card/profile-card.tsx new file mode 100644 index 0000000..1b1a986 --- /dev/null +++ b/src/components/organisms/profile-card/profile-card.tsx @@ -0,0 +1,410 @@ +import * as React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useLocale } from '@/hooks/useLocale'; +import { + MapPin, + Users, + BookOpen, + Star, + CheckCircle, + Crown, + Instagram, + Twitter, + Youtube, + Facebook, + Globe, + UserPlus, + UserCheck +} from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { Card } from '@/components/molecules/card'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import type { Profile, SocialLink } from '@/types'; + +/** + * Props for ProfileCard component - Organism level + * + * ProfileCard displays chef and user profile information with social interaction functionality. + * Supports multiple layout variants and cultural considerations for global users. + * + * @example + * handleFollow(id)} + * /> + */ +export interface ProfileCardProps { + /** Profile data to display */ + profile: Profile; + /** Visual layout variant */ + variant?: 'compact' | 'standard' | 'detailed'; + /** Whether to show follow/unfollow button */ + showFollowButton?: boolean; + /** Whether user is currently following this profile */ + isFollowing?: boolean; + /** Callback when follow button is clicked */ + onFollow?: (profileId: string) => void; + /** Callback when profile card is clicked */ + onProfileClick?: (profileId: string) => void; + /** Custom CSS class */ + className?: string; + /** Accessibility label */ + 'aria-label'?: string; +} + +const SOCIAL_ICONS = { + instagram: Instagram, + twitter: Twitter, + youtube: Youtube, + facebook: Facebook, + website: Globe, + tiktok: Globe, // Using Globe as fallback for TikTok +} as const; + +const ProfileCard = React.forwardRef( + ({ + profile, + variant = 'standard', + showFollowButton = false, + isFollowing = false, + onFollow, + onProfileClick, + className, + ...props + }, ref) => { + const { t } = useTranslation('profile'); + const { formatNumber } = useLocale(); + + const handleFollowClick = (e: React.MouseEvent) => { + e.stopPropagation(); + onFollow?.(profile.id); + }; + + const handleCardClick = () => { + onProfileClick?.(profile.id); + }; + + const getInitials = (name: string) => { + return name + .split(' ') + .map(word => word.charAt(0)) + .join('') + .toUpperCase() + .slice(0, 2); + }; + + const renderSocialLinks = (links: SocialLink[], limit?: number) => { + const socialLinksToShow = limit ? links.slice(0, limit) : links; + + return ( +
+ {socialLinksToShow.map((link, index) => { + const IconComponent = SOCIAL_ICONS[link.platform]; + return ( + + + + ); + })} +
+ ); + }; + + const renderBadges = (badges: string[], limit?: number) => { + const badgesToShow = limit ? badges.slice(0, limit) : badges; + const remainingCount = badges.length - (limit || badges.length); + + return ( +
+ {badgesToShow.map((badge, index) => ( + + {badge} + + ))} + {remainingCount > 0 && ( + + +{remainingCount} + + )} +
+ ); + }; + + const renderStats = () => ( +
+
+ + {formatNumber(profile.stats.followers)} + {t('stats.followers')} +
+
+ + {formatNumber(profile.stats.recipes)} + {t('stats.recipes')} +
+
+ + {profile.stats.avgRating.toFixed(1)} + {t('stats.rating')} +
+
+ ); + + const renderVerificationBadge = () => { + if (!profile.isVerified) return null; + + return ( +
+ + {t('badges.verified')} +
+ ); + }; + + const renderFollowButton = () => { + if (!showFollowButton) return null; + + const FollowIcon = isFollowing ? UserCheck : UserPlus; + + return ( + + ); + }; + + if (variant === 'compact') { + return ( + +
+
+ + + {getInitials(profile.name)} + + {renderVerificationBadge()} +
+ +
+
+

{profile.name}

+ {profile.title && ( + + {profile.title} + + )} +
+
+ + + {profile.stats.avgRating.toFixed(1)} • {formatNumber(profile.stats.recipes)} {t('stats.recipesShort')} + +
+
+ + {renderFollowButton()} +
+
+ ); + } + + if (variant === 'detailed') { + return ( + +
+ {/* Header with avatar and basic info */} +
+
+ + + {getInitials(profile.name)} + + {renderVerificationBadge()} +
+ +
+
+
+

{profile.name}

+ {profile.title && ( +

{profile.title}

+ )} +
+ {renderFollowButton()} +
+ + {profile.location && ( +
+ + {profile.location} +
+ )} +
+
+ + {/* Bio */} +

{profile.bio}

+ + {/* Stats */} +
+
+
{formatNumber(profile.stats.followers)}
+
{t('stats.followers')}
+
+
+
{formatNumber(profile.stats.recipes)}
+
{t('stats.recipes')}
+
+
+
{profile.stats.avgRating.toFixed(1)}
+
{t('stats.rating')}
+
+
+ + {/* Specialties */} + {profile.specialties.length > 0 && ( +
+

{t('sections.specialties')}

+ {renderBadges(profile.specialties)} +
+ )} + + {/* Badges */} + {profile.badges.length > 0 && ( +
+

{t('sections.achievements')}

+ {renderBadges(profile.badges)} +
+ )} + + {/* Social Links */} + {profile.socialLinks && profile.socialLinks.length > 0 && ( +
+

{t('sections.socialLinks')}

+ {renderSocialLinks(profile.socialLinks)} +
+ )} +
+
+ ); + } + + // Standard variant (default) + return ( + +
+ {/* Header */} +
+
+ + + {getInitials(profile.name)} + + {renderVerificationBadge()} +
+ +
+
+
+

{profile.name}

+ {profile.title && ( +

{profile.title}

+ )} +
+ {renderFollowButton()} +
+ + {profile.location && ( +
+ + {profile.location} +
+ )} +
+
+ + {/* Bio */} +

{profile.bio}

+ + {/* Stats */} +
+ {renderStats()} +
+ + {/* Specialties */} + {profile.specialties.length > 0 && ( +
+

{t('sections.specialties')}

+ {renderBadges(profile.specialties, 3)} +
+ )} + + {/* Social Links */} + {profile.socialLinks && profile.socialLinks.length > 0 && ( +
+ {t('sections.socialLinks')} + {renderSocialLinks(profile.socialLinks, 4)} +
+ )} +
+
+ ); + } +); + +ProfileCard.displayName = 'ProfileCard'; + +export { ProfileCard }; \ No newline at end of file diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 235d3d4..2b1e7ca 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -10,6 +10,9 @@ import arSACommon from './locales/ar-SA/common.json'; import enUSNutrition from './locales/en-US/nutrition.json'; import nlNLNutrition from './locales/nl-NL/nutrition.json'; import arSANutrition from './locales/ar-SA/nutrition.json'; +import enUSProfile from './locales/en-US/profile.json'; +import nlNLProfile from './locales/nl-NL/profile.json'; +import arSAProfile from './locales/ar-SA/profile.json'; import enUSCategories from './locales/en-US/categories.json'; import nlNLCategories from './locales/nl-NL/categories.json'; import arSACategories from './locales/ar-SA/categories.json'; @@ -19,17 +22,20 @@ const resources = { 'en-US': { common: enUSCommon, nutrition: enUSNutrition, - categories: enUSCategories, + profile: enUSProfile, + categories: enUSCategories }, 'nl-NL': { common: nlNLCommon, nutrition: nlNLNutrition, - categories: nlNLCategories, + profile: nlNLProfile, + categories: nlNLCategories }, 'ar-SA': { common: arSACommon, nutrition: arSANutrition, - categories: arSACategories, + profile: arSAProfile + categories: arSACategories }, }; @@ -74,7 +80,7 @@ i18nInstance // Default namespace defaultNS: 'common', - ns: ['common', 'nutrition', 'categories'], + ns: ['common', 'nutrition', 'categories', 'profile'], // Key separator keySeparator: '.', diff --git a/src/i18n/locales/ar-SA/profile.json b/src/i18n/locales/ar-SA/profile.json new file mode 100644 index 0000000..f6770ca --- /dev/null +++ b/src/i18n/locales/ar-SA/profile.json @@ -0,0 +1,24 @@ +{ + "stats": { + "followers": "متابعون", + "recipes": "وصفات", + "recipesShort": "وصفات", + "rating": "التقييم" + }, + "actions": { + "follow": "متابعة", + "following": "متابع", + "unfollow": "إلغاء المتابعة" + }, + "sections": { + "specialties": "التخصصات", + "achievements": "الإنجازات", + "socialLinks": "روابط التواصل" + }, + "badges": { + "verified": "ملف شخصي موثق" + }, + "socialLinks": { + "ariaLabel": "زيارة ملف {{platform}} الشخصي {{handle}}" + } +} \ No newline at end of file diff --git a/src/i18n/locales/en-US/profile.json b/src/i18n/locales/en-US/profile.json new file mode 100644 index 0000000..13add79 --- /dev/null +++ b/src/i18n/locales/en-US/profile.json @@ -0,0 +1,24 @@ +{ + "stats": { + "followers": "Followers", + "recipes": "Recipes", + "recipesShort": "recipes", + "rating": "Rating" + }, + "actions": { + "follow": "Follow", + "following": "Following", + "unfollow": "Unfollow" + }, + "sections": { + "specialties": "Specialties", + "achievements": "Achievements", + "socialLinks": "Social Links" + }, + "badges": { + "verified": "Verified Profile" + }, + "socialLinks": { + "ariaLabel": "Visit {{platform}} profile {{handle}}" + } +} \ No newline at end of file diff --git a/src/i18n/locales/nl-NL/profile.json b/src/i18n/locales/nl-NL/profile.json new file mode 100644 index 0000000..dcd7216 --- /dev/null +++ b/src/i18n/locales/nl-NL/profile.json @@ -0,0 +1,24 @@ +{ + "stats": { + "followers": "Volgers", + "recipes": "Recepten", + "recipesShort": "recepten", + "rating": "Beoordeling" + }, + "actions": { + "follow": "Volgen", + "following": "Volgt", + "unfollow": "Ontvolgen" + }, + "sections": { + "specialties": "Specialiteiten", + "achievements": "Prestaties", + "socialLinks": "Sociale Links" + }, + "badges": { + "verified": "Geverifieerd Profiel" + }, + "socialLinks": { + "ariaLabel": "Bezoek {{platform}} profiel {{handle}}" + } +} \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 9083f72..d79279d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,6 +1,11 @@ // TypeScript type definitions // Currently empty - types will be added as needed + +export type { Profile, ProfileStats, SocialLink } from './profile.types'; +// export type { Recipe } from './recipe.types'; + export type { Recipe, Category, CategoryCultural } from './recipe.types'; + // export type { User } from './user.types'; // export type { Collection } from './collection.types'; \ No newline at end of file diff --git a/src/types/profile.types.ts b/src/types/profile.types.ts new file mode 100644 index 0000000..ffb9218 --- /dev/null +++ b/src/types/profile.types.ts @@ -0,0 +1,28 @@ +// Profile and social media related type definitions + +export interface SocialLink { + platform: 'instagram' | 'twitter' | 'youtube' | 'tiktok' | 'facebook' | 'website'; + url: string; + handle?: string; +} + +export interface ProfileStats { + followers: number; + recipes: number; + avgRating: number; +} + +export interface Profile { + id: string; + name: string; + avatar: string; + title?: string; + bio: string; + stats: ProfileStats; + badges: string[]; + specialties: string[]; + location?: string; + socialLinks?: SocialLink[]; + isVerified?: boolean; + joinedDate?: string; +} \ No newline at end of file