This repository has been archived on 2026-02-06. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
serengo/src/lib/components/ProfilePictureSheet.svelte
Zias van Nes d3adac8acc 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.
2025-11-04 12:23:08 +01:00

256 lines
5.1 KiB
Svelte

<script lang="ts">
import { invalidateAll } from '$app/navigation';
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from './sheet';
import { Button } from './button';
import ProfilePicture from './ProfilePicture.svelte';
interface Props {
userId: string;
username: string;
profilePictureUrl?: string | null;
onClose?: () => void;
}
let { userId, username, profilePictureUrl, onClose }: Props = $props();
let fileInput: HTMLInputElement;
let selectedFile = $state<File | null>(null);
let isUploading = $state(false);
let isDeleting = $state(false);
let showModal = $state(true);
// Close modal when showModal changes to false
$effect(() => {
if (!showModal && onClose) {
onClose();
}
});
function handleFileSelect(e: Event) {
const target = e.target as HTMLInputElement;
if (target.files && target.files[0]) {
const file = target.files[0];
// Validate file type
if (!file.type.startsWith('image/')) {
alert('Please select an image file');
return;
}
// Validate file size (5MB max)
if (file.size > 5 * 1024 * 1024) {
alert('File size must be less than 5MB');
return;
}
selectedFile = file;
}
}
async function handleUpload() {
if (!selectedFile) return;
isUploading = true;
try {
const formData = new FormData();
formData.append('file', selectedFile);
formData.append('userId', userId);
const response = await fetch('/api/profile-picture/upload', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('Upload failed');
}
selectedFile = null;
if (fileInput) fileInput.value = '';
await invalidateAll();
showModal = false;
} catch (error) {
console.error('Upload error:', error);
alert('Failed to upload profile picture');
} finally {
isUploading = false;
}
}
async function handleDelete() {
if (!profilePictureUrl) return;
isDeleting = true;
try {
const response = await fetch('/api/profile-picture/delete', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ userId })
});
if (!response.ok) {
throw new Error('Delete failed');
}
await invalidateAll();
showModal = false;
} catch (error) {
console.error('Delete error:', error);
alert('Failed to delete profile picture');
} finally {
isDeleting = false;
}
}
</script>
<Sheet open={showModal} onOpenChange={(open) => (showModal = open)}>
<SheetContent side="bottom" class="profile-picture-sheet">
<SheetHeader>
<SheetTitle>Profile Picture</SheetTitle>
<SheetDescription>Upload, edit, or delete your profile picture</SheetDescription>
</SheetHeader>
<div class="profile-picture-content">
<div class="current-avatar">
<ProfilePicture {username} {profilePictureUrl} size="xl" class="large-avatar" />
</div>
<div class="upload-section">
<input
bind:this={fileInput}
type="file"
accept="image/*"
onchange={handleFileSelect}
class="file-input"
/>
{#if selectedFile}
<div class="selected-file">
<p class="file-name">{selectedFile.name}</p>
<p class="file-size">{(selectedFile.size / 1024 / 1024).toFixed(2)} MB</p>
</div>
{/if}
<div class="action-buttons">
{#if selectedFile}
<Button onclick={handleUpload} disabled={isUploading} class="upload-button">
{isUploading ? 'Uploading...' : 'Upload Picture'}
</Button>
{/if}
{#if profilePictureUrl}
<Button
variant="destructive"
onclick={handleDelete}
disabled={isDeleting}
class="delete-button"
>
{isDeleting ? 'Deleting...' : 'Delete Picture'}
</Button>
{/if}
</div>
</div>
</div>
</SheetContent>
</Sheet>
<style>
:global(.profile-picture-sheet) {
max-height: 80vh;
}
.profile-picture-content {
display: flex;
flex-direction: column;
gap: 24px;
padding: 16px 0;
}
.current-avatar {
display: flex;
justify-content: center;
align-items: center;
}
:global(.large-avatar) {
width: 120px;
height: 120px;
}
.upload-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.file-input {
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
background: white;
}
.selected-file {
padding: 12px;
background: #f5f5f5;
border-radius: 8px;
border: 1px solid #e0e0e0;
}
.file-name {
font-weight: 500;
margin: 0 0 4px 0;
word-break: break-word;
}
.file-size {
font-size: 12px;
color: #666;
margin: 0;
}
.action-buttons {
display: flex;
flex-direction: column;
gap: 12px;
}
:global(.upload-button) {
background: black;
color: white;
padding: 12px 24px;
font-weight: 500;
}
:global(.upload-button:hover) {
background: #333;
}
:global(.delete-button) {
padding: 12px 24px;
font-weight: 500;
}
@media (min-width: 640px) {
:global(.profile-picture-sheet) {
max-width: 500px;
margin: 0 auto;
}
.action-buttons {
flex-direction: row;
justify-content: center;
}
:global(.upload-button),
:global(.delete-button) {
min-width: 140px;
}
}
</style>