Work on profiles page
This commit is contained in:
parent
d47ed16700
commit
3dffa71a89
@ -6,6 +6,7 @@ import './src/env.js';
|
||||
|
||||
/** @type {import("next").NextConfig} */
|
||||
const config = {
|
||||
output: 'standalone',
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
@ -14,6 +15,11 @@ const config = {
|
||||
},
|
||||
],
|
||||
},
|
||||
experimental: {
|
||||
serverActions: {
|
||||
bodySizeLimit: '10mb',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
@ -21,9 +21,10 @@
|
||||
"@radix-ui/react-checkbox": "^1.3.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.14",
|
||||
"@radix-ui/react-label": "^2.1.6",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.2",
|
||||
"@supabase/ssr": "^0.6.1",
|
||||
"@supabase/supabase-js": "^2.49.5",
|
||||
"@supabase/supabase-js": "^2.49.6",
|
||||
"@t3-oss/env-nextjs": "^0.12.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@ -34,7 +35,7 @@
|
||||
"react-dom": "^19.1.0",
|
||||
"react-hook-form": "^7.56.4",
|
||||
"sonner": "^2.0.3",
|
||||
"zod": "^3.25.3"
|
||||
"zod": "^3.25.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
|
117
pnpm-lock.yaml
generated
117
pnpm-lock.yaml
generated
@ -23,18 +23,21 @@ importers:
|
||||
'@radix-ui/react-label':
|
||||
specifier: ^2.1.6
|
||||
version: 2.1.6(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-separator':
|
||||
specifier: ^1.1.7
|
||||
version: 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-slot':
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.2(@types/react@19.1.4)(react@19.1.0)
|
||||
'@supabase/ssr':
|
||||
specifier: ^0.6.1
|
||||
version: 0.6.1(@supabase/supabase-js@2.49.5)
|
||||
version: 0.6.1(@supabase/supabase-js@2.49.6)
|
||||
'@supabase/supabase-js':
|
||||
specifier: ^2.49.5
|
||||
version: 2.49.5
|
||||
specifier: ^2.49.6
|
||||
version: 2.49.6
|
||||
'@t3-oss/env-nextjs':
|
||||
specifier: ^0.12.0
|
||||
version: 0.12.0(typescript@5.8.3)(zod@3.25.3)
|
||||
version: 0.12.0(typescript@5.8.3)(zod@3.25.7)
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
@ -63,8 +66,8 @@ importers:
|
||||
specifier: ^2.0.3
|
||||
version: 2.0.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
zod:
|
||||
specifier: ^3.25.3
|
||||
version: 3.25.3
|
||||
specifier: ^3.25.7
|
||||
version: 3.25.7
|
||||
devDependencies:
|
||||
'@eslint/eslintrc':
|
||||
specifier: ^3.3.1
|
||||
@ -634,6 +637,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-primitive@2.1.3':
|
||||
resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-roving-focus@1.1.9':
|
||||
resolution: {integrity: sha512-ZzrIFnMYHHCNqSNCsuN6l7wlewBEq0O0BCSBkabJMFXVO51LRUTq71gLP1UxFvmrXElqmPjA5VX7IqC9VpazAQ==}
|
||||
peerDependencies:
|
||||
@ -647,6 +663,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-separator@1.1.7':
|
||||
resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-slot@1.2.2':
|
||||
resolution: {integrity: sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==}
|
||||
peerDependencies:
|
||||
@ -656,6 +685,15 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-slot@1.2.3':
|
||||
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-callback-ref@1.1.1':
|
||||
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
|
||||
peerDependencies:
|
||||
@ -762,8 +800,8 @@ packages:
|
||||
'@supabase/postgrest-js@1.19.4':
|
||||
resolution: {integrity: sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==}
|
||||
|
||||
'@supabase/realtime-js@2.11.2':
|
||||
resolution: {integrity: sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==}
|
||||
'@supabase/realtime-js@2.11.8':
|
||||
resolution: {integrity: sha512-H0VASfG3FGkoPL56AWs0z9Gu0w8luESwzlyvEKZgC9Aqxz16YAfkId+lcMYxuYMXF/WAhLc4lSbunB2+s3rkHg==}
|
||||
|
||||
'@supabase/ssr@0.6.1':
|
||||
resolution: {integrity: sha512-QtQgEMvaDzr77Mk3vZ3jWg2/y+D8tExYF7vcJT+wQ8ysuvOeGGjYbZlvj5bHYsj/SpC0bihcisnwPrM4Gp5G4g==}
|
||||
@ -773,8 +811,8 @@ packages:
|
||||
'@supabase/storage-js@2.7.1':
|
||||
resolution: {integrity: sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==}
|
||||
|
||||
'@supabase/supabase-js@2.49.5':
|
||||
resolution: {integrity: sha512-LfsQLK1WVDzkYQvS3rmaLDPpZdN75GPYxPJd7uJOS8jrV2Kchk5qzvhvmofRPb6esQhw+YYfAYDInURtPUupLQ==}
|
||||
'@supabase/supabase-js@2.49.6':
|
||||
resolution: {integrity: sha512-ErMJ+AJNp0s/0nh8oPl4Oi5l4WAj1CzMlgVqugarJYsXMNlv03V2/J8hEYTBj5xrNEbWw6/4XnHU6mHvxIXc2w==}
|
||||
|
||||
'@swc/counter@0.1.3':
|
||||
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
|
||||
@ -2291,8 +2329,8 @@ packages:
|
||||
tailwindcss@4.1.7:
|
||||
resolution: {integrity: sha512-kr1o/ErIdNhTz8uzAYL7TpaUuzKIE6QPQ4qmSdxnoX/lo+5wmUHQA6h3L5yIqEImSRnAAURDirLu/BgiXGPAhg==}
|
||||
|
||||
tapable@2.2.1:
|
||||
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
|
||||
tapable@2.2.2:
|
||||
resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tar@7.4.3:
|
||||
@ -2446,8 +2484,8 @@ packages:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
zod@3.25.3:
|
||||
resolution: {integrity: sha512-VGZqnyYNrl8JpEJRZaFPqeVNIuqgXNu4cXZ5cOb6zEUO1OxKbRnWB4UdDIXMmiERWncs0yDQukssHov8JUxykQ==}
|
||||
zod@3.25.7:
|
||||
resolution: {integrity: sha512-YGdT1cVRmKkOg6Sq7vY7IkxdphySKnXhaUmFI4r4FcuFVNgpCb9tZfNwXbT6BPjD5oz0nubFsoo9pIqKrDcCvg==}
|
||||
|
||||
snapshots:
|
||||
|
||||
@ -2907,6 +2945,15 @@ snapshots:
|
||||
'@types/react': 19.1.4
|
||||
'@types/react-dom': 19.1.5(@types/react@19.1.4)
|
||||
|
||||
'@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-slot': 1.2.3(@types/react@19.1.4)(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.4
|
||||
'@types/react-dom': 19.1.5(@types/react@19.1.4)
|
||||
|
||||
'@radix-ui/react-roving-focus@1.1.9(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.2
|
||||
@ -2924,6 +2971,15 @@ snapshots:
|
||||
'@types/react': 19.1.4
|
||||
'@types/react-dom': 19.1.5(@types/react@19.1.4)
|
||||
|
||||
'@radix-ui/react-separator@1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.4
|
||||
'@types/react-dom': 19.1.5(@types/react@19.1.4)
|
||||
|
||||
'@radix-ui/react-slot@1.2.2(@types/react@19.1.4)(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.0)
|
||||
@ -2931,6 +2987,13 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.4
|
||||
|
||||
'@radix-ui/react-slot@1.2.3(@types/react@19.1.4)(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.0)
|
||||
react: 19.1.0
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.4
|
||||
|
||||
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.4)(react@19.1.0)':
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
@ -3016,7 +3079,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
|
||||
'@supabase/realtime-js@2.11.2':
|
||||
'@supabase/realtime-js@2.11.8':
|
||||
dependencies:
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
'@types/phoenix': 1.6.6
|
||||
@ -3026,22 +3089,22 @@ snapshots:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
'@supabase/ssr@0.6.1(@supabase/supabase-js@2.49.5)':
|
||||
'@supabase/ssr@0.6.1(@supabase/supabase-js@2.49.6)':
|
||||
dependencies:
|
||||
'@supabase/supabase-js': 2.49.5
|
||||
'@supabase/supabase-js': 2.49.6
|
||||
cookie: 1.0.2
|
||||
|
||||
'@supabase/storage-js@2.7.1':
|
||||
dependencies:
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
|
||||
'@supabase/supabase-js@2.49.5':
|
||||
'@supabase/supabase-js@2.49.6':
|
||||
dependencies:
|
||||
'@supabase/auth-js': 2.69.1
|
||||
'@supabase/functions-js': 2.4.4
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
'@supabase/postgrest-js': 1.19.4
|
||||
'@supabase/realtime-js': 2.11.2
|
||||
'@supabase/realtime-js': 2.11.8
|
||||
'@supabase/storage-js': 2.7.1
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
@ -3053,17 +3116,17 @@ snapshots:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@t3-oss/env-core@0.12.0(typescript@5.8.3)(zod@3.25.3)':
|
||||
'@t3-oss/env-core@0.12.0(typescript@5.8.3)(zod@3.25.7)':
|
||||
optionalDependencies:
|
||||
typescript: 5.8.3
|
||||
zod: 3.25.3
|
||||
zod: 3.25.7
|
||||
|
||||
'@t3-oss/env-nextjs@0.12.0(typescript@5.8.3)(zod@3.25.3)':
|
||||
'@t3-oss/env-nextjs@0.12.0(typescript@5.8.3)(zod@3.25.7)':
|
||||
dependencies:
|
||||
'@t3-oss/env-core': 0.12.0(typescript@5.8.3)(zod@3.25.3)
|
||||
'@t3-oss/env-core': 0.12.0(typescript@5.8.3)(zod@3.25.7)
|
||||
optionalDependencies:
|
||||
typescript: 5.8.3
|
||||
zod: 3.25.3
|
||||
zod: 3.25.7
|
||||
|
||||
'@tailwindcss/node@4.1.7':
|
||||
dependencies:
|
||||
@ -3544,7 +3607,7 @@ snapshots:
|
||||
enhanced-resolve@5.18.1:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
tapable: 2.2.1
|
||||
tapable: 2.2.2
|
||||
|
||||
es-abstract@1.23.9:
|
||||
dependencies:
|
||||
@ -4686,7 +4749,7 @@ snapshots:
|
||||
|
||||
tailwindcss@4.1.7: {}
|
||||
|
||||
tapable@2.2.1: {}
|
||||
tapable@2.2.2: {}
|
||||
|
||||
tar@7.4.3:
|
||||
dependencies:
|
||||
@ -4886,4 +4949,4 @@ snapshots:
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
zod@3.25.3: {}
|
||||
zod@3.25.7: {}
|
||||
|
@ -15,6 +15,11 @@ const config = {
|
||||
},
|
||||
],
|
||||
},
|
||||
experimental: {
|
||||
serverActions: {
|
||||
bodySizeLimit: '10mb',
|
||||
},
|
||||
},
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
|
@ -15,6 +15,11 @@ const config = {
|
||||
},
|
||||
],
|
||||
},
|
||||
experimental: {
|
||||
serverActions: {
|
||||
bodySizeLimit: '10mb',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
@ -2,11 +2,19 @@
|
||||
import { z } from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { getProfile, updateProfile, uploadFile } from '@/lib/actions';
|
||||
import { getProfile, getSignedUrl, updateProfile, uploadFile } from '@/lib/actions';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import type { Profile } from '@/utils/supabase';
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
@ -15,6 +23,7 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
Separator,
|
||||
} from '@/components/ui';
|
||||
import { toast } from 'sonner';
|
||||
import { Pencil, User } from 'lucide-react'
|
||||
@ -28,6 +37,7 @@ const formSchema = z.object({
|
||||
|
||||
const ProfilePage = () => {
|
||||
const [profile, setProfile] = useState<Profile | undefined>(undefined);
|
||||
const [avatarUrl, setAvatarUrl] = useState<string | undefined>(undefined);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
@ -54,8 +64,7 @@ const ProfilePage = () => {
|
||||
email: profileResponse.data.email ?? '',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting profile:', error);
|
||||
toast.error('Failed to load profile data');
|
||||
setProfile(undefined);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@ -65,20 +74,81 @@ const ProfilePage = () => {
|
||||
});
|
||||
}, [form]);
|
||||
|
||||
useEffect(() => {
|
||||
const getAvatarUrl = async () => {
|
||||
if (profile?.avatar_url) {
|
||||
try {
|
||||
const response = await getSignedUrl({
|
||||
bucket: 'avatars',
|
||||
url: profile.avatar_url,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setAvatarUrl(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting signed URL:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
getAvatarUrl().catch((error) => {
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to get signed avatar url.');
|
||||
});
|
||||
}, [profile]);
|
||||
|
||||
const handleAvatarClick = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
if (!file)
|
||||
throw new Error('No file selected');
|
||||
|
||||
try {
|
||||
setIsUploading(true);
|
||||
|
||||
const fileExt = file.name.split('.').pop();
|
||||
const fileName = `${Date.now()}-${profile?.id ?? Math.random().toString(36).substring(2,15)}.${fileExt}`;
|
||||
|
||||
const uploadResult = await uploadFile({
|
||||
bucket: 'avatars',
|
||||
path: fileName,
|
||||
file,
|
||||
options: {
|
||||
upsert: true,
|
||||
contentType: file.type,
|
||||
},
|
||||
});
|
||||
if (!uploadResult.success)
|
||||
throw new Error(uploadResult.error ?? 'Failed to upload avatar');
|
||||
|
||||
const updateResult = await updateProfile({
|
||||
avatar_url: uploadResult.data,
|
||||
});
|
||||
|
||||
if (!updateResult.success)
|
||||
throw new Error(updateResult.error ?? 'Failed to update profile');
|
||||
|
||||
setProfile(updateResult.data);
|
||||
toast.success('Avatar updated successfully.')
|
||||
|
||||
} catch (error) {
|
||||
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to uploaad avatar.');
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const getInitials = (name: string) => {
|
||||
return name
|
||||
.split(' ')
|
||||
.map((n) => n[0])
|
||||
.join('')
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
@ -93,53 +163,120 @@ const ProfilePage = () => {
|
||||
setProfile(result.data);
|
||||
toast.success('Profile updated successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error updating profile: ', error);
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to update profile.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (profile === undefined)
|
||||
return (
|
||||
<div className='flex p-5 items-center justify-center'>
|
||||
<h1>Unauthorized</h1>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form { ...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className='space-y-8'
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='full_name'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Full Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='email'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Your email address associated with your account.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type='submit'>Save</Button>
|
||||
</form>
|
||||
</Form>
|
||||
<Card className='p-8'>
|
||||
<CardHeader className='pb-2'>
|
||||
<CardTitle className='text-2xl'>
|
||||
{profile?.full_name ?? 'Profile'}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your personal information & how it appears to others.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading && !profile ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="animate-pulse text-center">
|
||||
<div className="h-24 w-24 rounded-full bg-gray-200 mx-auto mb-4"></div>
|
||||
<div className="h-4 w-48 bg-gray-200 mx-auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-8">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="relative group cursor-pointer mb-4" onClick={handleAvatarClick}>
|
||||
<Avatar className="h-32 w-32">
|
||||
{avatarUrl ? (
|
||||
<AvatarImage src={avatarUrl} alt={profile.full_name ?? 'User'} />
|
||||
) : (
|
||||
<AvatarFallback className="text-2xl">
|
||||
{profile?.full_name
|
||||
? profile.full_name.split(' ').map(n => n[0]).join('').toUpperCase()
|
||||
: <User size={32} />}
|
||||
</AvatarFallback>
|
||||
)}
|
||||
</Avatar>
|
||||
<div className="absolute inset-0 rounded-full bg-black/0 group-hover:bg-black/50 transition-all flex items-center justify-center">
|
||||
<Pencil className="text-white opacity-0 group-hover:opacity-100 transition-opacity" size={24} />
|
||||
</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handleFileChange}
|
||||
disabled={isUploading}
|
||||
/>
|
||||
</div>
|
||||
{isUploading && (
|
||||
<div className="text-sm text-gray-500">Uploading...</div>
|
||||
)}
|
||||
<p className="text-sm text-gray-500 mt-2">
|
||||
Click on the avatar to upload a new image
|
||||
</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<Form { ...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className='space-y-6'
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='full_name'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Full Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='email'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Your email address associated with your account.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-center">
|
||||
<Button type='submit' disabled={isLoading}>
|
||||
{isLoading ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
export default ProfilePage;
|
||||
|
92
src/components/ui/card.tsx
Normal file
92
src/components/ui/card.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
export * from '@/components/ui/avatar';
|
||||
export * from '@/components/ui/badge';
|
||||
export * from '@/components/ui/button';
|
||||
export * from '@/components/ui/card';
|
||||
export * from '@/components/ui/checkbox';
|
||||
export * from '@/components/ui/dropdown-menu';
|
||||
export * from '@/components/ui/form';
|
||||
export * from '@/components/ui/input';
|
||||
export * from '@/components/ui/label';
|
||||
export * from '@/components/ui/separator';
|
||||
export * from '@/components/ui/sonner';
|
||||
|
28
src/components/ui/separator.tsx
Normal file
28
src/components/ui/separator.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator-root"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Separator }
|
Loading…
x
Reference in New Issue
Block a user