Users can now upload an avatar from Profile settings.
This commit is contained in:
@@ -10,6 +10,11 @@ import {
|
||||
} from 'lucide-react';
|
||||
import type { FormEvent } from 'react';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Avatar as ChatAvatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from '@/components/ui/avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
@@ -20,6 +25,7 @@ import {
|
||||
SheetTrigger,
|
||||
} from '@/components/ui/sheet';
|
||||
import { VideoPlayer } from '@/components/video-player';
|
||||
import { useInitials } from '@/hooks/use-initials';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { home, login } from '@/routes';
|
||||
import { follow, unfollow } from '@/routes/channels';
|
||||
@@ -278,6 +284,8 @@ function ChatPanel({
|
||||
onSubmit: (event: FormEvent) => void;
|
||||
compact?: boolean;
|
||||
}) {
|
||||
const getInitials = useInitials();
|
||||
|
||||
return (
|
||||
<div className="flex min-h-0 w-full flex-col">
|
||||
<div
|
||||
@@ -294,11 +302,28 @@ function ChatPanel({
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 space-y-3 overflow-y-auto p-4">
|
||||
{messages.map((message) => (
|
||||
<div key={message.id} className="text-sm">
|
||||
<span className="font-medium">{message.user.name}</span>
|
||||
<span className="ml-2 text-muted-foreground">
|
||||
{message.body}
|
||||
</span>
|
||||
<div
|
||||
key={message.id}
|
||||
className="flex min-w-0 gap-2 text-sm"
|
||||
>
|
||||
<ChatAvatar className="mt-0.5 size-7 rounded-md">
|
||||
<AvatarImage
|
||||
src={message.user.avatar_url ?? undefined}
|
||||
alt={message.user.name}
|
||||
className="object-cover"
|
||||
/>
|
||||
<AvatarFallback className="rounded-md bg-secondary text-[11px] font-semibold text-secondary-foreground">
|
||||
{getInitials(message.user.name)}
|
||||
</AvatarFallback>
|
||||
</ChatAvatar>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate font-medium">
|
||||
{message.user.name}
|
||||
</div>
|
||||
<div className="break-words text-muted-foreground">
|
||||
{message.body}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{messages.length === 0 && (
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { Form, Head, Link, usePage } from '@inertiajs/react';
|
||||
import { ImagePlus } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
|
||||
import DeleteUser from '@/components/delete-user';
|
||||
import Heading from '@/components/heading';
|
||||
import InputError from '@/components/input-error';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useInitials } from '@/hooks/use-initials';
|
||||
import { edit } from '@/routes/profile';
|
||||
import { send } from '@/routes/verification';
|
||||
|
||||
@@ -18,11 +22,34 @@ export default function Profile({
|
||||
}) {
|
||||
const { auth } = usePage().props;
|
||||
const user = auth.user;
|
||||
const getInitials = useInitials();
|
||||
const avatarInputRef = useRef<HTMLInputElement>(null);
|
||||
const [avatarPreviewUrl, setAvatarPreviewUrl] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (avatarPreviewUrl) {
|
||||
URL.revokeObjectURL(avatarPreviewUrl);
|
||||
}
|
||||
};
|
||||
}, [avatarPreviewUrl]);
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function previewAvatar(file: File | null) {
|
||||
setAvatarPreviewUrl((currentUrl) => {
|
||||
if (currentUrl) {
|
||||
URL.revokeObjectURL(currentUrl);
|
||||
}
|
||||
|
||||
return file ? URL.createObjectURL(file) : null;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title="Profile settings" />
|
||||
@@ -33,7 +60,7 @@ export default function Profile({
|
||||
<Heading
|
||||
variant="small"
|
||||
title="Profile information"
|
||||
description="Update your name and email address"
|
||||
description="Update your name, email address, and avatar"
|
||||
/>
|
||||
|
||||
<Form
|
||||
@@ -41,10 +68,66 @@ export default function Profile({
|
||||
options={{
|
||||
preserveScroll: true,
|
||||
}}
|
||||
resetOnSuccess={['avatar']}
|
||||
onSuccess={() => {
|
||||
setAvatarPreviewUrl(null);
|
||||
|
||||
if (avatarInputRef.current) {
|
||||
avatarInputRef.current.value = '';
|
||||
}
|
||||
}}
|
||||
encType="multipart/form-data"
|
||||
className="space-y-6"
|
||||
>
|
||||
{({ processing, errors }) => (
|
||||
{({ processing, progress, errors }) => (
|
||||
<>
|
||||
<div className="flex flex-col gap-4 rounded-md border bg-card p-4 sm:flex-row sm:items-center">
|
||||
<Avatar className="size-20 rounded-md">
|
||||
<AvatarImage
|
||||
src={
|
||||
avatarPreviewUrl ??
|
||||
user.avatar ??
|
||||
undefined
|
||||
}
|
||||
alt={user.name}
|
||||
className="object-cover"
|
||||
/>
|
||||
<AvatarFallback className="rounded-md bg-secondary text-lg font-semibold text-secondary-foreground">
|
||||
{getInitials(user.name)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<div className="grid min-w-0 flex-1 gap-2">
|
||||
<Label htmlFor="avatar">Avatar</Label>
|
||||
<Input
|
||||
id="avatar"
|
||||
ref={avatarInputRef}
|
||||
name="avatar"
|
||||
type="file"
|
||||
accept="image/jpeg,image/png,image/webp"
|
||||
onChange={(event) =>
|
||||
previewAvatar(
|
||||
event.target.files?.[0] ?? null,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
<ImagePlus className="size-3.5" />
|
||||
<span>
|
||||
JPG, PNG, or WebP up to 2 MB.
|
||||
</span>
|
||||
</div>
|
||||
{progress && (
|
||||
<progress
|
||||
value={progress.percentage}
|
||||
max="100"
|
||||
className="h-1.5 w-full"
|
||||
/>
|
||||
)}
|
||||
<InputError message={errors.avatar} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user