add:ProfilePicture component and replace Avatar
Move avatar initials, fallback styling, and loading/loading-state UI into ProfilePicture and update components to use it. Export ProfilePicture from the lib index.
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
import { Badge } from '$lib/components/badge';
|
import { Badge } from '$lib/components/badge';
|
||||||
import LikeButton from '$lib/components/LikeButton.svelte';
|
import LikeButton from '$lib/components/LikeButton.svelte';
|
||||||
import VideoPlayer from '$lib/components/VideoPlayer.svelte';
|
import VideoPlayer from '$lib/components/VideoPlayer.svelte';
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '$lib/components/avatar';
|
import ProfilePicture from '$lib/components/ProfilePicture.svelte';
|
||||||
import { MoreHorizontal, MessageCircle, Share } from '@lucide/svelte';
|
import { MoreHorizontal, MessageCircle, Share } from '@lucide/svelte';
|
||||||
|
|
||||||
interface FindCardProps {
|
interface FindCardProps {
|
||||||
@@ -42,24 +42,17 @@
|
|||||||
function handleExplore() {
|
function handleExplore() {
|
||||||
onExplore?.(id);
|
onExplore?.(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUserInitials(username: string): string {
|
|
||||||
return username.slice(0, 2).toUpperCase();
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="find-card">
|
<div class="find-card">
|
||||||
<!-- Post Header -->
|
<!-- Post Header -->
|
||||||
<div class="post-header">
|
<div class="post-header">
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<Avatar class="avatar">
|
<ProfilePicture
|
||||||
{#if user.profilePictureUrl}
|
username={user.username}
|
||||||
<AvatarImage src={user.profilePictureUrl} alt={user.username} />
|
profilePictureUrl={user.profilePictureUrl}
|
||||||
{/if}
|
class="avatar"
|
||||||
<AvatarFallback class="avatar-fallback">
|
/>
|
||||||
{getUserInitials(user.username)}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div class="user-details">
|
<div class="user-details">
|
||||||
<div class="username">@{user.username}</div>
|
<div class="username">@{user.username}</div>
|
||||||
{#if locationName}
|
{#if locationName}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '$lib/components/sheet';
|
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '$lib/components/sheet';
|
||||||
import LikeButton from '$lib/components/LikeButton.svelte';
|
import LikeButton from '$lib/components/LikeButton.svelte';
|
||||||
import VideoPlayer from '$lib/components/VideoPlayer.svelte';
|
import VideoPlayer from '$lib/components/VideoPlayer.svelte';
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '$lib/components/avatar';
|
import ProfilePicture from '$lib/components/ProfilePicture.svelte';
|
||||||
|
|
||||||
interface Find {
|
interface Find {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -112,14 +112,11 @@
|
|||||||
<SheetContent side={isMobile ? 'bottom' : 'right'} class="sheet-content">
|
<SheetContent side={isMobile ? 'bottom' : 'right'} class="sheet-content">
|
||||||
<SheetHeader class="sheet-header">
|
<SheetHeader class="sheet-header">
|
||||||
<div class="user-section">
|
<div class="user-section">
|
||||||
<Avatar class="user-avatar">
|
<ProfilePicture
|
||||||
{#if find.user.profilePictureUrl}
|
username={find.user.username}
|
||||||
<AvatarImage src={find.user.profilePictureUrl} alt={find.user.username} />
|
profilePictureUrl={find.user.profilePictureUrl}
|
||||||
{/if}
|
class="user-avatar"
|
||||||
<AvatarFallback class="avatar-fallback">
|
/>
|
||||||
{find.user.username.slice(0, 2).toUpperCase()}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<SheetTitle class="find-title">{find.title}</SheetTitle>
|
<SheetTitle class="find-title">{find.title}</SheetTitle>
|
||||||
<div class="find-meta">
|
<div class="find-meta">
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from './dropdown-menu';
|
} from './dropdown-menu';
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from './avatar';
|
|
||||||
import { Skeleton } from './skeleton';
|
import { Skeleton } from './skeleton';
|
||||||
|
import ProfilePicture from './ProfilePicture.svelte';
|
||||||
import ProfilePictureSheet from './ProfilePictureSheet.svelte';
|
import ProfilePictureSheet from './ProfilePictureSheet.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -22,9 +22,6 @@
|
|||||||
|
|
||||||
let showProfilePictureSheet = $state(false);
|
let showProfilePictureSheet = $state(false);
|
||||||
|
|
||||||
// Get the first letter of username for avatar
|
|
||||||
const initial = username.charAt(0).toUpperCase();
|
|
||||||
|
|
||||||
function openProfilePictureSheet() {
|
function openProfilePictureSheet() {
|
||||||
showProfilePictureSheet = true;
|
showProfilePictureSheet = true;
|
||||||
}
|
}
|
||||||
@@ -36,18 +33,7 @@
|
|||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger class="profile-trigger">
|
<DropdownMenuTrigger class="profile-trigger">
|
||||||
{#if loading}
|
<ProfilePicture {username} {profilePictureUrl} {loading} class="profile-avatar" />
|
||||||
<Skeleton class="h-10 w-10 rounded-full" />
|
|
||||||
{:else}
|
|
||||||
<Avatar class="profile-avatar">
|
|
||||||
{#if profilePictureUrl}
|
|
||||||
<AvatarImage src={profilePictureUrl} alt={username} />
|
|
||||||
{/if}
|
|
||||||
<AvatarFallback class="profile-avatar-fallback">
|
|
||||||
{initial}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
{/if}
|
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
<DropdownMenuContent align="end" class="profile-dropdown-content">
|
<DropdownMenuContent align="end" class="profile-dropdown-content">
|
||||||
@@ -144,15 +130,6 @@
|
|||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.profile-avatar-fallback) {
|
|
||||||
background: black;
|
|
||||||
color: white;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 16px;
|
|
||||||
border: 2px solid #fff;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.profile-dropdown-content) {
|
:global(.profile-dropdown-content) {
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
max-width: 240px;
|
max-width: 240px;
|
||||||
|
|||||||
59
src/lib/components/ProfilePicture.svelte
Normal file
59
src/lib/components/ProfilePicture.svelte
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from './avatar';
|
||||||
|
import { cn } from '$lib/utils';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
username: string;
|
||||||
|
profilePictureUrl?: string | null;
|
||||||
|
size?: 'sm' | 'md' | 'lg' | 'xl';
|
||||||
|
class?: string;
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
username,
|
||||||
|
profilePictureUrl,
|
||||||
|
size = 'md',
|
||||||
|
class: className,
|
||||||
|
loading = false
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
const initial = username.charAt(0).toUpperCase();
|
||||||
|
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: 'h-8 w-8',
|
||||||
|
md: 'h-10 w-10',
|
||||||
|
lg: 'h-16 w-16',
|
||||||
|
xl: 'h-24 w-24'
|
||||||
|
};
|
||||||
|
|
||||||
|
const fallbackSizeClasses = {
|
||||||
|
sm: 'text-xs',
|
||||||
|
md: 'text-base',
|
||||||
|
lg: 'text-xl',
|
||||||
|
xl: 'text-3xl'
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<div class={cn('animate-pulse rounded-full bg-gray-200', sizeClasses[size], className)}></div>
|
||||||
|
{:else}
|
||||||
|
<Avatar class={cn(sizeClasses[size], className)}>
|
||||||
|
{#if profilePictureUrl}
|
||||||
|
<AvatarImage src={profilePictureUrl} alt={username} />
|
||||||
|
{/if}
|
||||||
|
<AvatarFallback class={cn('profile-picture-fallback', fallbackSizeClasses[size])}>
|
||||||
|
{initial}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(.profile-picture-fallback) {
|
||||||
|
background: black;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import { invalidateAll } from '$app/navigation';
|
import { invalidateAll } from '$app/navigation';
|
||||||
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from './sheet';
|
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from './sheet';
|
||||||
import { Button } from './button';
|
import { Button } from './button';
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from './avatar';
|
import ProfilePicture from './ProfilePicture.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -19,8 +19,6 @@
|
|||||||
let isDeleting = $state(false);
|
let isDeleting = $state(false);
|
||||||
let showModal = $state(true);
|
let showModal = $state(true);
|
||||||
|
|
||||||
const initial = username.charAt(0).toUpperCase();
|
|
||||||
|
|
||||||
// Close modal when showModal changes to false
|
// Close modal when showModal changes to false
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!showModal && onClose) {
|
if (!showModal && onClose) {
|
||||||
@@ -118,14 +116,7 @@
|
|||||||
|
|
||||||
<div class="profile-picture-content">
|
<div class="profile-picture-content">
|
||||||
<div class="current-avatar">
|
<div class="current-avatar">
|
||||||
<Avatar class="large-avatar">
|
<ProfilePicture {username} {profilePictureUrl} size="xl" class="large-avatar" />
|
||||||
{#if profilePictureUrl}
|
|
||||||
<AvatarImage src={profilePictureUrl} alt={username} />
|
|
||||||
{/if}
|
|
||||||
<AvatarFallback class="large-avatar-fallback">
|
|
||||||
{initial}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="upload-section">
|
<div class="upload-section">
|
||||||
@@ -190,15 +181,6 @@
|
|||||||
height: 120px;
|
height: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.large-avatar-fallback) {
|
|
||||||
background: black;
|
|
||||||
color: white;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 48px;
|
|
||||||
border: 4px solid #fff;
|
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-section {
|
.upload-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export { default as Input } from './components/Input.svelte';
|
|||||||
export { default as Button } from './components/Button.svelte';
|
export { default as Button } from './components/Button.svelte';
|
||||||
export { default as ErrorMessage } from './components/ErrorMessage.svelte';
|
export { default as ErrorMessage } from './components/ErrorMessage.svelte';
|
||||||
export { default as ProfilePanel } from './components/ProfilePanel.svelte';
|
export { default as ProfilePanel } from './components/ProfilePanel.svelte';
|
||||||
|
export { default as ProfilePicture } from './components/ProfilePicture.svelte';
|
||||||
export { default as ProfilePictureSheet } from './components/ProfilePictureSheet.svelte';
|
export { default as ProfilePictureSheet } from './components/ProfilePictureSheet.svelte';
|
||||||
export { default as Header } from './components/Header.svelte';
|
export { default as Header } from './components/Header.svelte';
|
||||||
export { default as Modal } from './components/Modal.svelte';
|
export { default as Modal } from './components/Modal.svelte';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/card';
|
||||||
import { Button } from '$lib/components/button';
|
import { Button } from '$lib/components/button';
|
||||||
import { Input } from '$lib/components/input';
|
import { Input } from '$lib/components/input';
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '$lib/components/avatar';
|
import ProfilePicture from '$lib/components/ProfilePicture.svelte';
|
||||||
import { Badge } from '$lib/components/badge';
|
import { Badge } from '$lib/components/badge';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
@@ -184,10 +184,10 @@
|
|||||||
<div class="user-grid">
|
<div class="user-grid">
|
||||||
{#each friends as friend (friend.id)}
|
{#each friends as friend (friend.id)}
|
||||||
<div class="user-card">
|
<div class="user-card">
|
||||||
<Avatar>
|
<ProfilePicture
|
||||||
<AvatarImage src={friend.friendProfilePictureUrl} alt={friend.friendUsername} />
|
username={friend.friendUsername}
|
||||||
<AvatarFallback>{friend.friendUsername.charAt(0).toUpperCase()}</AvatarFallback>
|
profilePictureUrl={friend.friendProfilePictureUrl}
|
||||||
</Avatar>
|
/>
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<span class="username">{friend.friendUsername}</span>
|
<span class="username">{friend.friendUsername}</span>
|
||||||
<Badge variant="secondary">Friend</Badge>
|
<Badge variant="secondary">Friend</Badge>
|
||||||
@@ -218,15 +218,10 @@
|
|||||||
<div class="user-grid">
|
<div class="user-grid">
|
||||||
{#each receivedRequests as request (request.id)}
|
{#each receivedRequests as request (request.id)}
|
||||||
<div class="user-card">
|
<div class="user-card">
|
||||||
<Avatar>
|
<ProfilePicture
|
||||||
<AvatarImage
|
username={request.friendUsername}
|
||||||
src={request.friendProfilePictureUrl}
|
profilePictureUrl={request.friendProfilePictureUrl}
|
||||||
alt={request.friendUsername}
|
|
||||||
/>
|
/>
|
||||||
<AvatarFallback
|
|
||||||
>{request.friendUsername.charAt(0).toUpperCase()}</AvatarFallback
|
|
||||||
>
|
|
||||||
</Avatar>
|
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<span class="username">{request.friendUsername}</span>
|
<span class="username">{request.friendUsername}</span>
|
||||||
<Badge variant="outline">Pending</Badge>
|
<Badge variant="outline">Pending</Badge>
|
||||||
@@ -264,15 +259,10 @@
|
|||||||
<div class="user-grid">
|
<div class="user-grid">
|
||||||
{#each sentRequests as request (request.id)}
|
{#each sentRequests as request (request.id)}
|
||||||
<div class="user-card">
|
<div class="user-card">
|
||||||
<Avatar>
|
<ProfilePicture
|
||||||
<AvatarImage
|
username={request.friendUsername}
|
||||||
src={request.friendProfilePictureUrl}
|
profilePictureUrl={request.friendProfilePictureUrl}
|
||||||
alt={request.friendUsername}
|
|
||||||
/>
|
/>
|
||||||
<AvatarFallback
|
|
||||||
>{request.friendUsername.charAt(0).toUpperCase()}</AvatarFallback
|
|
||||||
>
|
|
||||||
</Avatar>
|
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<span class="username">{request.friendUsername}</span>
|
<span class="username">{request.friendUsername}</span>
|
||||||
<Badge variant="secondary">Sent</Badge>
|
<Badge variant="secondary">Sent</Badge>
|
||||||
@@ -305,10 +295,10 @@
|
|||||||
<div class="user-grid">
|
<div class="user-grid">
|
||||||
{#each searchResults as user (user.id)}
|
{#each searchResults as user (user.id)}
|
||||||
<div class="user-card">
|
<div class="user-card">
|
||||||
<Avatar>
|
<ProfilePicture
|
||||||
<AvatarImage src={user.profilePictureUrl} alt={user.username} />
|
username={user.username}
|
||||||
<AvatarFallback>{user.username.charAt(0).toUpperCase()}</AvatarFallback>
|
profilePictureUrl={user.profilePictureUrl}
|
||||||
</Avatar>
|
/>
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<span class="username">{user.username}</span>
|
<span class="username">{user.username}</span>
|
||||||
{#if user.friendshipStatus === 'accepted'}
|
{#if user.friendshipStatus === 'accepted'}
|
||||||
|
|||||||
Reference in New Issue
Block a user