diff --git a/next.config.js b/next.config.js index 04360ac..81477bc 100644 --- a/next.config.js +++ b/next.config.js @@ -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; diff --git a/package.json b/package.json index 8449215..276cc78 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 77c2976..d9b12ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/scripts/next.config.build.js b/scripts/next.config.build.js index 9a60dcc..8202d4b 100644 --- a/scripts/next.config.build.js +++ b/scripts/next.config.build.js @@ -15,6 +15,11 @@ const config = { }, ], }, + experimental: { + serverActions: { + bodySizeLimit: '10mb', + }, + }, typescript: { ignoreBuildErrors: true, }, diff --git a/scripts/next.config.default.js b/scripts/next.config.default.js index 433bd5b..81477bc 100644 --- a/scripts/next.config.default.js +++ b/scripts/next.config.default.js @@ -15,6 +15,11 @@ const config = { }, ], }, + experimental: { + serverActions: { + bodySizeLimit: '10mb', + }, + }, }; export default config; diff --git a/src/app/(auth-pages)/profile/page.tsx b/src/app/(auth-pages)/profile/page.tsx index fb27d46..c00c0fb 100644 --- a/src/app/(auth-pages)/profile/page.tsx +++ b/src/app/(auth-pages)/profile/page.tsx @@ -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(undefined); + const [avatarUrl, setAvatarUrl] = useState(undefined); const [isLoading, setIsLoading] = useState(true); const [isUploading, setIsUploading] = useState(false); const fileInputRef = useRef(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) => { 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) => { 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 ( +
+

Unauthorized

+
+ ); + return ( -
- - ( - - Full Name - - - - - Your public display name. - - - - )} - /> - ( - - Email - - - - - Your email address associated with your account. - - - - )} - /> - - - + + + + {profile?.full_name ?? 'Profile'} + + + Manage your personal information & how it appears to others. + + + + {isLoading && !profile ? ( +
+
+
+
+
+
+ ) : ( +
+
+
+ + {avatarUrl ? ( + + ) : ( + + {profile?.full_name + ? profile.full_name.split(' ').map(n => n[0]).join('').toUpperCase() + : } + + )} + +
+ +
+ +
+ {isUploading && ( +
Uploading...
+ )} +

+ Click on the avatar to upload a new image +

+
+ +
+ + ( + + Full Name + + + + + Your public display name. + + + + )} + /> + ( + + Email + + + + + Your email address associated with your account. + + + + )} + /> +
+ +
+ + +
+ )} +
+
); }; export default ProfilePage; diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..d05bbc6 --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/src/components/ui/index.tsx b/src/components/ui/index.tsx index acf9622..d39fd38 100644 --- a/src/components/ui/index.tsx +++ b/src/components/ui/index.tsx @@ -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'; diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx new file mode 100644 index 0000000..67c73e5 --- /dev/null +++ b/src/components/ui/separator.tsx @@ -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) { + return ( + + ) +} + +export { Separator }