feat: nextjs 14 (#20)
* feat: eslint update * feat: start over and layout * feat: nextjs13 boilerplate * feat: static homepage * feat: static pages * feat: static unirse * chore: remove old mui types * chore: moving from yarn to bun * chore: update dependencies * feat: static equipo unirse * feat: move appwrite to entgamers-database package * feat: improve ui components * feat: update dependencies * feat: static login & register pages * fix: remove unused logs * feat: state redux toolkit & feedback slice * fix: equipo div inside p * feat: session * feat: metadataBase * feat: basic apply form * feat: http verbs * feat: recover password flow * chore: updated dependencies * fix: fix image config * fix: api team-applications route * fix: remove not longer used fonts * feat: session with current user * fix: login form recuperar contraseña * feat: equipo pages now uses data from database package * feat: useManageErrors hook * feat: updated cuenta page * chore: updated old formik forms to use hooks * feat: updated dependencies &package name * fix: session related bugs * fix: missing helper texts * feat: static applications dashboard * chore: update dependencies * refactor: team applications * fix: session api update
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
'use client'
|
||||
import Button from '@/components/ui/Button'
|
||||
import ButtonGroup from '@/components/ui/ButtonGroup'
|
||||
import useSession from '@/hooks/useSession'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { useState, type FC } from 'react'
|
||||
import UpdateEmail from './UpdateEmail'
|
||||
import UpdatePassword from './UpdatePassword'
|
||||
import UpdateUserName from './UpdateUserName'
|
||||
import UpdateUserPreferences from './UpdateUserPreferences'
|
||||
|
||||
type Tab = 'perfil' | 'login'
|
||||
|
||||
const CuentaTabs: FC = () => {
|
||||
useSession('/login')
|
||||
const [currentTab, setCurrentTab] = useState<Tab>('perfil')
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonGroup
|
||||
fullWidth={true}
|
||||
>
|
||||
<Button
|
||||
fullWidth
|
||||
onClick={() => { setCurrentTab('perfil') }}
|
||||
disabled={currentTab === 'perfil'}
|
||||
>
|
||||
Perfil
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
onClick={() => { setCurrentTab('login') }}
|
||||
disabled={currentTab === 'login'}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<div
|
||||
className={css({
|
||||
overflow: 'hidden',
|
||||
marginTop: 'medium'
|
||||
})}
|
||||
>
|
||||
<AnimatePresence
|
||||
initial={false}
|
||||
mode='wait'
|
||||
>
|
||||
{currentTab === 'login' && (
|
||||
<motion.div
|
||||
transition={{ duration: 0.15, ease: 'easeInOut' }}
|
||||
initial={{ opacity: 0, x: '-100%' }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: '100%' }}
|
||||
key="login">
|
||||
<UpdateEmail />
|
||||
<UpdatePassword />
|
||||
</motion.div>
|
||||
)}
|
||||
{currentTab === 'perfil' && (
|
||||
<motion.div
|
||||
transition={{ duration: 0.15, ease: 'easeInOut' }}
|
||||
initial={{ opacity: 0, x: '-100%' }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: '100%' }}
|
||||
key="perfil">
|
||||
<UpdateUserName />
|
||||
<UpdateUserPreferences />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CuentaTabs
|
||||
@@ -0,0 +1,129 @@
|
||||
'use client'
|
||||
import Button from '@/components/ui/Button'
|
||||
import Typography from '@/components/ui/Typography'
|
||||
import FormGroup from '@/components/ui/form/FormGroup'
|
||||
import Input from '@/components/ui/form/Input'
|
||||
import PasswordInput from '@/components/ui/form/PasswordInput'
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch'
|
||||
import useSession from '@/hooks/useSession'
|
||||
import { addAlert } from '@/state/feedbackSlice'
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { AppwriteException } from 'appwrite'
|
||||
import { updateEmail } from 'entgamers-database/frontend/session'
|
||||
import { useFormik } from 'formik'
|
||||
import { type FC } from 'react'
|
||||
import { object, string } from 'yup'
|
||||
|
||||
interface UpdateEmailData {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
|
||||
const updateEmailSchema = object({
|
||||
email: string().email('El correo electrónico no es válido').required('El correo electrónico es requerido'),
|
||||
password: string().required('La contraseña es requerida')
|
||||
})
|
||||
|
||||
const UpdateEmail: FC = () => {
|
||||
const { status, session } = useSession('/login')
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const formik = useFormik<UpdateEmailData>({
|
||||
initialValues: {
|
||||
email: '',
|
||||
password: ''
|
||||
},
|
||||
onSubmit: async ({ email, password }) => {
|
||||
try {
|
||||
await updateEmail(email, password)
|
||||
dispatch(addAlert({
|
||||
id: nanoid(),
|
||||
title: 'Correo actualizado',
|
||||
message: 'Ahora puedes iniciar sesión',
|
||||
severity: 'success'
|
||||
}))
|
||||
} catch (error) {
|
||||
if (error instanceof AppwriteException) {
|
||||
dispatch(addAlert({
|
||||
id: nanoid(),
|
||||
message: error.message,
|
||||
title: 'Error mientras se actualizaba el correo',
|
||||
severity: 'error'
|
||||
}))
|
||||
} else {
|
||||
dispatch(addAlert({
|
||||
id: nanoid(),
|
||||
message: 'Error desconocido',
|
||||
title: 'Error mientras se actualizaba el correo',
|
||||
severity: 'error'
|
||||
}))
|
||||
}
|
||||
}
|
||||
},
|
||||
validationSchema: updateEmailSchema,
|
||||
validateOnMount: true
|
||||
})
|
||||
|
||||
if (status !== 'idle' || session === undefined) {
|
||||
// TODO: Replace with Skeleton
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="h2">Cambia tu correo</Typography>
|
||||
<form
|
||||
onSubmit={formik.handleSubmit}
|
||||
>
|
||||
<FormGroup>
|
||||
<label
|
||||
htmlFor="email"
|
||||
>
|
||||
Correo
|
||||
</label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
value={formik.values.email}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
status={formik.touched.email !== undefined && formik.errors.email !== undefined ? 'danger' : undefined}
|
||||
/>
|
||||
{formik.touched.email !== undefined && formik.errors.email !== undefined && (
|
||||
<Typography variant="caption" color="danger">{formik.errors.email}</Typography>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<label
|
||||
htmlFor="password"
|
||||
>
|
||||
Contraseña
|
||||
</label>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
name="password"
|
||||
value={formik.values.password}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
status={formik.touched.password !== undefined && formik.errors.password !== undefined ? 'danger' : undefined}
|
||||
fullWidth
|
||||
/>
|
||||
{formik.touched.password !== undefined && formik.errors.password !== undefined && (
|
||||
<Typography variant="caption" color="danger">{formik.errors.password}</Typography>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={formik.isSubmitting || !formik.isValid}
|
||||
>
|
||||
Actualizar contraseña
|
||||
</Button>
|
||||
</FormGroup>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpdateEmail
|
||||
@@ -0,0 +1,160 @@
|
||||
'use client'
|
||||
import Button from '@/components/ui/Button'
|
||||
import Typography from '@/components/ui/Typography'
|
||||
import FormGroup from '@/components/ui/form/FormGroup'
|
||||
import PasswordInput from '@/components/ui/form/PasswordInput'
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch'
|
||||
import useSession from '@/hooks/useSession'
|
||||
import { addAlert } from '@/state/feedbackSlice'
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { AppwriteException } from 'appwrite'
|
||||
import { updatePassword } from 'entgamers-database/frontend/session'
|
||||
import { useFormik } from 'formik'
|
||||
import { type FC } from 'react'
|
||||
import { object, ref, string } from 'yup'
|
||||
|
||||
interface UpdatePasswordData {
|
||||
password: string
|
||||
confirmPassword: string
|
||||
currentPassword: string
|
||||
}
|
||||
|
||||
const updatePasswordSchema = object({
|
||||
password: string()
|
||||
.min(6, 'La contraseña debe tener al menos 6 caracteres')
|
||||
.matches(/[a-z]/, 'La contraseña debe tener al menos una letra minúscula')
|
||||
.matches(/[A-Z]/, 'La contraseña debe tener al menos una letra mayúscula')
|
||||
.matches(/[0-9]/, 'La contraseña debe tener al menos un número')
|
||||
.required('La contraseña es requerida'),
|
||||
confirmPassword: string().oneOf([ref('password')], 'Las contraseñas no coinciden').required('La confirmación de la contraseña es requerida'),
|
||||
currentPassword: string().required('La contraseña actual es requerida')
|
||||
})
|
||||
|
||||
const UpdatePassword: FC = () => {
|
||||
const { status, session } = useSession('/login')
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const formik = useFormik<UpdatePasswordData>({
|
||||
initialValues: {
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
currentPassword: ''
|
||||
},
|
||||
onSubmit: async ({ password, currentPassword }) => {
|
||||
try {
|
||||
await updatePassword(password, currentPassword)
|
||||
dispatch(addAlert({
|
||||
id: nanoid(),
|
||||
title: 'Contrasenya actualizada',
|
||||
message: 'Ahora puedes iniciar sesión',
|
||||
severity: 'success'
|
||||
}))
|
||||
} catch (error) {
|
||||
if (error instanceof AppwriteException) {
|
||||
dispatch(addAlert({
|
||||
id: nanoid(),
|
||||
message: error.message,
|
||||
title: 'Error mientras se actualizaba la contraseña',
|
||||
severity: 'error'
|
||||
}))
|
||||
} else {
|
||||
dispatch(addAlert({
|
||||
id: nanoid(),
|
||||
message: 'Error desconocido',
|
||||
title: 'Error mientras se actualizaba la contraseña',
|
||||
severity: 'error'
|
||||
}))
|
||||
}
|
||||
}
|
||||
},
|
||||
validationSchema: updatePasswordSchema,
|
||||
validateOnMount: true
|
||||
})
|
||||
|
||||
if (status !== 'idle' || session === undefined) {
|
||||
// TODO: Replace with Skeleton
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="h2">Actualizar contraseña</Typography>
|
||||
<form
|
||||
onSubmit={formik.handleSubmit}
|
||||
>
|
||||
<FormGroup>
|
||||
<label
|
||||
htmlFor="password"
|
||||
>
|
||||
Nueva contraseña
|
||||
</label>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
name="password"
|
||||
value={formik.values.password}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
status={formik.touched.password !== undefined && formik.errors.password !== undefined ? 'danger' : undefined}
|
||||
fullWidth
|
||||
/>
|
||||
{formik.touched.password !== undefined && formik.errors.password !== undefined && (
|
||||
<Typography variant="caption" color="danger">
|
||||
{formik.errors.password}
|
||||
</Typography>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<label
|
||||
htmlFor="confirmPassword"
|
||||
>
|
||||
Confirmar nueva contraseña
|
||||
</label>
|
||||
<PasswordInput
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
value={formik.values.confirmPassword}
|
||||
status={formik.touched.confirmPassword !== undefined && formik.errors.confirmPassword !== undefined ? 'danger' : undefined}
|
||||
fullWidth
|
||||
/>
|
||||
{formik.touched.confirmPassword !== undefined && formik.errors.confirmPassword !== undefined && (
|
||||
<Typography variant="caption" color="danger">
|
||||
{formik.errors.confirmPassword}
|
||||
</Typography>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<label
|
||||
htmlFor="currentPassword"
|
||||
>
|
||||
Contraseña actual
|
||||
</label>
|
||||
<PasswordInput
|
||||
id="currentPassword"
|
||||
name="currentPassword"
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
value={formik.values.currentPassword}
|
||||
status={formik.touched.currentPassword !== undefined && formik.errors.currentPassword !== undefined ? 'danger' : undefined}
|
||||
fullWidth
|
||||
/>
|
||||
{formik.touched.currentPassword !== undefined && formik.errors.currentPassword !== undefined && (
|
||||
<Typography variant="caption" color="danger">
|
||||
{formik.errors.currentPassword}
|
||||
</Typography>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={formik.isSubmitting || !formik.isValid}
|
||||
>
|
||||
Actualizar contraseña
|
||||
</Button>
|
||||
</FormGroup>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default UpdatePassword
|
||||
@@ -0,0 +1,115 @@
|
||||
'use client'
|
||||
import Button from '@/components/ui/Button'
|
||||
import Typography from '@/components/ui/Typography'
|
||||
import FormGroup from '@/components/ui/form/FormGroup'
|
||||
import Input from '@/components/ui/form/Input'
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch'
|
||||
import useSession from '@/hooks/useSession'
|
||||
import { addAlert } from '@/state/feedbackSlice'
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { AppwriteException } from 'appwrite'
|
||||
import { updateName } from 'entgamers-database/frontend/session'
|
||||
import { useFormik } from 'formik'
|
||||
import { useEffect, type FC } from 'react'
|
||||
import { object, string } from 'yup'
|
||||
|
||||
interface UpdateUserNameData {
|
||||
name: string
|
||||
}
|
||||
|
||||
const UpdateUserNameSchema = object({
|
||||
name: string().required('El nombre es requerido')
|
||||
})
|
||||
|
||||
const UpdateUserName: FC = () => {
|
||||
const { status, session, user } = useSession('/login')
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const formik = useFormik<UpdateUserNameData>({
|
||||
initialValues: {
|
||||
name: ''
|
||||
},
|
||||
onSubmit: async ({ name }) => {
|
||||
try {
|
||||
await updateName(name)
|
||||
dispatch(addAlert({
|
||||
id: nanoid(),
|
||||
title: 'Nombre actualizado',
|
||||
message: 'Se actualizo correctamente el nombre',
|
||||
severity: 'success'
|
||||
}))
|
||||
} catch (error) {
|
||||
if (error instanceof AppwriteException) {
|
||||
dispatch(addAlert({
|
||||
id: nanoid(),
|
||||
message: error.message,
|
||||
title: 'Error mientras se actualizaba el nombre',
|
||||
severity: 'error'
|
||||
}))
|
||||
} else {
|
||||
dispatch(addAlert({
|
||||
id: nanoid(),
|
||||
message: 'Error desconocido',
|
||||
title: 'Error mientras se actualizaba el nombre',
|
||||
severity: 'error'
|
||||
}))
|
||||
}
|
||||
}
|
||||
},
|
||||
validationSchema: UpdateUserNameSchema,
|
||||
validateOnMount: true,
|
||||
initialTouched: { name: true }
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (status === 'idle' && session !== undefined && user !== undefined) {
|
||||
formik.setValues({
|
||||
name: user?.name ?? ''
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
}, [status, session, user])
|
||||
|
||||
if (status !== 'idle' || session === undefined) {
|
||||
// TODO: Replace with Skeleton
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Typography variant="h2">Cambia tu nombre de usuario</Typography>
|
||||
<form
|
||||
onSubmit={formik.handleSubmit}
|
||||
>
|
||||
<FormGroup>
|
||||
<label
|
||||
htmlFor="name"
|
||||
>
|
||||
Nombre de usuario
|
||||
</label>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
value={formik.values.name}
|
||||
status={formik.touched.name !== undefined && formik.errors.name !== undefined ? 'danger' : undefined}
|
||||
/>
|
||||
{formik.touched.name !== undefined && formik.errors.name !== undefined && (
|
||||
<Typography variant="caption" color="danger">{formik.errors.name}</Typography>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={formik.isSubmitting || !formik.isValid}
|
||||
>
|
||||
Actualizar nombre de usuario
|
||||
</Button>
|
||||
</FormGroup>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default UpdateUserName
|
||||
@@ -0,0 +1,111 @@
|
||||
'use client'
|
||||
import Button from '@/components/ui/Button'
|
||||
import Typography from '@/components/ui/Typography'
|
||||
import FormGroup from '@/components/ui/form/FormGroup'
|
||||
import TextArea from '@/components/ui/form/TextArea'
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch'
|
||||
import useManageError from '@/hooks/useManageError'
|
||||
import useSession from '@/hooks/useSession'
|
||||
import { setCurrentUser } from '@/state/sessionSlice'
|
||||
import { updatePreferences, type UserPreferences } from 'entgamers-database/frontend/session'
|
||||
import { useFormik } from 'formik'
|
||||
import { useEffect, type FC } from 'react'
|
||||
import { array, object, string, type ObjectSchema } from 'yup'
|
||||
|
||||
const socialLinksSchema: ObjectSchema<UserPreferences> = object({
|
||||
bio: string().max(280, 'La descripción debe tener menos de 280 caracteres'),
|
||||
profilePicture: string().url('La imagen debe ser una URL'),
|
||||
socialLinks: array().of(
|
||||
object({
|
||||
label: string().required('La etiqueta es requerida'),
|
||||
url: string().url('La URL debe ser una URL').required('La URL es requerida')
|
||||
})
|
||||
).min(0)
|
||||
})
|
||||
|
||||
const UpdateUserPreferences: FC = () => {
|
||||
const { status, session, user } = useSession('/login')
|
||||
const dispatch = useAppDispatch()
|
||||
const { manageError } = useManageError()
|
||||
|
||||
const formik = useFormik<UserPreferences>({
|
||||
initialValues: {
|
||||
bio: '',
|
||||
profilePicture: '',
|
||||
socialLinks: []
|
||||
},
|
||||
onSubmit: async ({ bio, profilePicture, socialLinks }) => {
|
||||
try {
|
||||
const updatedUserWithPreferences = await updatePreferences({ bio, profilePicture, socialLinks })
|
||||
dispatch(setCurrentUser(updatedUserWithPreferences))
|
||||
} catch (error) {
|
||||
manageError(error, 'Error mientras se actualizaba las preferencias', ' Error desconocido mientras se actualizaba las preferencias', 'error')
|
||||
}
|
||||
},
|
||||
validationSchema: socialLinksSchema,
|
||||
validateOnMount: true
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (status === 'idle' && session !== undefined) {
|
||||
formik.setValues({
|
||||
bio: user?.prefs.bio ?? '',
|
||||
profilePicture: user?.prefs.profilePicture ?? '',
|
||||
socialLinks: user?.prefs.socialLinks ?? []
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
}, [status, session])
|
||||
|
||||
if (status !== 'idle' || session === undefined) {
|
||||
// TODO: Replace with Skeleton
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography
|
||||
variant='h3'
|
||||
>
|
||||
Preferencias
|
||||
</Typography>
|
||||
<form
|
||||
onSubmit={formik.handleSubmit}
|
||||
>
|
||||
<FormGroup>
|
||||
<label
|
||||
htmlFor='bio'
|
||||
>
|
||||
Biografia
|
||||
</label>
|
||||
<TextArea
|
||||
id='bio'
|
||||
name='bio'
|
||||
value={formik.values.bio}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
disabled={formik.isSubmitting}
|
||||
status={formik.touched.bio !== undefined && formik.errors.bio !== undefined ? 'danger' : undefined}
|
||||
/>
|
||||
{formik.touched.bio !== undefined && formik.errors.bio !== undefined && (
|
||||
<Typography variant="caption" color="danger">
|
||||
{formik.errors.bio}
|
||||
</Typography>
|
||||
)}
|
||||
</FormGroup>
|
||||
{/* TODO: Add Profile Picture and Social Links fields */}
|
||||
<FormGroup>
|
||||
<Button
|
||||
type='submit'
|
||||
disabled={formik.isSubmitting || !formik.isValid}
|
||||
fullWidth
|
||||
>
|
||||
Actualizar
|
||||
</Button>
|
||||
</FormGroup>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpdateUserPreferences
|
||||
@@ -0,0 +1,17 @@
|
||||
import Typography from '@/components/ui/Typography'
|
||||
import { Container } from '@/styled-system/jsx'
|
||||
import { type FC } from 'react'
|
||||
import CuentaTabs from './CuentaTabs'
|
||||
|
||||
const CuentaPage: FC = () => {
|
||||
return (
|
||||
<Container>
|
||||
<Typography variant="h1" align="center">Cuenta</Typography>
|
||||
<Typography variant="body1">
|
||||
Desde aquí puedes administrar las preferencias y ajustes de tu cuenta.
|
||||
</Typography>
|
||||
<CuentaTabs/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
export default CuentaPage
|
||||
Reference in New Issue
Block a user