@@ -13,6 +13,9 @@ import {
1313import { useTranslations } from 'next-intl' ;
1414import { useState } from 'react' ;
1515
16+ import { toast } from 'sonner' ;
17+
18+ import { updateName , updatePassword } from '@/actions/profile' ;
1619import { UserAvatar } from '@/components/leaderboard/UserAvatar' ;
1720import { Link } from '@/i18n/routing' ;
1821
@@ -57,8 +60,46 @@ export function ProfileCard({
5760 document . getElementById ( id ) ?. scrollIntoView ( { behavior : 'smooth' } ) ;
5861 } ;
5962
63+ const handleUpdateName = async ( e : React . FormEvent < HTMLFormElement > ) => {
64+ e . preventDefault ( ) ;
65+ setIsSaving ( true ) ;
66+ const formData = new FormData ( e . currentTarget ) ;
67+
68+ try {
69+ const result = await updateName ( formData ) ;
70+ if ( ! result . success ) {
71+ toast . error ( result . error || 'Failed to update name' ) ;
72+ }
73+ } catch ( error ) {
74+ toast . error ( 'Something went wrong' ) ;
75+ } finally {
76+ setIsSaving ( false ) ;
77+ }
78+ } ;
79+
80+ const handleUpdatePassword = async ( e : React . FormEvent < HTMLFormElement > ) => {
81+ e . preventDefault ( ) ;
82+ setIsSaving ( true ) ;
83+ const formData = new FormData ( e . currentTarget ) ;
84+
85+ try {
86+ const result = await updatePassword ( formData ) ;
87+ if ( result . success ) {
88+ ( e . target as HTMLFormElement ) . reset ( ) ;
89+ } else {
90+ toast . error ( result . error || 'Failed to update password' ) ;
91+ }
92+ } catch ( error ) {
93+ toast . error ( 'Something went wrong' ) ;
94+ } finally {
95+ setIsSaving ( false ) ;
96+ }
97+ } ;
98+
6099 const statItemBase =
61- 'flex flex-row items-center gap-2 sm:gap-3 rounded-2xl border border-gray-100 bg-white/50 p-2 sm:p-3 text-left dark:border-white/5 dark:bg-black/20 xl:flex-row-reverse xl:items-center xl:text-right xl:p-3 xl:px-4 transition-all hover:border-(--accent-primary)/40 hover:bg-gray-50 dark:hover:bg-white/5 dark:hover:border-(--accent-primary)/20' ;
100+ 'flex flex-row items-center gap-2 sm:gap-3 rounded-2xl border border-gray-100 bg-white/50 p-2 sm:p-3 text-left dark:border-white/5 dark:bg-black/20 xl:flex-row-reverse xl:items-center xl:text-right xl:p-3 xl:px-4' ;
101+
102+ const iconBoxStyles = 'flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-white/40 border border-white/20 shadow-xs backdrop-blur-xs xl:h-auto xl:w-auto xl:p-2.5 dark:bg-white/5 dark:border-white/10' ;
62103
63104 return (
64105 < section className = { cardStyles } aria-labelledby = "profile-heading" >
@@ -102,12 +143,8 @@ export function ProfileCard({
102143 </ div >
103144 < dl className = "grid w-full grid-cols-2 gap-2 sm:grid-cols-4 sm:gap-3 xl:flex xl:w-auto xl:flex-nowrap xl:items-center xl:justify-end xl:gap-2 2xl:gap-3" >
104145 { /* Attempts */ }
105- < a
106- href = "#quiz-results"
107- onClick = { scrollTo ( 'quiz-results' ) }
108- className = { statItemBase }
109- >
110- < div className = "flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-purple-100/80 ring-1 ring-black/5 xl:h-auto xl:w-auto xl:p-2.5 dark:bg-purple-500/20 dark:ring-white/10" >
146+ < div className = { statItemBase } >
147+ < div className = { iconBoxStyles } >
111148 < Target className = "h-5 w-5 text-purple-600 dark:text-purple-400" />
112149 </ div >
113150 < div className = "flex w-full flex-col items-start overflow-hidden xl:items-end" >
@@ -118,11 +155,11 @@ export function ProfileCard({
118155 { totalAttempts }
119156 </ dd >
120157 </ div >
121- </ a >
158+ </ div >
122159
123160 { /* Points */ }
124- < a href = "#stats" onClick = { scrollTo ( 'stats' ) } className = { statItemBase } >
125- < div className = "flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-amber-100/80 ring-1 ring-black/5 xl:h-auto xl:w-auto xl:p-2.5 dark:bg-amber-500/20 dark:ring-white/10" >
161+ < div className = { statItemBase } >
162+ < div className = { iconBoxStyles } >
126163 < Trophy className = "h-5 w-5 text-amber-600 dark:text-amber-400" />
127164 </ div >
128165 < div className = "flex w-full flex-col items-start overflow-hidden xl:items-end" >
@@ -133,11 +170,11 @@ export function ProfileCard({
133170 { user . points }
134171 </ dd >
135172 </ div >
136- </ a >
173+ </ div >
137174
138175 { /* Global rank */ }
139- < Link href = "/leaderboard" className = { statItemBase } >
140- < div className = "flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-teal-100/80 ring-1 ring-black/5 xl:h-auto xl:w-auto xl:p-2.5 dark:bg-teal-500/20 dark:ring-white/10" >
176+ < div className = { statItemBase } >
177+ < div className = { iconBoxStyles } >
141178 < Globe className = "h-5 w-5 text-teal-600 dark:text-teal-400" />
142179 </ div >
143180 < div className = "flex w-full flex-col items-start overflow-hidden xl:items-end" >
@@ -148,11 +185,11 @@ export function ProfileCard({
148185 { globalRank ? `#${ globalRank } ` : '—' }
149186 </ dd >
150187 </ div >
151- </ Link >
188+ </ div >
152189
153190 { /* Joined */ }
154191 < div className = "flex flex-row items-center gap-2 rounded-2xl border border-gray-100 bg-white/50 p-2 text-left sm:gap-3 sm:p-3 xl:flex-row-reverse xl:items-center xl:p-3 xl:px-4 xl:text-right dark:border-white/5 dark:bg-black/20" >
155- < div className = "flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-blue-100/80 ring-1 ring-black/5 xl:h-auto xl:w-auto xl:p-2.5 dark:bg-blue-500/20 dark:ring-white/10" >
192+ < div className = { iconBoxStyles } >
156193 < Calendar className = "h-5 w-5 text-blue-600 dark:text-blue-400" />
157194 </ div >
158195 < div className = "flex w-full flex-col items-start overflow-hidden xl:items-end" >
@@ -204,20 +241,14 @@ export function ProfileCard({
204241 < h3 className = "mb-4 text-sm font-semibold tracking-wide text-gray-900 uppercase dark:text-white" >
205242 { t ( 'changeName' ) }
206243 </ h3 >
207- < form
208- onSubmit = { e => {
209- e . preventDefault ( ) ;
210- setIsSaving ( true ) ;
211- setTimeout ( ( ) => setIsSaving ( false ) , 1000 ) ;
212- } }
213- className = "flex flex-col gap-4 sm:flex-row sm:items-end"
214- >
244+ < form onSubmit = { handleUpdateName } className = "flex flex-col gap-4 sm:flex-row sm:items-end" >
215245 < div className = "flex-1" >
216246 < label htmlFor = "name-input" className = "sr-only" >
217247 { t ( 'changeName' ) }
218248 </ label >
219249 < input
220250 id = "name-input"
251+ name = "name"
221252 type = "text"
222253 defaultValue = { user . name || '' }
223254 className = "w-full rounded-xl border border-gray-200 bg-white px-4 py-2.5 text-sm transition-all outline-none placeholder:text-gray-400 focus:border-(--accent-primary) focus:ring-1 focus:ring-(--accent-primary) dark:border-neutral-800 dark:bg-neutral-900 dark:text-white"
@@ -240,20 +271,11 @@ export function ProfileCard({
240271 < h3 className = "mb-4 text-sm font-semibold tracking-wide text-gray-900 uppercase dark:text-white" >
241272 { t ( 'changePassword' ) }
242273 </ h3 >
243- < form
244- onSubmit = { e => {
245- e . preventDefault ( ) ;
246- if ( e . currentTarget . checkValidity ( ) ) {
247- setIsSaving ( true ) ;
248- setTimeout ( ( ) => setIsSaving ( false ) , 1000 ) ;
249- e . currentTarget . reset ( ) ;
250- }
251- } }
252- className = "flex flex-col gap-4"
253- >
274+ < form onSubmit = { handleUpdatePassword } className = "flex flex-col gap-4" >
254275 < div >
255276 < input
256277 type = "password"
278+ name = "currentPassword"
257279 placeholder = { t ( 'currentPassword' ) }
258280 className = "w-full rounded-xl border border-gray-200 bg-white px-4 py-2.5 text-sm transition-all outline-none placeholder:text-gray-400 focus:border-(--accent-primary) focus:ring-1 focus:ring-(--accent-primary) dark:border-neutral-800 dark:bg-neutral-900 dark:text-white"
259281 required
@@ -262,6 +284,7 @@ export function ProfileCard({
262284 < div >
263285 < input
264286 type = "password"
287+ name = "newPassword"
265288 placeholder = { t ( 'newPassword' ) }
266289 minLength = { 8 }
267290 className = "w-full rounded-xl border border-gray-200 bg-white px-4 py-2.5 text-sm transition-all outline-none placeholder:text-gray-400 focus:border-(--accent-primary) focus:ring-1 focus:ring-(--accent-primary) dark:border-neutral-800 dark:bg-neutral-900 dark:text-white"
0 commit comments