fix: rework branch naming updating to next 16
Squashed commit of the following: commit13fb596232Author: SrJuggernaut <jugger@srjuggernaut.dev> Date: Thu Apr 2 12:59:26 2026 -0600 chore(deps): upgrade sharp to v0.34.5, add @types/bun, remove @types/node commit17be2f09ddAuthor: SrJuggernaut <jugger@srjuggernaut.dev> Date: Thu Apr 2 12:49:47 2026 -0600 chore: add lint-staged with husky pre-commit hook for automated linting commitb4a28fb35eAuthor: SrJuggernaut <jugger@srjuggernaut.dev> Date: Thu Apr 2 12:49:29 2026 -0600 chore: update husky hooks for latest version and use bun commit796c952890Author: SrJuggernaut <jugger@srjuggernaut.dev> Date: Thu Apr 2 12:00:41 2026 -0600 build: add Dockerfile and .dockerignore for Next.js containerization commit166ac4350fAuthor: SrJuggernaut <jugger@srjuggernaut.dev> Date: Thu Apr 2 11:59:44 2026 -0600 chore: moves next config to typescript Also adds DOCKER_BUILD env variable in prepare for moving out from github workflows commitea498588e0Author: SrJuggernaut <jugger@srjuggernaut.dev> Date: Wed Apr 1 14:17:22 2026 -0600 chore(deps): bump date-fns from 3.3.1 to 4.1.0 commit3c66b5bad1Author: SrJuggernaut <jugger@srjuggernaut.dev> Date: Wed Apr 1 14:16:31 2026 -0600 chore: update @pandacss/dev to v1.9.1 commit221034133dAuthor: SrJuggernaut <jugger@srjuggernaut.dev> Date: Wed Apr 1 14:14:35 2026 -0600 chore: update husky and commitlint commit4be0de62fcAuthor: SrJuggernaut <jugger@srjuggernaut.dev> Date: Wed Apr 1 14:07:57 2026 -0600 chore: update FontAwesome dependencies to v7.2.0 commitb595cba4b0Author: SrJuggernaut <jugger@srjuggernaut.dev> Date: Wed Apr 1 14:01:16 2026 -0600 chore: update TypeScript module config to esnext and bundler commitb8454f750dAuthor: SrJuggernaut <jugger@srjuggernaut.dev> Date: Wed Apr 1 13:51:25 2026 -0600 feat: add suspense loading state to recover password page commitd0f4a88661Author: SrJuggernaut <jugger@srjuggernaut.dev> Date: Wed Apr 1 13:48:15 2026 -0600 refactor: enhance TypeScript typing and import consistency commit57a6032a24Author: SrJuggernaut <jugger@srjuggernaut.dev> Date: Wed Apr 1 13:46:02 2026 -0600 chore: upgrade next and deps commitf8018048bcAuthor: SrJuggernaut <jugger@srjuggernaut.dev> Date: Wed Jul 31 18:44:51 2024 -0600 fix: pm2 run with bun commitec196b2850Author: SrJuggernaut <jugger@srjuggernaut.dev> Date: Wed Jul 31 18:35:29 2024 -0600 fix: deploy commit8802b0fd68Author: SrJuggernaut <jugger@srjuggernaut.dev> Date: Tue Jul 30 18:23:15 2024 -0600 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 commit14b52a7800Author: SrJuggernaut <jugger@srjuggernaut.dev> Date: Thu Sep 14 21:15:53 2023 -0600 fix: remove bundle analyzer commitf11ae1c4f3Author: SrJuggernaut <jugger@srjuggernaut.dev> Date: Thu Sep 14 21:08:33 2023 -0600 revert: revert to yarn to deploy commitf5a9a88f84Author: SrJuggernaut <jugger@srjuggernaut.dev> Date: Thu Sep 14 14:52:25 2023 -0600 fix: pm2 script commitf59a7cc091Author: SrJuggernaut <jugger@srjuggernaut.dev> Date: Thu Sep 14 14:49:04 2023 -0600 fix: interpreter to use pm2 2 commit3a831acaeeAuthor: SrJuggernaut <jugger@srjuggernaut.dev> Date: Thu Sep 14 14:39:51 2023 -0600 fix: interpreter to use pm2 commitb514c6bc6fAuthor: SrJuggernaut <jugger@srjuggernaut.dev> Date: Thu Sep 14 14:21:14 2023 -0600 fix: bun doesnt run on pm2 commitce87aa5ee3Author: SrJuggernaut <jugger@srjuggernaut.dev> Date: Thu Sep 14 14:14:12 2023 -0600 feat: moving deployments to bun commitb26d5d8ebaAuthor: SrJuggernaut <jugger@srjuggernaut.dev> Date: Thu Sep 14 13:01:22 2023 -0600 chore: updated dependencies commit390f8bc858Author: SrJuggernaut <jugger@srjuggernaut.dev> Date: Tue Jul 11 15:46:34 2023 -0600 feat: riot review commitd926d3a5efAuthor: SrJuggernaut <jugger@srjuggernaut.dev> Date: Wed Jan 4 15:47:45 2023 -0600 fix: github action name, wrong triggers commitfdefa84ec7Author: SrJuggernaut <jugger@srjuggernaut.dev> Date: Wed Jan 4 15:46:14 2023 -0600 fix: github action naming commit9db5e5cebfAuthor: SrJuggernaut <jugger@srjuggernaut.dev> Date: Wed Jan 4 15:44:18 2023 -0600 feat: setup github action to run on new server commit35105441cbAuthor: Jugger <jugger@srjuggernaut.dev> Date: Sun Oct 9 13:56:41 2022 -0500 docs: deploy env vars commit7986456b9cAuthor: Jugger <jugger@srjuggernaut.dev> Date: Sun Oct 9 13:20:42 2022 -0500 fix: production git ref commitf9aea7eea4Author: Jugger <jugger@srjuggernaut.dev> Date: Sat Oct 1 13:34:31 2022 -0500 fix: glass text contrast commita68098e7e2Author: Jugger <jugger@srjuggernaut.dev> Date: Sat Oct 1 13:33:29 2022 -0500 ci: use yarn instead npm commit864ff91255Author: Jugger <jugger@srjuggernaut.dev> Date: Fri Sep 30 19:32:40 2022 -0500 fix: server use isomorphic-fetch commitc69a166c1fAuthor: Jugger <jugger@srjuggernaut.dev> Date: Fri Sep 30 18:23:38 2022 -0500 revert: deploy as sudo Refs:624b225commit731c9b8962Author: Jugger <jugger@srjuggernaut.dev> Date: Fri Sep 30 17:50:41 2022 -0500 fix: pass only required env to deploy commit624b2251c4Author: Jugger <jugger@srjuggernaut.dev> Date: Fri Sep 30 13:57:27 2022 -0500 feat: post deploy as sudo commitb73cc51e08Author: Jugger <jugger@srjuggernaut.dev> Date: Fri Sep 30 13:41:56 2022 -0500 fix: pass environment to actions commita35e99f8ffAuthor: Jugger <jugger@srjuggernaut.dev> Date: Thu Sep 29 21:54:58 2022 -0500 ci: deploy using pm2 * ci: pm2 configuration file * ci: github action deploy preview * ci: github action deploy production * ci: env variables now pass to pm2 commit4f8c4f6492Author: Jugger <jugger@srjuggernaut.dev> Date: Wed Sep 28 18:09:10 2022 -0500 docs: initial documentation commitc3dae929c6Author: Jugger <jugger@srjuggernaut.dev> Date: Mon Sep 26 12:01:26 2022 -0500 feat: static site * feat: mui support & basic theming * feat: entgamers favicon * feat: public images until dynamic content can be used * feat: entgamers & gaming assets * feat: eslint extra rules * feat: mui theme modifications * feat: fontawesome, gsap, bundle analyzer * feat: common interfaces * feat: basic layout * chore: upadted dependencies * chore: updated dependencies * feat: updated link styles * feat: layout now have better interfaces * feat: basic seo component * feat: static website * feat: env variable rules in .gitignore * feat: added lint to pre-commit commit8573d61066Author: SrJuggernaut <jugger@srjuggernaut.dev> Date: Sat Aug 13 11:29:32 2022 -0500 Initial commit from Create Next App
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
import Typography from '@/components/ui/Typography'
|
||||
import { css, cx } from '@/styled-system/css'
|
||||
import { Center } from '@/styled-system/jsx'
|
||||
import { center, container } from '@/styled-system/patterns'
|
||||
import { button, card } from '@/styled-system/recipes'
|
||||
import NextImage from 'next/image'
|
||||
import NextLink from 'next/link'
|
||||
import { type FC } from 'react'
|
||||
|
||||
const Clanes: FC = () => {
|
||||
return (
|
||||
<section
|
||||
id="clanes"
|
||||
className={cx(center({}), css({
|
||||
minHeight: '75vh',
|
||||
backgroundImage: 'url(/images/backgrounds/bricks.png)'
|
||||
}))}
|
||||
>
|
||||
<div
|
||||
className={cx(card({ variant: 'glass' }).body, container({}))}
|
||||
>
|
||||
<div
|
||||
className={card({ variant: 'glass' }).content}
|
||||
>
|
||||
<Typography variant="h2" align="center">Clanes</Typography>
|
||||
<div
|
||||
className={css({
|
||||
display: 'grid',
|
||||
alignItems: 'center',
|
||||
gridTemplateColumns: { base: '1fr 1fr', smDown: '1fr' },
|
||||
gap: 'medium'
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
order: { base: 0, smDown: 1 }
|
||||
})}
|
||||
>
|
||||
<Typography variant="body1">Los clanes son espacios donde compartir nuestros gustos con otros usuarios, dándonos la oportunidad de organizar proyectos y eventos en los cuales formar parte.</Typography>
|
||||
<Center>
|
||||
<NextLink
|
||||
className={button()}
|
||||
href="/clanes"
|
||||
>
|
||||
Ver Clanes
|
||||
</NextLink>
|
||||
|
||||
</Center>
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
order: { base: 1, smDown: 0 }
|
||||
})}
|
||||
>
|
||||
<NextImage
|
||||
src="/images/Clanes.png"
|
||||
alt="Clanes"
|
||||
width={1200}
|
||||
height={630}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
export default Clanes
|
||||
@@ -0,0 +1,78 @@
|
||||
'use client'
|
||||
import IconButton from '@/components/ui/IconButton'
|
||||
import Typography from '@/components/ui/Typography'
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch'
|
||||
import { useAppSelector } from '@/hooks/useAppSelector'
|
||||
import { removeAlert } from '@/state/feedbackSlice'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { alert } from '@/styled-system/recipes/alert'
|
||||
import { faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import type { FC } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
const FeedbackConsumer: FC = () => {
|
||||
const { alerts } = useAppSelector((state) => state.feedback)
|
||||
const dispatch = useAppDispatch()
|
||||
return (
|
||||
<>
|
||||
{alerts.length > 0 && createPortal(
|
||||
(
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
key="alerts"
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 'medium',
|
||||
position: 'fixed',
|
||||
bottom: 'medium',
|
||||
left: 'medium',
|
||||
padding: 'medium',
|
||||
zIndex: 'modalBackdrop',
|
||||
width: 'calc(100vw - 32px)',
|
||||
maxWidth: '400px'
|
||||
})}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1, y: 0, transition: { duration: 0.3, ease: 'backIn' } }}
|
||||
exit={{ opacity: 0, x: 400, transition: { duration: 0.3, ease: 'backOut' } }}
|
||||
>
|
||||
<AnimatePresence>
|
||||
{alerts.map((currentAlert) => (
|
||||
<motion.div
|
||||
key={currentAlert.id}
|
||||
// This is a workaround for PandaCSS to auto-generate styles and avoid Alerts with non-generated styles. See https://panda-css.com/docs/guides/dynamic-styling#runtime-conditions
|
||||
className={alert({
|
||||
severity: currentAlert.severity === 'success' ? 'success' : currentAlert.severity === 'info' ? 'info' : currentAlert.severity === 'warning' ? 'warning' : currentAlert.severity === 'error' ? 'error' : undefined
|
||||
}).body}
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
animate={{ opacity: 1, y: 0, transition: { duration: 0.3, ease: 'backIn' } }}
|
||||
exit={{ opacity: 0, x: 400, transition: { duration: 0.3, ease: 'backOut' } }}
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
className={alert().closeButton}
|
||||
onClick={() => dispatch(removeAlert(currentAlert.id))}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTimes} size="sm" />
|
||||
</IconButton>
|
||||
<Typography variant="h3" component="div">
|
||||
{currentAlert.title}
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
{currentAlert.message}
|
||||
</Typography>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
),
|
||||
document.body,
|
||||
'alerts'
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default FeedbackConsumer
|
||||
@@ -0,0 +1,132 @@
|
||||
import Typography from '@/components/ui/Typography'
|
||||
import { css, cx } from '@/styled-system/css'
|
||||
import { Center, Container } from '@/styled-system/jsx'
|
||||
import { iconButton } from '@/styled-system/recipes'
|
||||
import { faArrowDown } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import NextImage from 'next/image'
|
||||
import { type FC } from 'react'
|
||||
|
||||
const layerCss = css({
|
||||
backgroundPositionY: 'bottom',
|
||||
backgroundPositionX: 'x-start',
|
||||
backgroundRepeat: 'repeat',
|
||||
backgroundSize: 'initial',
|
||||
height: '100vh',
|
||||
width: '100%',
|
||||
willChange: 'background-position-y',
|
||||
animationName: 'bgMotion',
|
||||
animationTimingFunction: 'linear',
|
||||
animationIterationCount: 'infinite'
|
||||
})
|
||||
|
||||
const Hero: FC = () => {
|
||||
return (
|
||||
<section
|
||||
className={cx(layerCss, css({
|
||||
backgroundImage: 'url(/images/backgrounds/MysteriousForestNightLayer01.png)',
|
||||
marginTop: '-76px',
|
||||
animationDuration: '175s',
|
||||
position: 'relative'
|
||||
}))}
|
||||
>
|
||||
<div
|
||||
className={cx(layerCss, css({
|
||||
backgroundImage: 'url(/images/backgrounds/MysteriousForestNightLayer02.png)',
|
||||
animationDuration: '150s'
|
||||
}))}
|
||||
>
|
||||
<div
|
||||
className={cx(layerCss, css({
|
||||
backgroundImage: 'url(/images/backgrounds/MysteriousForestNightLayer03.png)',
|
||||
animationDuration: '125s'
|
||||
}))}
|
||||
>
|
||||
<div
|
||||
className={cx(layerCss, css({
|
||||
backgroundImage: 'url(/images/backgrounds/MysteriousForestNightLayer04.png)',
|
||||
animationDuration: '100s'
|
||||
}))}
|
||||
>
|
||||
<div
|
||||
className={cx(layerCss, css({
|
||||
backgroundImage: 'url(/images/backgrounds/MysteriousForestNightLayer05.png)',
|
||||
animationDuration: '75s'
|
||||
}))}
|
||||
>
|
||||
<Center
|
||||
className={cx(layerCss, css({
|
||||
backgroundImage: 'url(/images/backgrounds/MysteriousForestNightLayer06.png)',
|
||||
animationDuration: '50s'
|
||||
}))}
|
||||
>
|
||||
<Container
|
||||
className={css({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { base: '1fr 1fr', smDown: '1fr' },
|
||||
alignItems: 'center'
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
order: { base: 0, smDown: 1 }
|
||||
})}
|
||||
>
|
||||
<Typography
|
||||
variant="h1"
|
||||
align="center"
|
||||
>
|
||||
EntGamers
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h2"
|
||||
align="center"
|
||||
color="text"
|
||||
>
|
||||
Comunidad de y para los gamers
|
||||
</Typography>
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
order: { base: 1, smDown: 0 }
|
||||
})}
|
||||
>
|
||||
<NextImage
|
||||
src="/images/EntGamers.png"
|
||||
alt="EntGamers"
|
||||
width={500}
|
||||
height={500}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</Container>
|
||||
</Center>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href="#clanes"
|
||||
className={cx(iconButton({
|
||||
color: 'primary',
|
||||
size: 'large'
|
||||
}), css({
|
||||
'position': 'absolute',
|
||||
'bottom': '45px',
|
||||
'right': '50%',
|
||||
'animationName': 'bounce',
|
||||
'animationDuration': '1s',
|
||||
'animationIterationCount': 'infinite',
|
||||
'transform': 'translateX(50%)',
|
||||
'zIndex': 1,
|
||||
'&:hover': {
|
||||
animationPlayState: 'paused'
|
||||
}
|
||||
}))}
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowDown} size="lg" />
|
||||
</a>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
export default Hero
|
||||
@@ -0,0 +1,57 @@
|
||||
'use client'
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch'
|
||||
import { useAppSelector } from '@/hooks/useAppSelector'
|
||||
import { setClanes, setCurrentUser, setSession, setStatus } from '@/state/sessionSlice'
|
||||
import { AppwriteException } from 'appwrite'
|
||||
import { getClanes } from 'entgamers-database/frontend/clanes'
|
||||
import { getCurrentUser, getSession } from 'entgamers-database/frontend/session'
|
||||
import { type FC, useCallback, useEffect } from 'react'
|
||||
|
||||
const SessionConsumer: FC = () => {
|
||||
const { status, session, user, clanes } = useAppSelector((state) => state.session)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const ensureSession = useCallback(async () => {
|
||||
try {
|
||||
dispatch(setStatus('loading'))
|
||||
const currentSession = await getSession('current')
|
||||
const currentUser = await getCurrentUser()
|
||||
dispatch(setSession(currentSession))
|
||||
dispatch(setCurrentUser(currentUser))
|
||||
} catch (error) {
|
||||
dispatch(setSession())
|
||||
dispatch(setCurrentUser())
|
||||
throw error
|
||||
} finally {
|
||||
dispatch(setStatus('idle'))
|
||||
}
|
||||
}, [dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
if (status !== 'initializing' || session !== undefined) return
|
||||
ensureSession()
|
||||
.catch((error) => {
|
||||
if (error instanceof AppwriteException) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
}, [status, session, ensureSession])
|
||||
|
||||
useEffect(() => {
|
||||
if (user !== undefined && clanes === undefined) {
|
||||
getClanes()
|
||||
.then((clanes) => {
|
||||
dispatch(setClanes(clanes))
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error instanceof AppwriteException) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
} else if (user === undefined && clanes !== undefined) {
|
||||
dispatch(setClanes())
|
||||
}
|
||||
}, [user, clanes, dispatch])
|
||||
return null
|
||||
}
|
||||
export default SessionConsumer
|
||||
@@ -0,0 +1,75 @@
|
||||
import Typography from '@/components/ui/Typography'
|
||||
import { css, cx } from '@/styled-system/css'
|
||||
import { Center } from '@/styled-system/jsx'
|
||||
import { container } from '@/styled-system/patterns'
|
||||
import { button, card } from '@/styled-system/recipes'
|
||||
import { type FC } from 'react'
|
||||
|
||||
const layerCss = css({
|
||||
backgroundPositionY: 'bottom',
|
||||
backgroundPositionX: 'x-start',
|
||||
backgroundRepeat: 'repeat',
|
||||
backgroundSize: 'initial',
|
||||
minHeight: '75vh',
|
||||
width: '100%',
|
||||
willChange: 'background-position-y',
|
||||
animationName: 'bgMotion',
|
||||
animationTimingFunction: 'linear',
|
||||
animationIterationCount: 'infinite'
|
||||
})
|
||||
|
||||
const Social: FC = () => {
|
||||
return (
|
||||
<section
|
||||
className={css({
|
||||
backgroundImage: 'url(/images/backgrounds/SkyNightLayer01.png)',
|
||||
backgroundPositionY: 'center',
|
||||
backgroundPositionX: 'center'
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={cx(layerCss, css({
|
||||
backgroundImage: 'url(/images/backgrounds/SkyNightLayer02.png)',
|
||||
animationDuration: '150s'
|
||||
}))}
|
||||
>
|
||||
<div
|
||||
className={cx(layerCss, css({
|
||||
backgroundImage: 'url(/images/backgrounds/SkyNightLayer03.png)',
|
||||
animationDuration: '125s'
|
||||
}))}
|
||||
>
|
||||
|
||||
<Center
|
||||
className={cx(layerCss, css({
|
||||
backgroundImage: 'url(/images/backgrounds/SkyNightLayer04.png)',
|
||||
animationDuration: '100s'
|
||||
}))}
|
||||
>
|
||||
<div
|
||||
className={cx(card({ variant: 'glass' }).body, container({}))}
|
||||
>
|
||||
<div
|
||||
className={card({ variant: 'glass' }).content}
|
||||
>
|
||||
<Typography variant="h2" align="center">Redes Sociales</Typography>
|
||||
<Typography variant="body1">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptate deleniti dolore quas sed nemo sit, officia in rem nesciunt quisquam possimus ab! Labore sed reprehenderit quae, hic earum tempora placeat cumque id eos itaque perferendis nulla officia fuga porro, quis, unde facere accusamus repudiandae non?
|
||||
</Typography>
|
||||
<Center>
|
||||
<a
|
||||
className={button()}
|
||||
href="/links"
|
||||
>
|
||||
Nuestros Links
|
||||
</a>
|
||||
</Center>
|
||||
</div>
|
||||
</div>
|
||||
</Center>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
export default Social
|
||||
@@ -0,0 +1,19 @@
|
||||
'use client'
|
||||
import store from '@/state/store'
|
||||
import { type FC, type ReactNode } from 'react'
|
||||
import { Provider } from 'react-redux'
|
||||
|
||||
export interface StateProviderProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const StateProvider: FC<StateProviderProps> = ({ children }) => {
|
||||
return (
|
||||
<Provider
|
||||
store={store}
|
||||
>
|
||||
{children}
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
export default StateProvider
|
||||
@@ -0,0 +1,123 @@
|
||||
import { css, cx } from '@/styled-system/css'
|
||||
import { Container } from '@/styled-system/jsx'
|
||||
import { center } from '@/styled-system/patterns'
|
||||
import { button, card, iconButton } from '@/styled-system/recipes'
|
||||
import { type TeamMember } from '@/types/User'
|
||||
import { faFacebook, faInstagram, faTwitch, faTwitter, faYoutube } from '@fortawesome/free-brands-svg-icons'
|
||||
import { faGlobe } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import NextImage from 'next/image'
|
||||
import NextLink from 'next/link'
|
||||
import { type FC } from 'react'
|
||||
|
||||
const team: TeamMember[] = [
|
||||
{
|
||||
image: '/images/team/SrJuggernaut.png',
|
||||
name: 'SrJuggernaut',
|
||||
role: 'administrator',
|
||||
description: 'Soy desarrollador web y me gusta jugar videojuegos.',
|
||||
socialNetworks: [
|
||||
{ url: 'https://www.facebook.com/SrJuggernaut', label: 'SrJuggernaut Facebook', icon: faFacebook },
|
||||
{ url: 'https://twitter.com/SrJuggernaut', label: 'SrJuggernaut Twitter', icon: faTwitter },
|
||||
{ url: 'https://youtube.com/juggernautplays', label: 'SrJuggernaut YouTube', icon: faYoutube },
|
||||
{ url: 'https://twitch.tv/juggernautplays', label: 'SrJuggernaut Twitch', icon: faTwitch },
|
||||
{ url: 'https://www.instagram.com/sr_juggernaut', label: 'SrJuggernaut Instagram', icon: faInstagram },
|
||||
{ url: 'https://srjuggernaut.dev/', label: 'SrJuggernaut Website', icon: faGlobe }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const Team: FC = () => {
|
||||
return (
|
||||
<section
|
||||
className={center({
|
||||
minHeight: '75vh',
|
||||
backgroundImage: 'url(/images/backgrounds/MysteriousForest.jpg)'
|
||||
})}
|
||||
>
|
||||
|
||||
<Container>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 'medium',
|
||||
flexWrap: 'wrap'
|
||||
})}
|
||||
>
|
||||
{team.map((member, index) => (
|
||||
<div
|
||||
key={`team-member-${index}`}
|
||||
className={cx(card({ variant: 'retro' }).body, css({
|
||||
maxWidth: '300px',
|
||||
textAlign: 'center'
|
||||
}))}
|
||||
>
|
||||
<div
|
||||
className={cx(card({ variant: 'retro' }).media, center())}
|
||||
>
|
||||
<NextImage
|
||||
src={member.image}
|
||||
alt={member.name}
|
||||
width={120}
|
||||
height={120}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={card({ variant: 'retro' }).content}
|
||||
>
|
||||
<h3>{member.name}</h3>
|
||||
<p>{member.description}</p>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
gap: 'small',
|
||||
flexWrap: 'wrap'
|
||||
})}
|
||||
>
|
||||
{member.socialNetworks.map((socialNetwork, index) => (
|
||||
<a
|
||||
key={`team-member-${index}-social-network`}
|
||||
className={iconButton()}
|
||||
href={socialNetwork.url}
|
||||
>
|
||||
<FontAwesomeIcon icon={socialNetwork.icon} />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-evenly',
|
||||
gap: 'medium',
|
||||
paddingBlock: 'large',
|
||||
width: '100%'
|
||||
})}
|
||||
>
|
||||
<NextLink
|
||||
className={button({ color: 'info' })}
|
||||
href="/equipo"
|
||||
>
|
||||
Ver el equipo completo
|
||||
</NextLink>
|
||||
<NextLink
|
||||
className={button({ color: 'primary' })}
|
||||
href="/equipo/unirse"
|
||||
>
|
||||
Únete al equipo
|
||||
</NextLink>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
export default Team
|
||||
@@ -0,0 +1,100 @@
|
||||
import Typography from '@/components/ui/Typography'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { Container } from '@/styled-system/jsx'
|
||||
import { faChevronRight } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import type { FC } from 'react'
|
||||
|
||||
const ClanesPage: FC = () => {
|
||||
return (
|
||||
<Container>
|
||||
<Typography variant="h1" align="center">Clanes</Typography>
|
||||
<Typography variant="body1">Los clanes son espacios donde compartir nuestros gustos con otros usuarios, dándonos la oportunidad de organizar proyectos y eventos en los cuales formar parte.</Typography>
|
||||
<div
|
||||
className={css({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { base: '1fr 1fr', smDown: '1fr' },
|
||||
gap: 'medium'
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
<Typography variant="h2">Beneficios de los clanes</Typography>
|
||||
<Typography variant="body1">La intención de EntGamers es brindar beneficios a los clanes que les permitan operar en un ambiente de comunicación y colaboración.</Typography>
|
||||
<ul className="fa-ul">
|
||||
<li>
|
||||
<span className="fa-li"><FontAwesomeIcon icon={faChevronRight} /></span>
|
||||
{' '}
|
||||
Espacio en el servidor de Discord.
|
||||
</li>
|
||||
<li>
|
||||
<span className="fa-li"><FontAwesomeIcon icon={faChevronRight} /></span>
|
||||
{' '}
|
||||
Apoyo de la administración con proyectos y eventos.
|
||||
</li>
|
||||
<li>
|
||||
<span className="fa-li">
|
||||
<FontAwesomeIcon icon={faChevronRight} />
|
||||
</span>
|
||||
{' '}
|
||||
Apoyo del equipo de moderación.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="h2">Requisitos para formar un clan</Typography>
|
||||
<Typography variant="body1">Todos los clanes deben cumplir con los siguientes requisitos:</Typography>
|
||||
<ul className="fa-ul">
|
||||
<li>
|
||||
<span className="fa-li">
|
||||
<FontAwesomeIcon icon={faChevronRight} />
|
||||
</span>
|
||||
{' '}
|
||||
Tener un encargado.
|
||||
</li>
|
||||
<li>
|
||||
<span className="fa-li">
|
||||
<FontAwesomeIcon icon={faChevronRight} />
|
||||
</span>
|
||||
{' '}
|
||||
Fomentar el compañerismo y la comunidad.
|
||||
</li>
|
||||
<li>
|
||||
<span className="fa-li">
|
||||
<FontAwesomeIcon icon={faChevronRight} />
|
||||
</span>
|
||||
{' '}
|
||||
Aportar contenido de forma periódica para la comunidad.
|
||||
</li>
|
||||
<li>
|
||||
<span className="fa-li">
|
||||
<FontAwesomeIcon icon={faChevronRight} />
|
||||
</span>
|
||||
{' '}
|
||||
Realizar al menos una actividad mensual con los integrantes.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<Typography variant="h2">Clanes activos</Typography>
|
||||
<div
|
||||
className={css({
|
||||
'backgroundColor': 'info',
|
||||
'color': 'info.contrast',
|
||||
'borderRadius': 'medium',
|
||||
'padding': 'medium',
|
||||
'marginBlock': 'medium',
|
||||
'& a': {
|
||||
color: 'info.contrast',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
})}
|
||||
>
|
||||
Esta sección está en construcción. Puedes ver los clanes activos en nuestro
|
||||
{' '}
|
||||
<a href="http://discord.gg/nqwzHJC">Servidor de Discord</a>
|
||||
.
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
export default ClanesPage
|
||||
@@ -0,0 +1,79 @@
|
||||
'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,114 @@
|
||||
'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, formik])
|
||||
|
||||
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, user, formik, 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
|
||||
@@ -0,0 +1,73 @@
|
||||
'use client'
|
||||
import TeamApplications from '@/app/dashboard/_components/TeamApplications'
|
||||
import Button from '@/components/ui/Button'
|
||||
import ButtonGroup from '@/components/ui/ButtonGroup'
|
||||
import Typography from '@/components/ui/Typography'
|
||||
import useSession from '@/hooks/useSession'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { ADMIN_CLAN_ID } from 'entgamers-database/frontend/clanes/administrative'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { useState, type FC } from 'react'
|
||||
|
||||
type Tab = undefined | 'teamApplications'
|
||||
|
||||
const DashboardTabs: FC = () => {
|
||||
const { clanes, belongToClan } = useSession('/login')
|
||||
|
||||
const [currentTab, setCurrentTab] = useState<Tab>(undefined)
|
||||
|
||||
return (
|
||||
<>
|
||||
{clanes !== undefined && (
|
||||
<ButtonGroup
|
||||
fullWidth
|
||||
>
|
||||
<Button fullWidth onClick={() => { setCurrentTab(undefined) }} disabled={currentTab === undefined}>
|
||||
Panel
|
||||
</Button>
|
||||
{belongToClan(ADMIN_CLAN_ID) && (
|
||||
<Button fullWidth onClick={() => { setCurrentTab('teamApplications') }} disabled={currentTab === 'teamApplications'}>
|
||||
Aplicaciones
|
||||
</Button>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
)}
|
||||
<div
|
||||
className={css({
|
||||
overflow: 'hidden',
|
||||
marginTop: 'medium'
|
||||
})}
|
||||
>
|
||||
<AnimatePresence
|
||||
initial={false}
|
||||
mode="wait"
|
||||
>
|
||||
{currentTab === undefined && (
|
||||
<motion.div
|
||||
transition={{ duration: 0.15, ease: 'easeInOut' }}
|
||||
initial={{ opacity: 0, x: '-100%' }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: '100%' }}
|
||||
key="dashboard"
|
||||
>
|
||||
<Typography variant="body1">Selecciona una de las opciones de arriba para comenzar.</Typography>
|
||||
</motion.div>
|
||||
)}
|
||||
{currentTab === 'teamApplications' && (
|
||||
<motion.div
|
||||
transition={{ duration: 0.15, ease: 'easeInOut' }}
|
||||
initial={{ opacity: 0, x: '-100%' }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: '100%' }}
|
||||
key="teamApplications"
|
||||
>
|
||||
<TeamApplications />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DashboardTabs
|
||||
@@ -0,0 +1,14 @@
|
||||
import ApplicationsList from '@/app/dashboard/_components/teamApplications/ApplicationsList'
|
||||
import Typography from '@/components/ui/Typography'
|
||||
import { type FC } from 'react'
|
||||
|
||||
const TeamApplications: FC = () => {
|
||||
return (
|
||||
<>
|
||||
<Typography variant="h2">Aplicaciones</Typography>
|
||||
<ApplicationsList />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TeamApplications
|
||||
@@ -0,0 +1,48 @@
|
||||
import DebouncedInput from '@/components/ui/form/DebouncedInput'
|
||||
import { type Column } from '@tanstack/react-table'
|
||||
import { type TeamApplication, type TeamApplicationRole, type TeamApplicationStatus } from 'entgamers-database/types/teamApplications'
|
||||
import { type FC } from 'react'
|
||||
import RoleSelector from './RoleSelector'
|
||||
import StatusSelector from './StatusSelector'
|
||||
|
||||
export interface ApplicationsFilterProps {
|
||||
column: Column<TeamApplication, unknown>
|
||||
}
|
||||
|
||||
const ApplicationsFilter: FC<ApplicationsFilterProps> = ({ column }) => {
|
||||
const columnFilterValue = column.getFilterValue()
|
||||
switch (column.id) {
|
||||
case 'status':
|
||||
return (
|
||||
<StatusSelector
|
||||
id={`${column.id}-status-filter`}
|
||||
value={columnFilterValue as TeamApplicationStatus}
|
||||
onChange={(value) => { column.setFilterValue(value) }}
|
||||
allowEmpty
|
||||
/>
|
||||
)
|
||||
case 'role':
|
||||
return (
|
||||
<RoleSelector
|
||||
id={`${column.id}-role-filter`}
|
||||
value={columnFilterValue as TeamApplicationRole}
|
||||
onChange={(value) => { column.setFilterValue(value) }}
|
||||
allowEmpty
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<DebouncedInput
|
||||
fullWidth
|
||||
type="text"
|
||||
value={(columnFilterValue ?? '') as string}
|
||||
onChange={(value) => { column.setFilterValue(value) }}
|
||||
placeholder="Buscar..."
|
||||
className="w-36 border shadow rounded"
|
||||
list={column.id + 'list'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default ApplicationsFilter
|
||||
@@ -0,0 +1,280 @@
|
||||
import IconButton from '@/components/ui/IconButton'
|
||||
import { Table, TableBody, TableCell, TableContainer, TableHead, TableHeadCell, TableRow } from '@/components/ui/Table'
|
||||
import useManageError from '@/hooks/useManageError'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { formatDate } from '@/utilities/date'
|
||||
import type { TeamApplication, TeamApplicationList } from '@/utilities/teamApplication'
|
||||
import { faChevronLeft, faChevronRight, faSort, faSortAsc, faSortDesc } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { createColumnHelper, flexRender, getCoreRowModel, useReactTable, type ColumnFiltersState, type PaginationState, type RowData, type SortingState } from '@tanstack/react-table'
|
||||
import { getAllTeamApplications, updateTeamApplication } from 'entgamers-database/frontend/database/teamApplications'
|
||||
import { Query } from 'entgamers-database/lib/appwrite'
|
||||
import { useEffect, useState, type FC } from 'react'
|
||||
import ApplicationsFilter from './ApplicationsFilter'
|
||||
import StatusUpdater from './StatusUpdater'
|
||||
|
||||
declare module '@tanstack/table-core' {
|
||||
interface TableMeta<TData extends RowData> {
|
||||
updateRow: (id: string, value: Partial<TData>) => Promise<void>
|
||||
}
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<TeamApplication>()
|
||||
|
||||
const columns = [
|
||||
columnHelper.accessor('$id', {
|
||||
header: 'ID',
|
||||
enableColumnFilter: false
|
||||
}),
|
||||
columnHelper.accessor('status', {
|
||||
header: 'Estado',
|
||||
cell: StatusUpdater,
|
||||
getUniqueValues() {
|
||||
return ['Pending', 'Accepted', 'Rejected']
|
||||
}
|
||||
}),
|
||||
columnHelper.accessor('role', {
|
||||
header: 'Rol'
|
||||
}),
|
||||
columnHelper.accessor('name', {
|
||||
header: 'Nombre'
|
||||
}),
|
||||
columnHelper.accessor('message', {
|
||||
header: 'Mensaje',
|
||||
minSize: 450
|
||||
}),
|
||||
columnHelper.accessor('email', {
|
||||
header: 'Correo'
|
||||
}),
|
||||
columnHelper.accessor('discord', {
|
||||
header: 'Discord'
|
||||
}),
|
||||
columnHelper.accessor('$createdAt', {
|
||||
header: 'Creado',
|
||||
enableColumnFilter: false,
|
||||
cell: (info) => {
|
||||
return formatDate(new Date(info.getValue()))
|
||||
}
|
||||
}),
|
||||
columnHelper.accessor('$updatedAt', {
|
||||
header: 'Actualizado',
|
||||
enableColumnFilter: false,
|
||||
cell: (info) => {
|
||||
return formatDate(new Date(info.getValue()))
|
||||
}
|
||||
})
|
||||
]
|
||||
|
||||
const ApplicationsList: FC = () => {
|
||||
const { manageError } = useManageError()
|
||||
const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: 10 })
|
||||
const [sorting, setSorting] = useState<SortingState>([{ id: '$createdAt', desc: true }])
|
||||
const [applications, setApplications] = useState<TeamApplicationList>({ total: 0, documents: [] })
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([{ id: 'status', value: 'Pending' }])
|
||||
|
||||
const table = useReactTable({
|
||||
data: applications.documents,
|
||||
columns,
|
||||
initialState: {
|
||||
columnVisibility: {
|
||||
$id: false,
|
||||
email: false
|
||||
}
|
||||
},
|
||||
state: {
|
||||
pagination,
|
||||
sorting,
|
||||
columnFilters
|
||||
},
|
||||
meta: {
|
||||
updateRow: async (id: string, value: Partial<TeamApplication>) => {
|
||||
const updatedTeamApplication = await updateTeamApplication(id, value)
|
||||
const newApplications = applications.documents.map((application) => application.$id === updatedTeamApplication.$id ? updatedTeamApplication : application)
|
||||
setApplications({ total: applications.total, documents: newApplications })
|
||||
}
|
||||
},
|
||||
manualPagination: true,
|
||||
rowCount: applications.total,
|
||||
onPaginationChange: setPagination,
|
||||
enableSorting: true,
|
||||
manualSorting: true,
|
||||
onSortingChange: setSorting,
|
||||
manualFiltering: true,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getCoreRowModel: getCoreRowModel()
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const query: string[] = [
|
||||
Query.limit(pagination.pageSize),
|
||||
Query.offset(pagination.pageIndex * pagination.pageSize)
|
||||
]
|
||||
if (sorting.length > 0) {
|
||||
const sort: string = sorting[0].desc ? Query.orderDesc(sorting[0].id) : Query.orderAsc(sorting[0].id)
|
||||
query.push(sort)
|
||||
}
|
||||
if (columnFilters.length > 0) {
|
||||
const filter: string[] = columnFilters.map((columnFilter) => {
|
||||
return Query.contains(columnFilter.id, columnFilter.value as string)
|
||||
})
|
||||
query.push(...filter)
|
||||
}
|
||||
getAllTeamApplications(query)
|
||||
.then((applicationList) => { setApplications(applicationList) })
|
||||
.catch((error) => {
|
||||
if (error instanceof Error && error.name === 'AbortError') return
|
||||
manageError(error, 'Error al obtener las aplicaciones', 'Error desconocido al obtener las aplicaciones', 'error')
|
||||
})
|
||||
}, [pagination, sorting, columnFilters, manageError])
|
||||
|
||||
// TODO: Better UI Controls for: column visibility. Quantity selector.
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
gap: 'small',
|
||||
marginBottom: 'small'
|
||||
})}
|
||||
>
|
||||
{table.getAllLeafColumns().map((column) => (
|
||||
<div key={column.id}>
|
||||
<label htmlFor={`${column.id}-view`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`${column.id}-view`}
|
||||
checked={column.getIsVisible()}
|
||||
onChange={column.getToggleVisibilityHandler()}
|
||||
/>
|
||||
{' '}
|
||||
{column.columnDef.header?.toString() ?? column.id}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHeadCell
|
||||
key={header.id}
|
||||
className={css({
|
||||
'verticalAlign': 'top',
|
||||
'position': 'relative',
|
||||
'&:hover > [data-is-resizing]': {
|
||||
backgroundColor: 'border'
|
||||
}
|
||||
})}
|
||||
style={{ minWidth: header.getSize() }}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 'small'
|
||||
})}
|
||||
>
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext()) }
|
||||
{header.column.getCanSort() && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
<FontAwesomeIcon icon={header.column.getIsSorted() === 'asc' ? faSortAsc : header.column.getIsSorted() === 'desc' ? faSortDesc : faSort} size="sm" />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
{header.column.getCanFilter()
|
||||
? (
|
||||
<ApplicationsFilter column={header.column} />
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={css({
|
||||
'position': 'absolute',
|
||||
'top': 0,
|
||||
'right': 0,
|
||||
'height': '100%',
|
||||
'width': '5px',
|
||||
'cursor': 'col-resize',
|
||||
'userSelect': 'none',
|
||||
'touchAction': 'none',
|
||||
'&:hover': {
|
||||
backgroundColor: 'border'
|
||||
},
|
||||
'&[data-is-resizing=true]': {
|
||||
backgroundColor: 'primary'
|
||||
}
|
||||
})}
|
||||
style={{
|
||||
transform: `translateX(${1 * (table.getState().columnSizingInfo
|
||||
.deltaOffset ?? 0)}px)`
|
||||
}}
|
||||
data-is-resizing={header.column.getIsResizing()}
|
||||
onDoubleClick={header.getResizeHandler()}
|
||||
onTouchStart={header.getResizeHandler()}
|
||||
onMouseDown={header.getResizeHandler()}
|
||||
/>
|
||||
</TableHeadCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
gap: 'small',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingBlock: 'medium'
|
||||
})}
|
||||
>
|
||||
<IconButton
|
||||
onClick={() => { table.previousPage() }}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} />
|
||||
</IconButton>
|
||||
Pagina
|
||||
{' '}
|
||||
{table.getState().pagination.pageIndex + 1}
|
||||
{' '}
|
||||
de
|
||||
{' '}
|
||||
{table.getPageCount()}
|
||||
<IconButton
|
||||
onClick={() => { table.nextPage() }}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronRight} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ApplicationsList
|
||||
@@ -0,0 +1,46 @@
|
||||
import { css } from '@/styled-system/css'
|
||||
import { type TeamApplicationRole } from 'entgamers-database/types/teamApplications'
|
||||
import { type FC } from 'react'
|
||||
|
||||
export interface RoleSelectorProps {
|
||||
id: string
|
||||
value: TeamApplicationRole
|
||||
onChange: (role: TeamApplicationRole) => void
|
||||
allowEmpty?: boolean
|
||||
}
|
||||
|
||||
const RoleSelector: FC<RoleSelectorProps> = ({ id, value, onChange, allowEmpty }) => {
|
||||
/* TODO: Change for Select UI Component when it's ready */
|
||||
return (
|
||||
<select
|
||||
id={`${id}-status`}
|
||||
className={css({
|
||||
'width': '100%',
|
||||
'border': 'none',
|
||||
'background': 'transparent',
|
||||
'color': 'inherit',
|
||||
'outline': 'none',
|
||||
'cursor': 'pointer',
|
||||
'fontSize': 'inherit',
|
||||
'fontWeight': 'inherit',
|
||||
'lineHeight': 'inherit',
|
||||
'padding': '0',
|
||||
'borderRadius': '0',
|
||||
'&:focus': {
|
||||
outline: 'none'
|
||||
}
|
||||
})}
|
||||
value={value}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value as TeamApplicationRole)
|
||||
}}
|
||||
>
|
||||
{allowEmpty === true && <option value="">Todos</option>}
|
||||
<option value="Admin">Administrador</option>
|
||||
<option value="Collaborator">Colaborador</option>
|
||||
<option value="Moderator">Moderador</option>
|
||||
</select>
|
||||
)
|
||||
}
|
||||
|
||||
export default RoleSelector
|
||||
@@ -0,0 +1,46 @@
|
||||
import { css } from '@/styled-system/css'
|
||||
import { type TeamApplicationStatus } from 'entgamers-database/types/teamApplications'
|
||||
import { type FC } from 'react'
|
||||
|
||||
export interface StatusSelectorProps {
|
||||
id: string
|
||||
value: TeamApplicationStatus
|
||||
onChange: (status: TeamApplicationStatus) => void
|
||||
allowEmpty?: boolean
|
||||
}
|
||||
|
||||
const StatusSelector: FC<StatusSelectorProps> = ({ id, value, onChange, allowEmpty }) => {
|
||||
/* TODO: Change for Select UI Component when it's ready */
|
||||
return (
|
||||
<select
|
||||
id={`${id}-status`}
|
||||
className={css({
|
||||
'width': '100%',
|
||||
'border': 'none',
|
||||
'background': 'transparent',
|
||||
'color': 'inherit',
|
||||
'outline': 'none',
|
||||
'cursor': 'pointer',
|
||||
'fontSize': 'inherit',
|
||||
'fontWeight': 'inherit',
|
||||
'lineHeight': 'inherit',
|
||||
'padding': '0',
|
||||
'borderRadius': '0',
|
||||
'&:focus': {
|
||||
outline: 'none'
|
||||
}
|
||||
})}
|
||||
value={value}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value as TeamApplicationStatus)
|
||||
}}
|
||||
>
|
||||
{allowEmpty === true && <option value="">Todos</option>}
|
||||
<option value="Pending">Pendiente</option>
|
||||
<option value="Accepted">Aceptado</option>
|
||||
<option value="Rejected">Rechazado</option>
|
||||
</select>
|
||||
)
|
||||
}
|
||||
|
||||
export default StatusSelector
|
||||
@@ -0,0 +1,21 @@
|
||||
import { type CellContext } from '@tanstack/react-table'
|
||||
import { type TeamApplication, type TeamApplicationStatus } from 'entgamers-database/types/teamApplications'
|
||||
import { type FC } from 'react'
|
||||
import StatusSelector from './StatusSelector'
|
||||
|
||||
const StatusUpdater: FC<CellContext<TeamApplication, TeamApplicationStatus>> = ({ cell: { id, row }, table }) => {
|
||||
return (
|
||||
<>
|
||||
<StatusSelector
|
||||
id={`${id}-status`}
|
||||
value={row.original.status}
|
||||
onChange={(status) => {
|
||||
table.options.meta?.updateRow(row.original.$id, { status })
|
||||
.catch(console.error)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default StatusUpdater
|
||||
@@ -0,0 +1,15 @@
|
||||
import Typography from '@/components/ui/Typography'
|
||||
import { Container } from '@/styled-system/jsx'
|
||||
import { type FC } from 'react'
|
||||
import DashboardTabs from './_components/DashboardTabs'
|
||||
|
||||
const page: FC = () => {
|
||||
return (
|
||||
<Container>
|
||||
<Typography variant="h1" align="center">Panel de control</Typography>
|
||||
<DashboardTabs />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default page
|
||||
@@ -0,0 +1,234 @@
|
||||
import Typography from '@/components/ui/Typography'
|
||||
import { css, cx } from '@/styled-system/css'
|
||||
import { Container } from '@/styled-system/jsx'
|
||||
import { center } from '@/styled-system/patterns'
|
||||
import { button, card } from '@/styled-system/recipes'
|
||||
import { getClanMembers } from 'entgamers-database/backend/clanes'
|
||||
import { getUser } from 'entgamers-database/backend/users'
|
||||
import { ADMIN_CLAN_ID, COLLABORATOR_CLAN_ID, MODERATOR_CLAN_ID } from 'entgamers-database/lib/env'
|
||||
import { type UserList } from 'entgamers-database/types/user'
|
||||
import NextImage from 'next/image'
|
||||
import NextLink from 'next/link'
|
||||
import { type Models } from 'node-appwrite'
|
||||
import { type FC } from 'react'
|
||||
|
||||
interface GetTeamsResponse {
|
||||
admins: UserList & { id: string }
|
||||
moderators: UserList & { id: string }
|
||||
collaborators: UserList & { id: string }
|
||||
}
|
||||
|
||||
const getTeams = async (): Promise<GetTeamsResponse> => {
|
||||
// await ensureAdministrativeClans()
|
||||
|
||||
const adminMembers: Models.MembershipList = await getClanMembers(ADMIN_CLAN_ID)
|
||||
const moderatorMembers: Models.MembershipList = await getClanMembers(MODERATOR_CLAN_ID)
|
||||
const collaboratorMembers: Models.MembershipList = await getClanMembers(COLLABORATOR_CLAN_ID)
|
||||
|
||||
const adminsPromises = adminMembers.memberships.map(async (membership) => await getUser(membership.userId))
|
||||
const moderatorsPromises = moderatorMembers.memberships.map(async (membership) => await getUser(membership.userId))
|
||||
const collaboratorsPromises = collaboratorMembers.memberships.map(async (membership) => await getUser(membership.userId))
|
||||
|
||||
const [admins, moderators, collaborators] = await Promise.all([
|
||||
Promise.all(adminsPromises), Promise.all(moderatorsPromises), Promise.all(collaboratorsPromises)
|
||||
])
|
||||
|
||||
return { admins: { id: ADMIN_CLAN_ID, total: admins.length, users: admins }, moderators: { id: MODERATOR_CLAN_ID, total: moderators.length, users: moderators }, collaborators: { id: COLLABORATOR_CLAN_ID, total: collaborators.length, users: collaborators } }
|
||||
}
|
||||
|
||||
const EquipoPage: FC = async () => {
|
||||
const { admins, moderators, collaborators } = await getTeams()
|
||||
return (
|
||||
<Container>
|
||||
<Typography variant="h1" align="center">Equipo</Typography>
|
||||
<Typography variant="body1">
|
||||
El equipo de EntGamers está formado por personas que se dedican a la administración de la comunidad, a la organización de eventos y a la creación de contenido. EntGamers siempre intenta recompensar a sus miembros más activos, por lo que si quieres formar parte de nuestro equipo, ¡no dudes en contactar con nosotros!
|
||||
</Typography>
|
||||
<Typography variant="h2" align="center">Administradores</Typography>
|
||||
<Typography variant="body1">
|
||||
Los administradores son quienes se encargan de que todo funcione como es debido en la comunidad, desde la moderación de los grupos hasta la organización de eventos.
|
||||
</Typography>
|
||||
{admins.total >= 1
|
||||
? (
|
||||
<Container
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 'medium',
|
||||
flexWrap: 'wrap',
|
||||
padding: 'medium',
|
||||
width: '100%'
|
||||
})}
|
||||
>
|
||||
{admins.users.map((user, index) => (
|
||||
<div
|
||||
key={`admin-${index}`}
|
||||
className={cx(card({ variant: 'retro' }).body, css({
|
||||
maxWidth: '300px',
|
||||
textAlign: 'center'
|
||||
}))}
|
||||
>
|
||||
<div
|
||||
className={cx(card({ variant: 'retro' }).media, center())}
|
||||
>
|
||||
<NextImage
|
||||
src={user.prefs.profilePicture !== undefined && user.prefs.profilePicture.trim() !== '' ? user.prefs.profilePicture.trim() : '/images/EntGamers.png'}
|
||||
alt={user.name !== '' ? user.name : `Usuario ${index + 1} avatar`}
|
||||
width={120}
|
||||
height={120}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={card({ variant: 'retro' }).content}
|
||||
>
|
||||
<Typography variant="h3" align="center">{user.name !== '' ? user.name : `Usuario ${index + 1}`}</Typography>
|
||||
{user.prefs.bio !== undefined && user.prefs.bio !== '' && (
|
||||
<Typography variant="body1">{user.prefs.bio}</Typography>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Container>
|
||||
)
|
||||
: (
|
||||
<Typography variant="body2" color="info">
|
||||
Ups, parece que ahora mismo no hay administradores, pero en EntGamers siempre estamos estamos buscando gente que quiera organizar cosas para la comunidad, puedes contactarnos para formar parte de nuestro equipo haciendo click en el siguiente enlace.
|
||||
</Typography>
|
||||
)}
|
||||
<div className={center()}>
|
||||
<NextLink
|
||||
className={button({ color: 'info' })}
|
||||
href={`/equipo/unirse?role=${ADMIN_CLAN_ID}`}
|
||||
>
|
||||
¡Quiero ser Administrador!
|
||||
</NextLink>
|
||||
</div>
|
||||
<Typography variant="h2" align="center">Moderadores</Typography>
|
||||
<Typography variant="body1">
|
||||
Los moderadores son los encargados de mantener el orden en los grupos de la comunidad, así como de ayudar a los usuarios a resolver sus dudas.
|
||||
</Typography>
|
||||
{moderators.total >= 1
|
||||
? (
|
||||
<Container
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 'medium',
|
||||
flexWrap: 'wrap',
|
||||
padding: 'medium',
|
||||
width: '100%'
|
||||
})}
|
||||
>
|
||||
{moderators.users.map((user, index) => (
|
||||
<div
|
||||
key={`moderator-${index}`}
|
||||
className={cx(card({ variant: 'retro' }).body, css({
|
||||
maxWidth: '300px',
|
||||
textAlign: 'center'
|
||||
}))}
|
||||
>
|
||||
<div
|
||||
className={cx(card({ variant: 'retro' }).media, center())}
|
||||
>
|
||||
<NextImage
|
||||
src={user.prefs.profilePicture !== undefined && user.prefs.profilePicture.trim() !== '' ? user.prefs.profilePicture.trim() : '/images/EntGamers.png'}
|
||||
alt={user.name !== '' ? user.name : `Usuario ${index + 1} avatar`}
|
||||
width={120}
|
||||
height={120}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={card({ variant: 'retro' }).content}
|
||||
>
|
||||
<Typography variant="h3" align="center">{user.name !== '' ? user.name : `Usuario ${index + 1}`}</Typography>
|
||||
{user.prefs.bio !== undefined && user.prefs.bio !== '' && (
|
||||
<Typography variant="body1">{user.prefs.bio}</Typography>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Container>
|
||||
)
|
||||
: (
|
||||
<Typography variant="body2" color="info">
|
||||
Ups, parece que ahora mismo no hay moderadores, pero en EntGamers siempre estamos buscando gente que quiera ayudar a la comunidad. si quieres ser moderador, puedes hacer click en el botón de abajo.
|
||||
</Typography>
|
||||
)}
|
||||
<div className={center()}>
|
||||
<NextLink
|
||||
className={button({ color: 'info' })}
|
||||
href={`/equipo/unirse?role=${moderators.id}`}
|
||||
>
|
||||
¡Quiero ser moderador!
|
||||
</NextLink>
|
||||
</div>
|
||||
<Typography variant="h2" align="center">Colaboradores</Typography>
|
||||
<Typography variant="body1">
|
||||
Los colaboradores son los encargados de crear contenido para la comunidad, como artículos, tutoriales, vídeos, eventos etc.
|
||||
</Typography>
|
||||
{collaborators.total >= 1
|
||||
? (
|
||||
<Container
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 'medium',
|
||||
flexWrap: 'wrap',
|
||||
padding: 'medium',
|
||||
width: '100%'
|
||||
})}
|
||||
>
|
||||
{collaborators.users.map((user, index) => (
|
||||
<div
|
||||
key={`collaborator-${index}`}
|
||||
className={cx(card({ variant: 'retro' }).body, css({
|
||||
maxWidth: '300px',
|
||||
textAlign: 'center'
|
||||
}))}
|
||||
>
|
||||
<div
|
||||
className={cx(card({ variant: 'retro' }).media, center())}
|
||||
>
|
||||
<NextImage
|
||||
src={user.prefs.profilePicture !== undefined && user.prefs.profilePicture.trim() !== '' ? user.prefs.profilePicture.trim() : '/images/EntGamers.png'}
|
||||
alt={user.name !== '' ? user.name : `Usuario ${index + 1} avatar`}
|
||||
width={120}
|
||||
height={120}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={card({ variant: 'retro' }).content}
|
||||
>
|
||||
<Typography variant="h3" align="center">{user.name !== '' ? user.name : `Usuario ${index + 1}`}</Typography>
|
||||
{user.prefs.bio !== undefined && user.prefs.bio !== '' && (
|
||||
<Typography variant="body1">{user.prefs.bio}</Typography>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Container>
|
||||
)
|
||||
: (
|
||||
<Typography variant="body2" color="info">
|
||||
Ups, parece que ahora mismo no hay colaboradores, pero en EntGamers siempre estamos buscando gente que quiera ayudar a la comunidad. si quieres ser colaborador, puedes hacer click en el botón de abajo.
|
||||
</Typography>
|
||||
)}
|
||||
<div className={center()}>
|
||||
<NextLink
|
||||
className={button({ color: 'info' })}
|
||||
href={`/equipo/unirse?role=${collaborators.id}`}
|
||||
>
|
||||
¡Quiero ser colaborador!
|
||||
</NextLink>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default EquipoPage
|
||||
@@ -0,0 +1,409 @@
|
||||
'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 TextArea from '@/components/ui/form/TextArea'
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch'
|
||||
import useManageError from '@/hooks/useManageError'
|
||||
import { addAlert } from '@/state/feedbackSlice'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { teamApplicationDataSchema } from '@/utilities/teamApplication'
|
||||
import { faChevronRight } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon, type FontAwesomeIconProps } from '@fortawesome/react-fontawesome'
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { ADMIN_CLAN_ID, COLLABORATOR_CLAN_ID, MODERATOR_CLAN_ID } from 'entgamers-database/frontend/clanes/administrative'
|
||||
import { createTeamApplication, type TeamApplicationData } from 'entgamers-database/frontend/database/teamApplications'
|
||||
import { useFormik } from 'formik'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { usePathname, useSearchParams } from 'next/navigation'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, type FC } from 'react'
|
||||
|
||||
const ApplyForm: FC = () => {
|
||||
const searchParams = useSearchParams()
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
const { manageError } = useManageError()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const formik = useFormik <Omit<TeamApplicationData, 'status'>>({
|
||||
initialValues: {
|
||||
name: '',
|
||||
email: '',
|
||||
discord: '',
|
||||
message: '',
|
||||
role: 'Moderator'
|
||||
},
|
||||
onSubmit: async (values) => {
|
||||
try {
|
||||
await createTeamApplication(values)
|
||||
dispatch(addAlert({
|
||||
id: nanoid(),
|
||||
title: 'Formulario enviado',
|
||||
message: 'Gracias por interesarte en unirte al equipo',
|
||||
severity: 'success'
|
||||
}))
|
||||
} catch (error) {
|
||||
manageError(error, 'Error al enviar el formulario', 'Error desconocido al enviar el formulario', 'error')
|
||||
}
|
||||
},
|
||||
validationSchema: teamApplicationDataSchema,
|
||||
validateOnMount: true
|
||||
})
|
||||
useEffect(() => {
|
||||
if (searchParams.has('role')) {
|
||||
formik.setFieldValue('role', searchParams.get('role'))
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
})
|
||||
.finally(() => {
|
||||
router.replace(pathname)
|
||||
})
|
||||
}
|
||||
}, [searchParams, formik, pathname, router])
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={formik.handleSubmit}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: { base: 'column', md: 'row' },
|
||||
justifyContent: 'space-evenly',
|
||||
gap: 'medium'
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
formik.setFieldValue('role', MODERATOR_CLAN_ID)
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
})
|
||||
}}
|
||||
disabled={formik.values.role === MODERATOR_CLAN_ID}
|
||||
>
|
||||
Moderador
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
formik.setFieldValue('role', ADMIN_CLAN_ID)
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
})
|
||||
}}
|
||||
disabled={formik.values.role === ADMIN_CLAN_ID}
|
||||
>
|
||||
Administrador
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
formik.setFieldValue('role', COLLABORATOR_CLAN_ID)
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
})
|
||||
}}
|
||||
disabled={formik.values.role === 'Collaborator'}
|
||||
>
|
||||
Colaborador
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { base: '1fr', md: '1fr 1fr' },
|
||||
gap: 'medium'
|
||||
})}
|
||||
>
|
||||
{formik.submitCount <= 0
|
||||
? (
|
||||
<div
|
||||
className={css({
|
||||
order: { base: 2, md: 1 }
|
||||
})}
|
||||
>
|
||||
<FormGroup>
|
||||
<label htmlFor="name">Nombre</label>
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
value={formik.values.name}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
/>
|
||||
{formik.touched.name !== undefined && formik.errors.name !== undefined
|
||||
? (
|
||||
<Typography variant="caption" color="danger">
|
||||
{formik.errors.name}
|
||||
</Typography>
|
||||
)
|
||||
: (
|
||||
<Typography variant="caption" color="info">
|
||||
Tu nombre.
|
||||
</Typography>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<label htmlFor="email">Email</label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={formik.values.email}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
/>
|
||||
{formik.touched.email !== undefined && formik.errors.email !== undefined
|
||||
? (
|
||||
<Typography variant="caption" color="danger">
|
||||
{formik.errors.email}
|
||||
</Typography>
|
||||
)
|
||||
: (
|
||||
<Typography variant="caption" color="info">
|
||||
Tu email, para poder contactarte.
|
||||
</Typography>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<label htmlFor="discord">Nombre de Discord</label>
|
||||
<Input
|
||||
id="discord"
|
||||
type="text"
|
||||
value={formik.values.discord}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
/>
|
||||
{formik.touched.discord !== undefined && formik.errors.discord !== undefined
|
||||
? (
|
||||
<Typography variant="caption" color="danger">
|
||||
{formik.errors.discord}
|
||||
</Typography>
|
||||
)
|
||||
: (
|
||||
<Typography variant="caption" color="info">
|
||||
Tu nombre de Discord, para poder contactarte.
|
||||
</Typography>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<label htmlFor="message">Mensaje</label>
|
||||
<TextArea
|
||||
id="message"
|
||||
value={formik.values.message}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
/>
|
||||
{formik.touched.message !== undefined && formik.errors.message !== undefined
|
||||
? (
|
||||
<Typography variant="caption" color="danger">
|
||||
{formik.errors.message}
|
||||
</Typography>
|
||||
)
|
||||
: (
|
||||
<Typography variant="caption" color="info">
|
||||
¿Por que te gustaría unirte al equipo?, ¿Que te gustaría hacer?, etc.
|
||||
</Typography>
|
||||
)}
|
||||
</FormGroup>
|
||||
<div
|
||||
className={css({
|
||||
paddingBlock: 'medium'
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!formik.isValid || !formik.dirty}
|
||||
fullWidth
|
||||
|
||||
>
|
||||
Enviar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div
|
||||
className={css({
|
||||
order: { base: 2, md: 1 },
|
||||
paddingBlock: 'medium'
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
backgroundColor: 'surface',
|
||||
borderRadius: 'medium',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
})}
|
||||
>
|
||||
<Typography variant="h2" align="center">¡Gracias por interesarte en unirte al equipo!</Typography>
|
||||
<Typography variant="body1">
|
||||
El equipo de EntGamers se pondrá en contacto contigo a la brevedad posible.
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={css({
|
||||
overflow: 'hidden',
|
||||
order: { base: 1, md: 2 },
|
||||
paddingBlock: 'medium'
|
||||
})}
|
||||
>
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
{formik.values.role === MODERATOR_CLAN_ID && (
|
||||
<motion.div
|
||||
key="motion-moderator"
|
||||
transition={{ duration: 0.15, ease: 'easeInOut' }}
|
||||
initial={{ opacity: 0, x: '-250px' }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: '250px' }}
|
||||
>
|
||||
<Typography variant="h2" align="center">Moderadores</Typography>
|
||||
<Typography variant="body1">
|
||||
El equipo de moderación de EntGamers se encarga de moderar los distintos espacios en los que se desenvuelve la comunidad, como los grupos de Facebook, Discord, Etc.
|
||||
</Typography>
|
||||
<Typography variant="h3">Requisitos</Typography>
|
||||
<ul className="fa-ul">
|
||||
<li>
|
||||
<span className="fa-li">
|
||||
<FontAwesomeIcon icon={faChevronRight as FontAwesomeIconProps['icon']} />
|
||||
</span>
|
||||
{' '}
|
||||
<strong>Imparcialidad</strong>
|
||||
<br />
|
||||
La comunidad esta conformada por amigos y conocidos, por lo tanto es importante poder actuar de forma imparcial y responsable.
|
||||
</li>
|
||||
</ul>
|
||||
<Typography variant="h3">Beneficios</Typography>
|
||||
<ul className="fa-ul">
|
||||
<li>
|
||||
<span className="fa-li">
|
||||
<FontAwesomeIcon icon={faChevronRight as FontAwesomeIconProps['icon']} />
|
||||
</span>
|
||||
{' '}
|
||||
<strong>Experiencia</strong>
|
||||
<br />
|
||||
Uno de los objetivos de la comunidad es brindar experiencia en gestión y desarrollo de proyectos equiparable a un entorno laboral, que sea comprobable y útil.
|
||||
</li>
|
||||
</ul>
|
||||
</motion.div>
|
||||
)}
|
||||
{formik.values.role === COLLABORATOR_CLAN_ID && (
|
||||
<motion.div
|
||||
key="motion-collaborator"
|
||||
transition={{ duration: 0.15, ease: 'easeInOut' }}
|
||||
initial={{ opacity: 0, x: '-250px' }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: '250px' }}
|
||||
>
|
||||
<Typography variant="h2" align="center">Colaborador</Typography>
|
||||
<Typography variant="body1">
|
||||
Los colaboradores son personas ajenas al staff central de EntGamers que nos ayudan a traer contenido, eventos y actividades a la comunidad.
|
||||
</Typography>
|
||||
<Typography variant="h3">Requisitos</Typography>
|
||||
<ul className="fa-ul">
|
||||
<li>
|
||||
<span className="fa-li">
|
||||
<FontAwesomeIcon icon={faChevronRight as FontAwesomeIconProps['icon']} />
|
||||
</span>
|
||||
{' '}
|
||||
<strong>Profesionalismo</strong>
|
||||
<br />
|
||||
La comunidad siempre intenta conseguir el mayor nivel de calidad en todos sus proyectos, por lo que buscamos gente dispuesta a otorgar este nivel de profesionalismo para el disfrute de la comunidad.
|
||||
</li>
|
||||
</ul>
|
||||
<Typography variant="h3">Beneficios</Typography>
|
||||
<ul className="fa-ul">
|
||||
<li>
|
||||
<span className="fa-li">
|
||||
<FontAwesomeIcon icon={faChevronRight as FontAwesomeIconProps['icon']} />
|
||||
</span>
|
||||
{' '}
|
||||
<strong>Apoyo</strong>
|
||||
<br />
|
||||
Puedes contar con el apoyo de la comunidad para tus proyectos, ya sea en forma de difusión, asesoramiento o recursos.
|
||||
</li>
|
||||
</ul>
|
||||
</motion.div>
|
||||
)}
|
||||
{formik.values.role === ADMIN_CLAN_ID && (
|
||||
<motion.div
|
||||
key="motion-administrator"
|
||||
transition={{ duration: 0.15, ease: 'easeInOut' }}
|
||||
initial={{ opacity: 0, x: '-250px' }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: '250px' }}
|
||||
>
|
||||
<Typography variant="h2" align="center">Administradores</Typography>
|
||||
<Typography variant="body1">
|
||||
Los administradores son quienes se encargan de que todo funcione como es debido en la comunidad, desde la moderación de los grupos hasta la organización de eventos y actividades. Son los responsables de que la comunidad siga creciendo y mejorando.
|
||||
</Typography>
|
||||
<Typography variant="h3">Requisitos</Typography>
|
||||
<ul className="fa-ul">
|
||||
<li>
|
||||
<span className="fa-li">
|
||||
<FontAwesomeIcon icon={faChevronRight as FontAwesomeIconProps['icon']} />
|
||||
</span>
|
||||
{' '}
|
||||
<strong>Profesionalismo</strong>
|
||||
<br />
|
||||
La comunidad siempre intenta conseguir el mayor nivel de calidad en todos sus proyectos, por lo que buscamos gente dispuesta a otorgar este nivel de profesionalismo para el disfrute de la comunidad.
|
||||
</li>
|
||||
<li>
|
||||
<span className="fa-li">
|
||||
<FontAwesomeIcon icon={faChevronRight as FontAwesomeIconProps['icon']} />
|
||||
</span>
|
||||
{' '}
|
||||
<strong>Constancia</strong>
|
||||
<br />
|
||||
La comunidad busca gente que en sus posibilidades sea activa, que pueda estar al tanto de lo que pasa en ella.
|
||||
</li>
|
||||
<li>
|
||||
<span className="fa-li">
|
||||
<FontAwesomeIcon icon={faChevronRight as FontAwesomeIconProps['icon']} />
|
||||
</span>
|
||||
{' '}
|
||||
<strong>Proactividad</strong>
|
||||
<br />
|
||||
La comunidad esta en constante crecimiento, por eso, buscamos gente que ayude a buscar nuevas oportunidades para diferentes proyectos y actividades de interés a la comunidad.
|
||||
</li>
|
||||
</ul>
|
||||
<Typography variant="h3">Beneficios</Typography>
|
||||
<ul className="fa-ul">
|
||||
<li>
|
||||
<span className="fa-li">
|
||||
<FontAwesomeIcon icon={faChevronRight as FontAwesomeIconProps['icon']} />
|
||||
</span>
|
||||
{' '}
|
||||
<strong>Experiencia</strong>
|
||||
<br />
|
||||
Uno de los objetivos de la comunidad es brindar experiencia en gestión y desarrollo de proyectos equiparable a un entorno laboral, que sea comprobable y útil.
|
||||
</li>
|
||||
<li>
|
||||
<span className="fa-li">
|
||||
<FontAwesomeIcon icon={faChevronRight as FontAwesomeIconProps['icon']} />
|
||||
</span>
|
||||
{' '}
|
||||
<strong>Capacitación</strong>
|
||||
<br />
|
||||
La comunidad buscara dar capacitación a sus miembros en lo referido a herramientas y procedimientos utilizados.
|
||||
</li>
|
||||
<li></li>
|
||||
</ul>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default ApplyForm
|
||||
@@ -0,0 +1,22 @@
|
||||
import Typography from '@/components/ui/Typography'
|
||||
import { Container } from '@/styled-system/jsx'
|
||||
import { ensureTeamApplicationsCollection } from 'entgamers-database/backend/database/teamApplications'
|
||||
import { Suspense, type FC } from 'react'
|
||||
import ApplyForm from './ApplyForm'
|
||||
|
||||
const EquipoUnirsePage: FC = async () => {
|
||||
await ensureTeamApplicationsCollection()
|
||||
return (
|
||||
<Container>
|
||||
<Typography variant="h1" align="center">Únete al Bosque</Typography>
|
||||
<Typography variant="body1">
|
||||
El equipo de EntGamers está formado por personas que se dedican a la administración de la comunidad, a la organización de eventos y a la creación de contenido. Aquí podrás enterarte cuales son las funciones de cada uno de los miembros del equipo y como puedes unirte a nosotros.
|
||||
</Typography>
|
||||
<Suspense fallback={<Typography variant="body1">Cargando...</Typography>}>
|
||||
<ApplyForm />
|
||||
</Suspense>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default EquipoUnirsePage
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1 @@
|
||||
@layer reset, base, tokens, recipes, utilities;
|
||||
@@ -0,0 +1,51 @@
|
||||
import '@/app/global.css'
|
||||
import Footer from '@/components/layout/Footer'
|
||||
import Header from '@/components/layout/Header'
|
||||
import { css } from '@/styled-system/css'
|
||||
import '@fontsource/open-sans/latin-300.css'
|
||||
import '@fontsource/open-sans/latin-400.css'
|
||||
import '@fontsource/open-sans/latin-700.css'
|
||||
import '@fontsource/permanent-marker/latin-400.css'
|
||||
import { config } from '@fortawesome/fontawesome-svg-core'
|
||||
import '@fortawesome/fontawesome-svg-core/styles.css'
|
||||
import type { Metadata } from 'next'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import FeedbackConsumer from './FeedbackConsumer'
|
||||
import SessionConsumer from './SessionConsumer'
|
||||
import StateProvider from './StateProvider'
|
||||
|
||||
config.autoAddCss = false
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Home | EntGamers',
|
||||
description: 'Una comunidad de jugadores, para jugadores',
|
||||
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL ?? 'https://entgamers.pro')
|
||||
}
|
||||
|
||||
interface RootLayoutProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const RootLayout: FC<RootLayoutProps> = ({ children }) => {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<StateProvider>
|
||||
<Header />
|
||||
<main
|
||||
className={css({
|
||||
paddingBlock: 'medium',
|
||||
minHeight: 'calc(100vh - 60px - 72px)'
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
<FeedbackConsumer />
|
||||
<SessionConsumer />
|
||||
</StateProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
export default RootLayout
|
||||
@@ -0,0 +1,112 @@
|
||||
'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 { useAppSelector } from '@/hooks/useAppSelector'
|
||||
import useManageError from '@/hooks/useManageError'
|
||||
import { setCurrentUser, setSession, setStatus } from '@/state/sessionSlice'
|
||||
import { getCurrentUser, login } from 'entgamers-database/frontend/session'
|
||||
import { useFormik } from 'formik'
|
||||
import NextLink from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useEffect, type FC } from 'react'
|
||||
import { object, string } from 'yup'
|
||||
|
||||
interface LoginData {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
|
||||
const loginSchema = 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 LoginForm: FC = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
const { manageError } = useManageError()
|
||||
const session = useAppSelector((state) => state.session)
|
||||
const router = useRouter()
|
||||
|
||||
const formik = useFormik<LoginData>({
|
||||
initialValues: {
|
||||
email: '',
|
||||
password: ''
|
||||
},
|
||||
onSubmit: async ({ email, password }) => {
|
||||
dispatch(setStatus('loading'))
|
||||
try {
|
||||
const session = await login(email, password)
|
||||
const user = await getCurrentUser()
|
||||
dispatch(setSession(session))
|
||||
dispatch(setCurrentUser(user))
|
||||
} catch (error) {
|
||||
manageError(error, 'Error mientras se iniciaba sesión', 'Error desconocido mientras se iniciaba sesión', 'error')
|
||||
} finally {
|
||||
dispatch(setStatus('idle'))
|
||||
}
|
||||
},
|
||||
validationSchema: loginSchema,
|
||||
validateOnMount: true
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (session.status === 'idle' && session.session !== undefined) {
|
||||
router.push('/')
|
||||
}
|
||||
}, [session, router])
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={formik.handleSubmit}
|
||||
>
|
||||
<FormGroup>
|
||||
<label htmlFor="email">Correo electrónico</label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.email}
|
||||
status={formik.touched.email !== undefined && formik.errors.email !== undefined ? 'danger' : undefined}
|
||||
onBlur={formik.handleBlur}
|
||||
fullWidth
|
||||
/>
|
||||
{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"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.password}
|
||||
status={formik.touched.password !== undefined && formik.errors.password !== undefined ? 'danger' : undefined}
|
||||
onBlur={formik.handleBlur}
|
||||
fullWidth
|
||||
/>
|
||||
{formik.touched.password !== undefined && formik.errors.password !== undefined && (
|
||||
<Typography variant="caption" color="danger">{formik.errors.password}</Typography>
|
||||
)}
|
||||
</FormGroup>
|
||||
<Typography variant="caption" color="muted">
|
||||
¿Perdiste tu contraseña?
|
||||
{' '}
|
||||
<NextLink href="/recover-password">Recupérala</NextLink>
|
||||
</Typography>
|
||||
<FormGroup>
|
||||
<Button
|
||||
type="submit"
|
||||
>
|
||||
Iniciar sesión
|
||||
</Button>
|
||||
</FormGroup>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
export default LoginForm
|
||||
@@ -0,0 +1,54 @@
|
||||
import LoginForm from '@/app/login/LoginForm'
|
||||
import Typography from '@/components/ui/Typography'
|
||||
import { css, cx } from '@/styled-system/css'
|
||||
import { Center } from '@/styled-system/jsx'
|
||||
import { container } from '@/styled-system/patterns'
|
||||
import { card } from '@/styled-system/recipes'
|
||||
import NextLink from 'next/link'
|
||||
import { type FC } from 'react'
|
||||
|
||||
const LoginPage: FC = () => {
|
||||
return (
|
||||
<>
|
||||
<Center
|
||||
className={cx(
|
||||
container(),
|
||||
css({
|
||||
minHeight: 'calc(100vh - 60px - 72px)'
|
||||
}))}
|
||||
>
|
||||
<div
|
||||
className={cx(
|
||||
card().body,
|
||||
css({
|
||||
width: '100%',
|
||||
maxWidth: { sm: 'breakpoint-sm' },
|
||||
overflow: 'visible'
|
||||
})
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
card().header
|
||||
}
|
||||
>
|
||||
<Typography align="center" variant="h1">
|
||||
Iniciar sesión
|
||||
</Typography>
|
||||
</div>
|
||||
<div
|
||||
className={card().content}
|
||||
>
|
||||
<LoginForm />
|
||||
<Typography variant="caption" align="center">
|
||||
¿No tienes una cuenta?
|
||||
{' '}
|
||||
<NextLink href="/register">Regístrate</NextLink>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</Center>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default LoginPage
|
||||
@@ -0,0 +1,22 @@
|
||||
import Typography from '@/components/ui/Typography'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { Center } from '@/styled-system/jsx'
|
||||
import { button } from '@/styled-system/recipes'
|
||||
import NextLink from 'next/link'
|
||||
import { type FC } from 'react'
|
||||
|
||||
const NotFoundPage: FC = () => {
|
||||
return (
|
||||
<Center
|
||||
flexDirection="column"
|
||||
width="100%"
|
||||
height="calc(100vh - 60px - 72px)"
|
||||
gap="medium"
|
||||
>
|
||||
<Typography variant="h1" className={css({ fontSize: '100px' })}>404</Typography>
|
||||
<Typography variant="h2" color="text">El árbol que buscas no está aquí</Typography>
|
||||
<NextLink className={button({})} href="/">Volver Al inicio</NextLink>
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
export default NotFoundPage
|
||||
@@ -0,0 +1 @@
|
||||
EntGamers
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
@@ -0,0 +1,18 @@
|
||||
import Clanes from '@/app/Clanes'
|
||||
import Hero from '@/app/Hero'
|
||||
import Social from '@/app/Social'
|
||||
import Team from '@/app/Team'
|
||||
import { type FC } from 'react'
|
||||
|
||||
const HomePage: FC = async () => {
|
||||
return (
|
||||
<>
|
||||
<Hero />
|
||||
<Clanes />
|
||||
<Social />
|
||||
<Team />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default HomePage
|
||||
@@ -0,0 +1,89 @@
|
||||
'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 useManageError from '@/hooks/useManageError'
|
||||
import { addAlert } from '@/state/feedbackSlice'
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { createPasswordRecovery } from 'entgamers-database/frontend/session'
|
||||
import { useFormik } from 'formik'
|
||||
import { type FC } from 'react'
|
||||
import { object, string } from 'yup'
|
||||
|
||||
interface RecoverPasswordData {
|
||||
email: string
|
||||
}
|
||||
|
||||
const recoverPasswordSchema = object({
|
||||
email: string().email('El correo electrónico no es válido').required('El correo electrónico es requerido')
|
||||
})
|
||||
|
||||
const CreateRecoverPasswordForm: FC = () => {
|
||||
const { manageError } = useManageError()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const formik = useFormik<RecoverPasswordData>({
|
||||
initialValues: {
|
||||
email: ''
|
||||
},
|
||||
onSubmit: async ({ email }) => {
|
||||
try {
|
||||
const callBackUrl = `${process.env.NEXT_PUBLIC_SITE_URL ?? 'http://localhost:3000'}/recover-password`
|
||||
await createPasswordRecovery(email, callBackUrl)
|
||||
dispatch(addAlert({
|
||||
id: nanoid(),
|
||||
title: 'Solicitud de recuperación de contraseña enviada',
|
||||
message: 'Si el correo electrónico está registrado, se enviarán instrucciones para la recuperación de contraseña',
|
||||
severity: 'success'
|
||||
}))
|
||||
} catch (error) {
|
||||
manageError(error, 'Error mientras se registraba', 'Error desconocido mientras se registraba', 'error')
|
||||
}
|
||||
},
|
||||
validationSchema: recoverPasswordSchema,
|
||||
validateOnMount: true
|
||||
})
|
||||
return (
|
||||
<form
|
||||
onSubmit={formik.handleSubmit}
|
||||
>
|
||||
<FormGroup>
|
||||
<label htmlFor="email">
|
||||
Correo electrónico
|
||||
</label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={formik.values.email}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
status={formik.touched.email !== undefined && formik.errors.email !== undefined ? 'danger' : 'success'}
|
||||
/>
|
||||
{formik.touched.email !== undefined && formik.errors.email !== undefined
|
||||
? (
|
||||
<Typography variant="caption" color="danger">
|
||||
{formik.errors.email}
|
||||
</Typography>
|
||||
)
|
||||
: (
|
||||
<Typography variant="caption" color="info">
|
||||
Por favor, introduce el correo electrónico con el que te has registrado. Te enviaremos un correo con instrucciones para la recuperación de contraseña
|
||||
</Typography>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={formik.isSubmitting}
|
||||
>
|
||||
Enviar correo de recuperación
|
||||
</Button>
|
||||
</FormGroup>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateRecoverPasswordForm
|
||||
@@ -0,0 +1,24 @@
|
||||
'use client'
|
||||
import CreateRecoverPasswordForm from '@/app/recover-password/CreateRecoverPasswordForm'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import type { FC } from 'react'
|
||||
import UpdateRecoverPasswordForm, { type UpdateRecoverPasswordFormProps } from './UpdateRecoverPasswordForm'
|
||||
|
||||
const ManageRecoverPassword: FC = () => {
|
||||
const searchParams = useSearchParams()
|
||||
const userId = searchParams.get('userId')
|
||||
const secret = searchParams.get('secret')
|
||||
const recoverData: UpdateRecoverPasswordFormProps | undefined = (userId !== null && secret !== null) ? { userId, secret } : undefined
|
||||
|
||||
if (recoverData === undefined) {
|
||||
return <CreateRecoverPasswordForm />
|
||||
} else {
|
||||
return (
|
||||
<UpdateRecoverPasswordForm
|
||||
{...recoverData}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default ManageRecoverPassword
|
||||
@@ -0,0 +1,125 @@
|
||||
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 useManageError from '@/hooks/useManageError'
|
||||
import { addAlert } from '@/state/feedbackSlice'
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { updatePasswordRecovery } from 'entgamers-database/frontend/session'
|
||||
import { useFormik } from 'formik'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { type FC } from 'react'
|
||||
import { object, ref, string } from 'yup'
|
||||
|
||||
export interface UpdateRecoverPasswordFormProps {
|
||||
userId: string
|
||||
secret: string
|
||||
}
|
||||
|
||||
interface UpdateRecoverPasswordData extends UpdateRecoverPasswordFormProps {
|
||||
password: string
|
||||
confirmPassword: string
|
||||
}
|
||||
|
||||
const updateRecoverPasswordSchema = 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')
|
||||
})
|
||||
|
||||
const UpdateRecoverPasswordForm: FC<UpdateRecoverPasswordFormProps> = (props) => {
|
||||
const dispatch = useAppDispatch()
|
||||
const { manageError } = useManageError()
|
||||
const router = useRouter()
|
||||
|
||||
const formik = useFormik<UpdateRecoverPasswordData>({
|
||||
initialValues: {
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
userId: props.userId,
|
||||
secret: props.secret
|
||||
},
|
||||
onSubmit: async ({ confirmPassword, password, secret, userId }) => {
|
||||
try {
|
||||
if (password !== confirmPassword) throw new Error('Las contraseñas no coinciden')
|
||||
await updatePasswordRecovery(userId, secret, password)
|
||||
dispatch(addAlert({
|
||||
id: nanoid(),
|
||||
title: 'Contraseña actualizada',
|
||||
message: 'Ahora puedes iniciar sesión',
|
||||
severity: 'success'
|
||||
}))
|
||||
router.push('/login')
|
||||
} catch (error) {
|
||||
manageError(error, 'Error al recuperar contraseña', 'Error desconocido mientras se solicitaba la recuperación de contraseña', 'error')
|
||||
}
|
||||
},
|
||||
validationSchema: updateRecoverPasswordSchema,
|
||||
validateOnMount: true
|
||||
})
|
||||
return (
|
||||
<form
|
||||
onSubmit={formik.handleSubmit}
|
||||
>
|
||||
<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>
|
||||
)
|
||||
: (
|
||||
<Typography variant="caption" color="info">
|
||||
Escribe tu nueva contraseña
|
||||
</Typography>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<label htmlFor="confirmPassword">
|
||||
Confirmar contraseña
|
||||
</label>
|
||||
<PasswordInput
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
value={formik.values.confirmPassword}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
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>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={formik.isSubmitting}
|
||||
>
|
||||
Actualizar contraseña
|
||||
</Button>
|
||||
</FormGroup>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpdateRecoverPasswordForm
|
||||
@@ -0,0 +1,47 @@
|
||||
import ManageRecoverPassword from '@/app/recover-password/ManageRecoverPassword'
|
||||
import Typography from '@/components/ui/Typography'
|
||||
import { css, cx } from '@/styled-system/css'
|
||||
import { Center } from '@/styled-system/jsx'
|
||||
import { container } from '@/styled-system/patterns'
|
||||
import { card } from '@/styled-system/recipes'
|
||||
import { Suspense, type FC } from 'react'
|
||||
|
||||
const page: FC = () => {
|
||||
return (
|
||||
<Center
|
||||
className={cx(
|
||||
container(),
|
||||
css({
|
||||
minHeight: 'calc(100vh - 60px - 72px)'
|
||||
}))}
|
||||
>
|
||||
<div
|
||||
className={cx(
|
||||
card().body,
|
||||
css({
|
||||
width: '100%',
|
||||
maxWidth: { sm: 'breakpoint-sm' },
|
||||
overflow: 'visible'
|
||||
})
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
card().header
|
||||
}
|
||||
>
|
||||
<Typography variant="h1" align="center">Recuperar contraseña</Typography>
|
||||
</div>
|
||||
<div
|
||||
className={card().content}
|
||||
>
|
||||
<Suspense fallback={<Typography variant="body1">Cargando...</Typography>}>
|
||||
<ManageRecoverPassword />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
|
||||
export default page
|
||||
@@ -0,0 +1,121 @@
|
||||
'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 useManageError from '@/hooks/useManageError'
|
||||
import { addAlert } from '@/state/feedbackSlice'
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { register } from 'entgamers-database/frontend/session'
|
||||
import { useFormik } from 'formik'
|
||||
import { type FC } from 'react'
|
||||
import { object, ref, string } from 'yup'
|
||||
|
||||
interface RegisterData {
|
||||
email: string
|
||||
password: string
|
||||
passwordConfirmation: string
|
||||
}
|
||||
|
||||
const RegisterSchema = object({
|
||||
email: string().email('El correo electrónico no es válido').required('El correo electrónico es requerido'),
|
||||
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'),
|
||||
passwordConfirmation: string().oneOf([ref('password')], 'Las contraseñas no coinciden').required('La confirmación de la contraseña es requerida')
|
||||
})
|
||||
|
||||
const RegisterForm: FC = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
const { manageError } = useManageError()
|
||||
|
||||
const formik = useFormik<RegisterData>({
|
||||
initialValues: {
|
||||
email: '',
|
||||
password: '',
|
||||
passwordConfirmation: ''
|
||||
},
|
||||
onSubmit: async ({ email, password }) => {
|
||||
try {
|
||||
await register(email, password)
|
||||
dispatch(addAlert({
|
||||
id: nanoid(),
|
||||
title: 'Registro completado',
|
||||
message: 'Ahora puedes iniciar sesión',
|
||||
severity: 'success'
|
||||
}))
|
||||
formik.resetForm()
|
||||
} catch (error) {
|
||||
manageError(error, 'Error mientras se registraba', 'Error desconocido mientras se registraba', 'error')
|
||||
}
|
||||
},
|
||||
validationSchema: RegisterSchema,
|
||||
validateOnMount: true
|
||||
})
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={formik.handleSubmit}
|
||||
>
|
||||
<FormGroup>
|
||||
<label htmlFor="email">Correo electrónico</label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.email}
|
||||
status={formik.touched.email !== undefined && formik.errors.email !== undefined ? 'danger' : undefined}
|
||||
onBlur={formik.handleBlur}
|
||||
fullWidth
|
||||
/>
|
||||
{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"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.password}
|
||||
status={formik.touched.password !== undefined && formik.errors.password !== undefined ? 'danger' : undefined}
|
||||
onBlur={formik.handleBlur}
|
||||
fullWidth
|
||||
/>
|
||||
{formik.touched.password !== undefined && formik.errors.password !== undefined && (
|
||||
<Typography variant="caption" color="danger">{formik.errors.password}</Typography>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<label htmlFor="passwordConfirmation">Confirmación de la contraseña</label>
|
||||
<PasswordInput
|
||||
id="passwordConfirmation"
|
||||
name="passwordConfirmation"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.passwordConfirmation}
|
||||
status={formik.touched.passwordConfirmation !== undefined && formik.errors.passwordConfirmation !== undefined ? 'danger' : undefined}
|
||||
onBlur={formik.handleBlur}
|
||||
fullWidth
|
||||
/>
|
||||
{formik.touched.passwordConfirmation !== undefined && formik.errors.passwordConfirmation !== undefined && (
|
||||
<Typography variant="caption" color="danger">{formik.errors.passwordConfirmation}</Typography>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Button
|
||||
type="submit"
|
||||
>
|
||||
Registrarse
|
||||
</Button>
|
||||
</FormGroup>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
export default RegisterForm
|
||||
@@ -0,0 +1,55 @@
|
||||
import RegisterForm from '@/app/register/RegisterForm'
|
||||
import Typography from '@/components/ui/Typography'
|
||||
import { css, cx } from '@/styled-system/css'
|
||||
import { Center } from '@/styled-system/jsx'
|
||||
import { container } from '@/styled-system/patterns'
|
||||
import { card } from '@/styled-system/recipes'
|
||||
import NextLink from 'next/link'
|
||||
import { type FC } from 'react'
|
||||
|
||||
const RegisterPage: FC = () => {
|
||||
return (
|
||||
<>
|
||||
<Center
|
||||
className={cx(
|
||||
container(),
|
||||
css({
|
||||
minHeight: 'calc(100vh - 60px - 72px)'
|
||||
}))}
|
||||
>
|
||||
|
||||
<div
|
||||
className={cx(
|
||||
card().body,
|
||||
css({
|
||||
width: '100%',
|
||||
maxWidth: { sm: 'breakpoint-sm' },
|
||||
overflow: 'visible'
|
||||
})
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
card().header
|
||||
}
|
||||
>
|
||||
<Typography align="center" variant="h1">
|
||||
Regístrate
|
||||
</Typography>
|
||||
</div>
|
||||
<div
|
||||
className={card().content}
|
||||
>
|
||||
<RegisterForm />
|
||||
<Typography variant="caption" align="center">
|
||||
¿Ya tienes una cuenta?
|
||||
{' '}
|
||||
<NextLink href="/login">Inicia sesión</NextLink>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</Center>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default RegisterPage
|
||||
@@ -0,0 +1 @@
|
||||
EntGamers
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
@@ -0,0 +1,15 @@
|
||||
import { type IconDefinition } from '@fortawesome/fontawesome-svg-core'
|
||||
|
||||
const trees: IconDefinition = {
|
||||
icon: [
|
||||
640,
|
||||
512,
|
||||
[],
|
||||
'f7c5',
|
||||
'M298.4 288H329c9 0 17-5 20.88-13c3.75-8.125 2.5-17.38-3.375-24.12L268.4 160h28.88c9.127 0 17.38-5.375 20.88-13.62c3.625-8.125 1.875-17.62-4.25-24.12L203.6 4.875c-6-6.5-17.25-6.5-23.25 0L69.97 122.3c-6 6.5-7.75 16-4.125 24.12C69.34 154.6 77.59 160 86.72 160h28.88L37.46 250.9c-5.875 6.875-7.125 16-3.375 24.12C37.96 283 45.84 288 54.96 288h30.63l-79.88 90.5c-6 6.75-7.377 16.12-3.625 24.25C5.834 410.8 14.08 416 23.09 416H160v64C160 497.7 174.3 512 192 512S224 497.7 224 480V416h136.9c9 0 17.25-5.25 21-13.25c3.75-8.125 2.5-17.5-3.5-24.25L298.4 288zM634.3 378.5L554.4 288h30.63c9 0 17-5 20.88-13c3.75-8.125 2.5-17.38-3.375-24.12L524.4 160h28.88c9.125 0 17.38-5.375 20.88-13.62c3.625-8.125 1.875-17.62-4.25-24.12l-110.3-117.4c-6-6.5-17.25-6.5-23.25 0l-95.14 101.3c11.13 15.38 14 35.25 6.377 52.88c-4 9.375-10.38 17.12-18.25 22.75l41.5 48.25c14 16.25 17.13 39.25 8.002 58.62c-4.25 8.875-10.5 16.12-18.13 21.5l41.63 47.13c8.625 9.875 13.37 14.2 13.62 26.7L416 480C416 497.7 430.3 512 448 512C465.7 512 480 497.7 480 480V416h136.9c9.002 0 17.25-5.25 21-13.25C641.7 394.6 640.3 385.3 634.3 378.5z'
|
||||
],
|
||||
iconName: 'trees',
|
||||
prefix: 'fas'
|
||||
}
|
||||
|
||||
export default trees
|
||||
@@ -0,0 +1,378 @@
|
||||
import { type FC, type SVGProps } from 'react'
|
||||
|
||||
const EntGamers: FC<SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 551.73 614.78"
|
||||
focusable="false"
|
||||
>
|
||||
<defs>
|
||||
<radialGradient
|
||||
id="a"
|
||||
cx={241.07}
|
||||
cy={372.19}
|
||||
r={6.56}
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset={0} stopColor="#fefd81" />
|
||||
<stop offset={1} stopColor="#e5a541" />
|
||||
</radialGradient>
|
||||
<radialGradient id="b" cx={335.55} cy={372.19} r={6.56} xlinkHref="#a" />
|
||||
</defs>
|
||||
<g data-name="Tronco">
|
||||
<path
|
||||
style={{
|
||||
fill: '#80725c'
|
||||
}}
|
||||
d="m149.78 275.36 41.36 77.16-1.51 88.77 8.57 21.18 7.06-2.52 166.93 14.12 11.6-94.31 20.68-93.3-254.69-11.1z"
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
fill: '#4d4435',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
d="m318.23 380.77-7.56-13.11-1.01-6.56 15.13-34.8 13.11-14.62-16.14-5.55-3.53-10.59 52.45-3.03 10.59 18.66-63.04 69.6z"
|
||||
/>
|
||||
<path
|
||||
d="M236 384.8a45.26 45.26 0 0 1-15.64 9.08c3.62-2.82 11.52-6.76 12.11-11.6s3.53 2.52 3.53 2.52Z"
|
||||
style={{
|
||||
fill: '#d7c5a8',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
fill: '#4d4435',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
d="m289.99 435.24-2.52-14.63 3.53-5.04 5.04-6.56 1.01-15.63-2.52-18.16-6.05-24.71 7.06-17.65 3.02-19.17 8.57 17.15-4.03 16.64-4.03 5.05 1.51 11.09 2.02 10.09 8.57 10.08 8.07 15.64 5.55 20.17-19.67 20.17-15.13-4.53z"
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
fill: '#d7c5a8',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
d="m190.63 321.26 8.08 22.19 6.55-10.59 1.01 82.71 14.12 13.61v13.12l-12.1 11.59-21.69-41.85 3.03-40.35.5-22.19-9.58-19.16 10.08-9.08zM271.33 375.72l-6.56-1.51-13.11-28.75-8.07-5.54a18.84 18.84 0 0 0 2 10.08c-8.2-5.29-35.81-22.69-35.81-22.69l7.57 9.58 24.2 20.17 9.59 6.06 7.06 14.62 2 4.54 5 1ZM276.37 410l2.52-12.61-1.51-10.09 4.54-11.6-1-12.1-6.56 15.64-4.53 6.55s6.29 18.21 6.54 24.21Z"
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
fill: '#4d4435',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
d="m279.9 355.55-15.63-28.74-14.12-5.55 11.09 9.08L268.3 349l-3.53 5.54 8.07 22.19h4.54l3.53-12.6-1.01-8.58z"
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
fill: '#d7c5a8',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
d="m319.24 382.28-13.11-12.1-1.01-7.57 8.57-18.66-6.56 4.54 4.55-13.11-11.61 16.64-.5 9.58 3.03 13.62 11.6 9.58 5.04-2.52z"
|
||||
/>
|
||||
</g>
|
||||
<g data-name="Ojos">
|
||||
<path
|
||||
style={{
|
||||
fill: '#4d4435',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
d="m275.36 407.5-9.07-10.59-22.2-2.53-13.61 3.53-18.16-5.04 14.12.51 18.16-6.06 17.65-5.04 9.58 4.54 3.53 20.68z"
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
fill: '#4d4435',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
d="m244.09 394.38-12.1 8.07 4.54-6.55 7.56-1.52zM230.98 397.91l-9.58 3.03 6.05-5.04 3.53 2.01z"
|
||||
/>
|
||||
</g>
|
||||
<g data-name="Rasgos Faciales">
|
||||
<path
|
||||
style={{
|
||||
fill: '#05080f',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
d="m226.95 446.83.5-19.16-16.64-16.64-5.55-20.17 1.52 25.71 11.09 11.1 2.52 26.22 6.56-7.06zM277.88 356.56l1 7.57-2.52 8.06-14.12 7.57s-2.52-.25-2.52-4.54-12.1-26.73-12.1-26.73l1 8.57-34.29-23.2 8.58 10.14-17.65 1.51L238 361.6s-15.63-4.54-19.16 1S228 381.78 236 384.3c-3.28 4.54-14.63 9.08-14.63 9.08s17.9.25 33.79-7.57c12.36-5.8 18.91 12.61 20.68 25.22.75-9.08-1.52-27.24-1.52-27.24s11.13-8.79 3.56-27.23ZM302.09 356.56l-1 7.57 2.53 8.06 14.12 7.57s2.52-.25 2.52-4.54 12.1-26.73 12.1-26.73l-1 8.57 34.3-23.2-8.6 10.14 17.66 1.51-32.79 16.09s15.64-4.54 19.17 1-9.1 19.18-17.1 21.7c3.28 4.54 14.63 9.08 14.63 9.08s-17.91.25-33.79-7.57c-12.36-5.8-18.91 12.61-20.68 25.22-.76-9.08 1.51-27.24 1.51-27.24s-11.09-8.82-3.53-27.23Z"
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
fill: '#05080f',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
d="m274.5 316.1-6.17 18.15 10.95 20.83-.69 20.92 6.19-25.01-7.3-13.89 5.38-25.79-8.36 4.79zM305.98 316.61l6.17 18.15-10.95 20.83.69 20.91-6.19-25.01 7.3-13.89-5.38-25.79 8.36 4.8zM387.68 306.01l6.17 18.16L382.9 345l.69 20.91-6.19-25.01 7.3-13.89-5.38-25.79 8.36 4.79zM318.74 350.51l22.19-33.79 1 11.09-17.14 26.23-6.05-3.53z"
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
fill: '#05080f',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
d="M279.4 364.13 272.34 350l8.57 10.6-1.51 3.53zM300.58 363.62l7.06-14.12-8.57 10.59 1.51 3.53z"
|
||||
/>
|
||||
</g>
|
||||
<g data-name="Barba Base">
|
||||
<path
|
||||
d="M392.87 344.46S392 408 431.71 434.23c-9.21.5-17.15 1-17.15 1s6.17 25.77-2.56 41.86-23.54 37.83-22.15 46.91c-5.68-2.52-4.54-14.63-4.54-14.63s-5.43 16.14-25.72 29.76c-15.26 10.59-20.68 23.2-20.68 23.2l.5-14.63s-4.87 13.79-21.18 29.76-26.06 27.4-27.23 37.32c-2.69-10.76-19-124.91-3.53-173.49 4.87-12.28 14.79-21 39.84-19.67s48.92 31.27 48.92 31.27 14.29-47.75 10.59-105.41c.34-3.19 6.05-3.02 6.05-3.02Z"
|
||||
style={{
|
||||
fill: '#05080f',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M188.62 344.46s.88 63.54-38.83 89.77c9.2.5 17.14 1 17.14 1s-6.18 25.72 2.52 41.85S193 514.92 191.64 524c5.68-2.52 4.54-14.63 4.54-14.63s5.43 16.14 25.72 29.76c15.26 10.59 20.68 23.2 20.68 23.2l-.5-14.63s4.87 13.79 21.18 29.76 26.06 27.4 27.23 37.32c2.69-10.76 19-124.91 3.53-173.49-4.87-12.28-14.79-21-39.84-19.67s-48.92 31.27-48.92 31.27-14.26-47.75-10.59-105.41c-.34-3.19-6.05-3.02-6.05-3.02Z"
|
||||
style={{
|
||||
fill: '#05080f',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
fill: '#05080f',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
d="m182.71 310.05-6.17 18.15 10.96 20.83-.69 20.91 6.19-25.01-7.31-13.88 5.38-25.8-8.36 4.8zM212.83 460.45v-24.71l-14.63-23.7 2.52-26.23 1.52-39.84-6.56 28.24-11.1 56.49 29.76 32.27"
|
||||
/>
|
||||
</g>
|
||||
<g data-name="Barba Claro">
|
||||
<path
|
||||
d="M230.48 540.64a80.33 80.33 0 0 0-10.09-14.12c-5.67-6-24.33-27-16.14-54a130 130 0 0 0 5.55-14.13 120.14 120.14 0 0 0-.5 15.13c1.89-13.11 7.18-21.68 11.09-25.72-4.16 19.2-5.67 60.81 10.61 76.7a68 68 0 0 1-14.12-12.61s13.12 23.71 13.6 28.75ZM175.51 427.17s-12 19.67-10.09 35.3c1.13 6.81 1.51 9.58 1.51 9.58s-4.41-9.33-4.54-17.15 2.46-21 8.07-26.22c4.92-4.54 7.2-12.66 7.06-16.64-.14-4.22 2.37 9-2 15.13Z"
|
||||
style={{
|
||||
fill: '#ced7a8',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M165.92 464s-5.71-8.14-3.53-22.69c.51-5.65 9.42-16.88 11.1-18.66s5-9.59 5-9.59-5 16.75-10.59 21.69-3.53 18.16-3.53 18.16l1.55 11.09Z"
|
||||
style={{
|
||||
fill: '#ced7a8',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M166.93 447.84s3.78-15.63 11.6-18.66c-7.56-.75-13.11 7.06-13.11 7.06l1.51 11.6ZM238 519.46s-.47 22.12 5.55 33.28c10.47 19.41 28.2 32 31.27 36.82-3-12.11-10.13-25.21-16.64-32.28s-17.11-13.36-20.18-37.82ZM182.86 466.58s2.64 7.49 3.74 9c-2.86-3.73-14.69-8.31-7-28.81 1-5.24 2.66-9.64 2.42-11.85.62 5.37-.73 29.8 8.07 42.68"
|
||||
style={{
|
||||
fill: '#ced7a8',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M274.35 587s.63-6.7-5-18.66c-2.39-6.16-4.66-5.69-6.05-12.11s-2.4 6.92-.5 18.66A33 33 0 0 0 274.35 587ZM216.36 513.41a41.49 41.49 0 0 0-6.07-6.66c-3.29-2.76-14.19-12.39-11.73-26.9a67.39 67.39 0 0 0 1.92-7.67 64.49 64.49 0 0 0 .72 7.87c.12-6.92 2.28-11.7 4-14.05-.89 10.2 1 31.85 10.36 39a35.21 35.21 0 0 1-8-5.6s8.24 11.42 8.82 14ZM178.24 474.07s-10.85-23.72 3.64-40.32c-1.18 16.27-6.4 32.09-3.64 40.32Z"
|
||||
style={{
|
||||
fill: '#ced7a8',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M180.22 466.17s-7.57-16.75 2.71-28.55c-.89 11.51-4.63 22.73-2.71 28.55ZM201.23 427.17S199 448.85 191.64 461c2-7.82 4.17-16.9 3.54-21.19-1.39 5.3-6.06 12.61-6.06 12.61s7.19-19.92 6.06-35.3c2.64 13.62 1.51 15.13 1.51 15.13l4.54-5ZM241.33 440.15s-20 7.53-17.66 24.77c5.57-8.48 3.22-9.89 17.66-24.77Z"
|
||||
style={{
|
||||
fill: '#ced7a8',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M223.42 466s11.2-29.1 28.24-39.84c-10.34-.19-28.78 12-36.41 20.32 9.28-6.1 13-6.8 13-6.8s-5.87 10.21-4.82 26.32ZM186.07 422s6.91-15.32 2.59-25.19M244.05 438.12s7.12-10.64 15.22-11.82"
|
||||
style={{
|
||||
fill: '#ced7a8',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
/>
|
||||
</g>
|
||||
<g data-name="Barba Detalles">
|
||||
<path
|
||||
d="M167.44 473.06s-3.6-11.4-.51-29.76c.82-3.85-3 5.55-3 5.55l-1.52-6.55s-3.15 16.13 5.05 30.76ZM183.07 452.89s2.35 17.46 7.57 24.2c-2.75-2-6.56-8.06-6.56-8.06l4 8.06s-9.25-5.83-10.08-10.59c.48 6.45.5 8.58.5 8.58s-4.31-6.14-4.54-17.15a57.09 57.09 0 0 1 .51-7.57l1.53 10.09.5-7.56s2.15 10.6 5.05 14.62a61.11 61.11 0 0 0-1.51-7.56l2.52 5.54s-.31-8.47.5-12.6ZM274.86 588.55s-1.85-10.81-6.05-15.63-8.09-5.19-10.09-9.59c3 8 6 15.64 6 15.64s5.42 6.93 10.09 9.58Z"
|
||||
style={{
|
||||
fill: '#889065',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M266.79 581a37.68 37.68 0 0 0-9.58-15.13c-6.92-6.3-14-14.39-16.65-21.19 1 4.93 9 22.07 26.23 36.32ZM201.23 428.68s-6.18 25.47-10.09 34.29c6.68-8.57 7.06-12.1 7.06-12.1a30.86 30.86 0 0 1-3 17.15 96.83 96.83 0 0 0 12.1-27.74l-6.05 11.6v-23.2Z"
|
||||
style={{
|
||||
fill: '#889065',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
/>
|
||||
</g>
|
||||
<g data-name="Barba Sombra">
|
||||
<path
|
||||
d="M212.7 488.2s.34 4.88 10 19.79 3.9 10.84 3.9 10.84-11.11-13.83-13.9-30.63ZM255.76 563.15s-3.66-3.31-9-20.2-6-9.85-6-9.85 3.49 17.35 15 30ZM170.85 430.24s-2.39 4.29-2.49 22.11-2.66 11.27-2.66 11.27-1.75-17.68 5.15-33.38ZM208.14 500.58s1.48 4.66 14.31 16.9 6.35 9.62 6.35 9.62-14-10.81-20.66-26.52Z"
|
||||
style={{
|
||||
fill: '#05080f',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
style={{
|
||||
fill: '#05080f',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
d="m304.11 429.69 9.58-16.64-13.62 5.54 7.57-27.73 10.09-10.09 1.51-29.76 41.36-44.88 20.17-2.52 9.08 47.91 2.52 71.61-11.1 39.34-77.16-32.78z"
|
||||
data-name="Sombra Facial"
|
||||
/>
|
||||
<g data-name="Ojo Izq">
|
||||
<path
|
||||
d="M241.07 365.64a6.56 6.56 0 1 1-6.56 6.55 6.56 6.56 0 0 1 6.56-6.55Z"
|
||||
style={{
|
||||
fillRule: 'evenodd',
|
||||
fill: 'url(#a)'
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M241.32 365.64a5.3 5.3 0 1 1-5.29 5.29 5.3 5.3 0 0 1 5.29-5.29Z"
|
||||
style={{
|
||||
fill: '#080c17',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
fill: '#07090e',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
d="m229.97 368.16 22.19 8.57-1-10.08-9.08-3.03-12.11 4.54z"
|
||||
/>
|
||||
</g>
|
||||
<g data-name="Ojo Derecho">
|
||||
<path
|
||||
d="M335.55 365.64a6.56 6.56 0 1 1-6.56 6.55 6.56 6.56 0 0 1 6.56-6.55Z"
|
||||
style={{
|
||||
fillRule: 'evenodd',
|
||||
fill: 'url(#b)'
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M335.3 365.64a5.3 5.3 0 1 1-5.3 5.29 5.29 5.29 0 0 1 5.3-5.29Z"
|
||||
style={{
|
||||
fill: '#080c17',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
fill: '#05080f',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
d="m346.64 368.16-22.19 8.57 1.01-10.08 9.08-3.03 12.1 4.54z"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="m178 341.43-3 1.51-4-.5-3 1-3.59 5.56-8.07-1v3l-7.56-1.51-4-1.51h-4.58s2-2.78-1.51-3-4-5-4-5l.5-3.53-12.1.5-2-3-4.54-2.52 1-5.54 2-4-3.55-6.18.5-6.56 4-3.53-1.51-3-.5-5.05 3-1.51 1.52-4-5.55-5.55-2.52 1-9.08-.51-5.55 1.52-5 7.56-4 .5-5.54 5.55-3.53-3.53-6.06-.5-5.54-2.52-1-6.56-2.52-3.53-3-3-4-.5-2 1-6.56 2.52L44.5 284l-4.54 3-8.07 1-2.52-5.55-1.51-5.54-5.55-2-4-1-4-5-1.51-5.05 3-5-3-3-7.8-3.7-.5-6a5.08 5.08 0 0 1 1-3.53c1.26-1.51 6.55-2.52 6.55-2.52l.51-4-2-2 .51-4 1.51-3.53-3.53-3-5-5.55-3.53-.5L3 211.31l1.51-8.57L3 198.2l5-6 3-3-1.52-4 1.52-4h7.06l5-.5 2-4 3.53 2 5.54-1 1.68-4.7-.51-8.06 3-4h5.55l8.15 1.45 4.53 1.52 4 .5.5-4.54-4.54-1.51.51-5 .5-5 4-2.52-2-3s-2.27-1.52-2-2.53 4.5-5.65 4.5-5.65l5.55-3h4l.51-4-2.52-6.06 4-7.06 3.62-3.54h2.52l7.57 4 5 1 3.53-2 3-2-4.54-3-.5-7.07 5-2.52 7.56.51 4.7-4.6 3.53-4 11.6-1 6 3.53 5.55 2.52 3 1-1.52-7.06 3-10.09-2.52-5 1-4 3-4V61l3.53-3 6.06-4 1-6.55 4.54-4 12.11-2 4-4.54 5.55-12.61 12.1-10.59 9.08-.51L212.32 3l14.12-.51 8.58 3 5-3L244.6 1l19.67-1 7.06 1.51 9.58 3 3 .5 2.52-3L290 4l2.52 2.53 7.57 3 .5-3.53 6.56 3 3 1.51 3 2.52 2.52-.5 1.51 3.53 1.51 2.52 6.56 2 4-2.52 4.53 5.54 2 3.53v3.53l3.53 2.53 3.53 1 1.52 4 2 6.56 6.06.5v4.54l1.58 3.67.5 3.53 1.52 1.51 8.07 2 5 1 5-4 6.17-1.5 2.52 2 4-1h4.54l1.51-2 7.06-3 5.11 4 5.54-2.52 2 3 5 2 8.62 2.52 3.53 3.53 2 4.54 2 4 4 1.51h4.54l4.53 1.51 4.54 4.54 2.53-1.51 4-.5 2 8.06 2.52 3.54v7.06l1 5 2.52 3.53-1 3.53 5.55 1h4.54l3 6 1 10.59 2 4 4.54 1.51 4 7.06 1.51 5.55.51 6.05-4 6 6.06 6.05h4l3 2 6.05 3.53 4.54 5v6.56l-2.52 4-3.53 5s-.51 5.29-.51 6.05v5l-3.53 5.05 3.53 4 4 2.52 2 5.05 5.55.5 3-3.53 8.07 1.51 5.54.51 3.54 1 2.52 5 3.53 3.53 3 4.54-1.19 3.03 2 5.55 2 2.52 2.53 4.54 1 3.53-1.51 3-3 4-3 3-6.56 2.53-1.51 3-.5 7.06-3.53 3-3.54 1-4.55-3.39-4 2.52-1.51 2 1 5 2.52 4.54 1.51 3-2 5-5.55 5.55-3.53 3.53-.51 4.53-3 3-2 3-3 5-8.57.5-6.05 4-1.52 3.53-5 2.52-4.54.51-5.54-.51-2-3-2.71-2.28h-6v3l.51 4 2.52 3-3 5.55-6.55 1.51-.58 6.66v5l-5 3.53-5.55-.5-4 2 .5 2-7.56 1.51-2.52 5-4-2-4.54-4h-8.57l-3.66-3.48-1-6.06-6-3-4.54-2 1-4.54v-2.52l-5.05 4-2 2.52-4.54-.5-4.54-19.67-3.53-10.59-11.1-11.09-5.54-5.55-13.62 7.06-10.59.5-4.54-6.05h-22.73l-8.58-1-21.69 6.05-14.62-8.62-10.59 4h-10.09l-9.11 3.61-12.1 4.53-10.09-4.53-10.59 1.51 3 7.56-9.08 9.08-6 9.08-8.07 5.55-3.53 4"
|
||||
style={{
|
||||
fill: '#05080f',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
data-name="Hojas Base"
|
||||
/>
|
||||
<g data-name="Hojas Claro">
|
||||
<path
|
||||
d="M151.8 158.86s.06.23.2.65a55.22 55.22 0 0 0 24 29.11c19.42 11.64 27.74 23.7 27.74 23.7l11.1 14.68s4.29-.55 19.17 6.55 23.95 12.57 24.21 15.64 7.56 21.18 7.56 21.18 7.31 5 9.58 9.58 6.05 17.15 6.05 17.15l1 8.07-15.13-8.58-12.6 4.54-7.06-1-22.7 8.57s-8.75-1.1-17.65-2c-4.46-.46 0 6.05 0 6.05l-9.58 11.1-5.55 7.06-2.52 2.52h-2l-5 4.54-2 2.52-3.53 1-2.52 1.51-3.53-.5-2.52 1-3.6 5.5-9.08-1.51 1 3.53-11.6-3h-6.06l1.52-2-2-1h-1.51l-3-3 1.51-5.55-13.61.5v-2.52l-3.53-1.51-2-1 .5-6.05 2.53-4-3.53-5v-7.57l4.53-4-1.59-2.72-1-5.55 3-.51 2-3.53-5-6.55-2.52 1-8.57-.51-6.56 2-5 7.56h-3.56l-6.56 6.06-3.53-4h-5l-6.56-3-1-7.06-4.54-5.55-5-1L52 286l-7.57-2-4.54 3.53-4 .5h-4.62l-4-11.09-9.07-3-4.54-5.55-1.52-4.54 4-5-7.57-5-3.53-2-1-7.57s4.8-4.45 8.07-3.53c.38-2.47.51-4.53.51-4.53l-2-1.52V231l2.52-5s-7.21-2.88-9.14-8.13c-2.14-.08-4 0-4 0l1.51-3 1.52-3 1-9.08-1.51-4.59 8.07-9.58-1-3 1.52-5H23.2l2-4.54 3.53 2.52 5.54-1 1.52-5-1-7.56 3.53-4h6.55l15.64 4 .5-5-4.54-1.51V155l1.53-7.23 4-2-4.54-4.54v-1.53l5-6 4.54-2.52h4.53s.76-6.23 0-6.06-2-3-2-3l4-9.08 3.53-2.52h2.52s4.54 2.35 5 2.52 5 3 5 3h2.52l5.55-2 8.07 13.11 1 9.58 3 5.05 5.55.5-5.55 8.07 3.53 3 4 .5a8.28 8.28 0 0 1 2.52-.5c1 0 9.08 1.51 9.08 1.51s-.51 4.62-1 5.55 1.51 6.05 1.51 6.05h4.54l5.55-1.51 5.55-3.53"
|
||||
style={{
|
||||
fill: '#96a166',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M222.41 305.62s-25-8.4-40.35-11.6c8.91-.16 13.62 1 13.62 1s22.53 7.23 26.73 10.59Z"
|
||||
style={{
|
||||
fill: 'none',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
/>
|
||||
</g>
|
||||
<g data-name="Hojas Medio">
|
||||
<path
|
||||
d="m126.08 90.28 7.57 8.57 5 8.14 1.51 2.45v5l9.59 6h9.58l11.6-4-11.6-.5-10.09-3.53-5.55-16.14-17.65-6ZM37.83 231.49l-2.53-8.07-2.52-11.1 5-6.55 8.57-4.54 8.07-3 6.55-6 5-6.05 4-8.58s1.51 23.71 1.51 26.73 4.54 17.15 4.54 17.15L89.27 230l-.51 5.55-6.05-1-10.09 1-8.57 1L55 236l-8.07-.51-8.6-2.49-.5-1.51ZM150.29 247.63l-7.57-4.54-5.54-8.58-6.05-6.55-6.56-8.58 10.09-11.09 12.6-4.54 2.52 2.52 11.6-6.05-1.51-8.57-1.01-6.56 4.04-3.53 5.04 1.01 10.59 8.07 6.05 4.54 4.04 1.51 9.58 9.08 6.56 7.06 4.54 7.06 2.52 2.02-23.2 9.58-8.07 7.06-6.56 3.02-16.14 4.04-7.56 2.02zM97.33 159.87l-1.51-7.56 3.53-8.07-8.57-8.07 1-7.06 7.06-1.51 1.51-11.1 6 11.6 1.52 9.08 3.53 5h5.6l-6 8.07 2.52 3.53 5 .5 2-1 10.09 1.51v7.56a2.25 2.25 0 0 1-2 1.52c-1.51 0-8.57 1.51-8.57 1.51l-13.11-.5-9.59-5ZM147.77 332.35l-3.03-3.02-2.52-1.52 2.02-7.56.5-4.04v-5.54l2.52-1.52 5.55-5.04 3.53-3.03 3.53.51 7.57.5 19.67-2.52 3.52 10.59v10.09l-12.1 9.58-30.76 2.52zM133.65 283.94l-15.13-19.67 15.13.5v-4.54l9.08 3.53 6.05 4 3-8.58 17.65 19.67s3.28 11.6 2 11.6-37.82-6.55-37.82-6.55ZM266.29 291l-1.01-3.03-3.53-2.02-2.52-4.03 3.02-7.56 8.07-4.55 7.56 6.06 7.06 14.62-2.01 9.08-16.64-7.06M219.89 297.05l-14.63-8.57-2.02-1.52-9.58-7.56-9.08-8.58-1.51-6.55 6.56-1.01 6.55-2.52 11.6 1.01 4.54-4.54.51-7.57.5-5.55 7.06-8.57 5.55-3.02 3.53-1.01 20.17 9.07 14.63 16.14 1.51 14.12-5.04 3.54-6.05 7.06-8.07 3.02-6.56 4.04-7.56 3.02-9.08.51-3.53 5.04zM117.51 286.46l-8.07-6.56-4.54-1.51-6.56-11.6-1.01-9.08 2.02-6.05H93.3l-7.06 3.03-7.56 3.02-7.57 6.56-6.56 3.53h-7.56l-1.01 5.04-3.53 4.54-4.03 5.04-3.54 1.52 5.55 1 1.52 1.01 7.06-3.02h4.03l3.53 2.01 2.52 4.04.51 5.55 2.52 3.02 5.04 1.01 5.05 1.01 3.02 4.04 6.56-7.57 3.02 1.01 4.54-7.06 6.05-2.02 12.11-1.51z"
|
||||
style={{
|
||||
fill: '#889065',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
/>
|
||||
</g>
|
||||
<g data-name="Hojas Detalles">
|
||||
<path
|
||||
d="M123.06 248.63s-10.59-12.48-8.58-34.79a22.32 22.32 0 0 0 1-3l3.53 8.57s16.2 25 19.67 28.75 4 6.56 4 6.56v3.53l-12.11-7.06 2 5-9.58-7.57Z"
|
||||
style={{
|
||||
fill: '#05080f',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="m125.58 245.1-11.1-8.57-9.08-3.53 6.56-.5-8.07-8.58-5.55-10.08-3-5.55-4.54-5.55-2-13.11-3.53-19.17 2.52-15.13v-16.14l-11.6-8.06-5 .5h-4.62L63 133.14l-5.55 6.05v1.52l4 4 .5 1.52-4.54 1.51-1 6s.07 2.89 0 3.53a3 3 0 0 0 .51 1.51l4 .51-.5 3v2l7.56-3 2-3-.5-4 6.55 2 2 6.56-1.51 12.61s2.69 4.4 3 5 3 4.54 3 4.54l-9.08-5.55 3.53-12.61-7.56 6-2 10.6L65.9 191l1 5.55.5 4.54-4-8.58-.4 8.21-.5 5.55a38.43 38.43 0 0 0-1.52 4c-.06.63 4.54 6.55 4.54 6.55l2 2.52a25.89 25.89 0 0 0 1.51 3.53c.44.54 5 2 5 2l6.55 2.52 19.17 11.1 7.06 5 18.66 1.51Z"
|
||||
style={{
|
||||
fill: '#05080f',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="m130.62 166.43-6.05-.51-9.08.51-6.56.5-3.53 1-4.53-3-14.13-1-.5-2-1.51-12.61 1.51-7.06s2.52 4.29 2.52 5.55 2 7.56 2 7.56l5 2.53 8.58 1.51 6.55 2.52 9.58 1.51 12.61-3-2.52 6.05ZM100.36 243.59l-18.66-8.07-17.65 2.02-21.69-1.51-20.67-2.02-5.55-6.05-3.53-2.02-2.52-1.51-3.03-2.52-3.03-3.54 2.02-2.01 3.03 2.01 4.54 5.55 10.59 3.03 4.54 2.52h6.55l9.58-.5 1.01 2.52 7.57 2.52 9.58-1.01 8.07-1.51 10.09.5 2.01-2.02-3.53-7.06 5.55 1.01 8.07 4.04 10.09 14.62 3.53 4.04-6.56-3.03z"
|
||||
style={{
|
||||
fill: '#05080f',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
fill: '#05080f',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
d="m139.7 253.17-15.13-11.6 1.01 8.58-22.7 3.02v13.62l9.08 12.61-4.54 2.02-10.09-9.59-4.03-12.1 3.53-7.57 2.02-8.07-7.57-9.58 21.69 6.05 10.59-2.52 16.14 15.13z"
|
||||
/>
|
||||
<path
|
||||
d="m201.23 288-11.6-4.54-29.76-29.25-.5-5-.51-3 20.68-4L193.16 231l17.65-4.54 5-.5-.5-8.57-13.11 5.54s-14.63 6.81-17.15 8.07-13.62 8.58-13.62 8.58l-10.09 1-15.63 6.56-12.1-.5 7.06 8.57h6.55l7.57 11.6 7.06 16.64"
|
||||
style={{
|
||||
fill: '#05080f',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="m184.08 301.59-9.08 2-15.63-1.52-5.55-4.54-4.54 1.52-5.55-2.53-1.51-3.52-10.09 1-11.6 2.52 2-3.53L118 287l-12.61-8.57 2-6.56 7.06 1.52 4 5.54 11.6 2.52 4.54.51-3-6.56-5-7.56 12.6 8.07 16.14 4.54-2.52-6.56 7.06 7.06 1.51-13.62 2.53-4 12.1 12.1c.76.76 13.11 7.57 13.11 7.57l8.58 2.52h6.05l3.53 2 5 5 5.55 3-33.79 6ZM249.14 291.5l6.55-4.54.51-1 5-2.52v-5l3.53-3.53 4.54-3.53-2-3.53-2-2.52s-3.87 6.38-4.54 7.06-4.54 5-4.54 5l-8.57 4.54-5 1-2.52 4-30.76-3.53 25.21 9.08 14.63-1Z"
|
||||
style={{
|
||||
fill: '#05080f',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
fill: '#05080f',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
d="m282.93 300.58-3.03-7.06-3.02-3.53-8.58-2.52-3.53 1.51-23.2 9.58h-12.6l-9.08-3.02-5.05-3.53 15.64 14.62 16.14-6.05 7.56 1.01 12.61-4.54 15.13 8.57 4.54-2.52-2.52-2.02"
|
||||
/>
|
||||
<path
|
||||
d="M231 307.14 216.36 293l-23.71-6.55-10.08-5 6.55 13.12-3 4.54-4-1 1.52 9.08 3 5-2.52 5-5 6.56-9.12 4.57-10.09 3-15.13-3s-4.28 5.29-5.55 5.54-4.53 3-4.53 3v4l1 2.52 4 1.51v2l-.51 1h5.55l11.1 3 1-3 4.53 1h4l3-5 2-1H175l6.05-2 6.56-7.07h2l17.65-20.17-1.51-4v-1.52l1-1 18.16 2 6-2Z"
|
||||
style={{
|
||||
fill: '#05080f',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
/>
|
||||
</g>
|
||||
<g data-name="Hojas Brillos">
|
||||
<path
|
||||
d="M104.33 246a79.49 79.49 0 0 0-11.17-13.28c-6.13-5.58-26.39-25-20.36-52.52a131.74 131.74 0 0 0 4.41-14.51 119.7 119.7 0 0 0 .7 15.12c.84-13.22 5.44-22.19 9-26.52-2.63 19.43-.84 61 16.63 75.58a68.14 68.14 0 0 1-15.08-11.45s15 22.59 15.85 27.58ZM266.16 287.77a49.74 49.74 0 0 0-9.76 4.53c-4.33 2.77-19.21 11.74-34.78 3.71a81.05 81.05 0 0 0-8-5 76.07 76.07 0 0 0 9.19 2c-7.8-2.63-12.44-6.82-14.47-9.65 11.24 4.69 36.49 10.32 48 2.23a42 42 0 0 1-9.28 7.16s15.95-5.29 19.09-5ZM131.72 254.94s-20.64-16-15.52-37.38c6.51 14.94 9.24 31.38 15.52 37.38ZM223.61 306.32a80 80 0 0 0-17-3.29c-8.28-.48-36.22-2.8-48.86-28A130 130 0 0 0 152 261a118.64 118.64 0 0 0 10.06 11.31c-7.66-10.81-9.73-20.67-9.67-26.29 10.18 16.76 37.74 48 60.47 48.28a68 68 0 0 1-18.92.59s25.83 8.15 29.67 11.46Z"
|
||||
style={{
|
||||
fill: '#ced7a8',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M142.85 256s-24.06-10.07-24.66-32.1c10.16 12.76 17.05 27.93 24.66 32.1ZM96.77 240.07a56.35 56.35 0 0 0-9.84-6.81c-5.09-2.63-22.07-11.91-23.16-30.86a85.13 85.13 0 0 0 .23-10.19 81.92 81.92 0 0 0 3.26 9.79c-1.88-8.76-.49-15.36 1.08-18.78 1.84 13.1 10.7 39.92 24.94 46.51a48.21 48.21 0 0 1-12.08-5s14 12.25 15.57 15.36ZM197.69 296.53a62.79 62.79 0 0 0-13.63.81c-6.41 1.25-28.17 4.94-42.72-11.88a102.85 102.85 0 0 0-7.08-9.65 94.3 94.3 0 0 0 9.87 6.7c-7.95-6.78-11.44-13.93-12.5-18.24 11 10.84 38.12 29.34 55.52 25.14a53.16 53.16 0 0 1-14.31 4.14s21.28 1.19 24.85 3Z"
|
||||
style={{
|
||||
fill: '#ced7a8',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
/>
|
||||
</g>
|
||||
<g data-name="Hojas SOmbras">
|
||||
<path
|
||||
d="M116 226.33s10.53 16.92 15 27c-2.19-1.39-9.93-15.82-9.93-15.82M229.33 294s15-2.63 23.48-3c-1.61 1.16-14.07 2.52-14.07 2.52"
|
||||
style={{
|
||||
fill: '#889065',
|
||||
fillRule: 'evenodd'
|
||||
}}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default EntGamers
|
||||
@@ -0,0 +1,66 @@
|
||||
import Typography from '@/components/ui/Typography'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { Container } from '@/styled-system/jsx'
|
||||
import { faChevronRight, faHeart } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import NextLink from 'next/link'
|
||||
import type { FC } from 'react'
|
||||
|
||||
const Footer: FC = () => {
|
||||
return (
|
||||
<footer
|
||||
className={css({
|
||||
backgroundColor: 'surface',
|
||||
color: 'text',
|
||||
paddingY: 'medium'
|
||||
})}
|
||||
>
|
||||
<Container
|
||||
className={css({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { base: 'repeat(3, 1fr)', mdDown: '1fr' }
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
<Typography variant="h3" component="div"> Acerca de </Typography>
|
||||
<ul className="fa-ul">
|
||||
<li>
|
||||
<span className="fa-li">
|
||||
<FontAwesomeIcon icon={faChevronRight} />
|
||||
</span>
|
||||
<NextLink href="/acerca-de"> EntGamers</NextLink>
|
||||
</li>
|
||||
<li>
|
||||
<span className="fa-li">
|
||||
<FontAwesomeIcon icon={faChevronRight} />
|
||||
</span>
|
||||
<NextLink href="/clanes"> Clanes</NextLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="h3" component="div"> Contacto </Typography>
|
||||
</div>
|
||||
<div></div>
|
||||
</Container>
|
||||
<Container
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
})}
|
||||
>
|
||||
<Typography variant="body2" component="div">
|
||||
Hecho con
|
||||
{' '}
|
||||
<FontAwesomeIcon className={css({ color: 'red' })} icon={faHeart} />
|
||||
{' '}
|
||||
por
|
||||
{' '}
|
||||
<a href="https://srjuggernaut.dev">SrJuggernaut</a>
|
||||
</Typography>
|
||||
</Container>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
export default Footer
|
||||
@@ -0,0 +1,83 @@
|
||||
'use client'
|
||||
import EntGamers from '@/assets/logos/EntGamers'
|
||||
import Menu from '@/components/layout/Menu'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { Container } from '@/styled-system/jsx'
|
||||
import NextLink from 'next/link'
|
||||
import { useCallback, useEffect, useState, type FC } from 'react'
|
||||
import SessionButtons from './Header/SessionButtons'
|
||||
|
||||
const Header: FC = () => {
|
||||
const [isScrolled, setIsScrolled] = useState(typeof window !== 'undefined' ? window.scrollY > 0 : false)
|
||||
|
||||
const handleScroll = useCallback(() => {
|
||||
if (typeof window === 'undefined') return
|
||||
setIsScrolled(window.scrollY > 0)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
}, [handleScroll])
|
||||
|
||||
return (
|
||||
<>
|
||||
<header
|
||||
className={css({
|
||||
'display': 'flex',
|
||||
'alignItems': 'center',
|
||||
'justifyContent': 'center',
|
||||
'backgroundColor': 'transparent',
|
||||
'color': 'text',
|
||||
'minHeight': '60px',
|
||||
'position': 'fixed',
|
||||
'top': 0,
|
||||
'left': 0,
|
||||
'width': '100%',
|
||||
'zIndex': 'sticky',
|
||||
'boxShadow': 'none',
|
||||
'transitionProperty': 'background-color, box-shadow',
|
||||
'transitionDuration': '0.25s',
|
||||
'transitionTimingFunction': 'easeInOut',
|
||||
'willChange': 'background-color, box-shadow',
|
||||
'&[data-scrolled=true]': {
|
||||
backgroundColor: 'surface',
|
||||
boxShadow: '2px 2px 4px 0px rgba(0, 0, 0, 0.25)'
|
||||
}
|
||||
})}
|
||||
data-scrolled={isScrolled}
|
||||
>
|
||||
<Container
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
<NextLink
|
||||
href="/"
|
||||
>
|
||||
<EntGamers
|
||||
width="40px"
|
||||
/>
|
||||
</NextLink>
|
||||
</div>
|
||||
<div>
|
||||
<SessionButtons />
|
||||
<Menu />
|
||||
</div>
|
||||
</Container>
|
||||
</header>
|
||||
<div
|
||||
className={css({
|
||||
height: '60px'
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default Header
|
||||
@@ -0,0 +1,100 @@
|
||||
import IconButton from '@/components/ui/IconButton'
|
||||
import Tooltip from '@/components/ui/Tooltip'
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch'
|
||||
import { useAppSelector } from '@/hooks/useAppSelector'
|
||||
import useManageError from '@/hooks/useManageError'
|
||||
import { setCurrentUser, setSession, setStatus } from '@/state/sessionSlice'
|
||||
import { iconButton } from '@/styled-system/recipes'
|
||||
import { faCogs, faRightFromBracket } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faUser } from '@fortawesome/free-solid-svg-icons/faUser'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { ADMIN_CLAN_ID, MODERATOR_CLAN_ID } from 'entgamers-database/frontend/clanes/administrative'
|
||||
import { logout } from 'entgamers-database/frontend/session'
|
||||
import NextLink from 'next/link'
|
||||
import type { FC } from 'react'
|
||||
|
||||
const SessionButtons: FC = () => {
|
||||
const { session, status, clanes } = useAppSelector((state) => state.session)
|
||||
const { manageError } = useManageError()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
if (status !== 'idle') return null
|
||||
|
||||
if (status === 'idle' && session === undefined) {
|
||||
return (
|
||||
<Tooltip
|
||||
title="Iniciar sesión"
|
||||
position="bottom"
|
||||
>
|
||||
<NextLink
|
||||
href="/login"
|
||||
className={
|
||||
iconButton()
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faUser} />
|
||||
</NextLink>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
if (status === 'idle' && session !== undefined) {
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
title="Mi cuenta"
|
||||
position="bottom"
|
||||
>
|
||||
<NextLink
|
||||
href="/cuenta"
|
||||
className={
|
||||
iconButton()
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faUser} />
|
||||
</NextLink>
|
||||
</Tooltip>
|
||||
{clanes?.teams.some((team) => team.$id === ADMIN_CLAN_ID || team.$id === MODERATOR_CLAN_ID) && (
|
||||
<Tooltip
|
||||
title="Panel de administración"
|
||||
position="bottom"
|
||||
>
|
||||
<NextLink
|
||||
href="/dashboard"
|
||||
className={
|
||||
iconButton()
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCogs} />
|
||||
</NextLink>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip
|
||||
title="Cerrar sesión"
|
||||
position="bottom"
|
||||
>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
dispatch(setStatus('loading'))
|
||||
logout('current')
|
||||
.then(() => {
|
||||
dispatch(setSession(undefined))
|
||||
dispatch(setCurrentUser(undefined))
|
||||
})
|
||||
.catch((error) => {
|
||||
manageError(error, 'Error al cerrar sesión', 'Error desconocido al cerrar sesión')
|
||||
})
|
||||
.finally(() => {
|
||||
dispatch(setStatus('idle'))
|
||||
})
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faRightFromBracket} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default SessionButtons
|
||||
@@ -0,0 +1,125 @@
|
||||
import trees from '@/assets/icons/trees'
|
||||
import BackDrop from '@/components/ui/BackDrop'
|
||||
import IconButton from '@/components/ui/IconButton'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { iconButton } from '@/styled-system/recipes'
|
||||
import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
|
||||
import { faBars, faHome, faTimes, faUsers } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import NextLink from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useCallback, useState, type FC } from 'react'
|
||||
|
||||
interface MenuLink {
|
||||
label: string
|
||||
href: string
|
||||
icon: IconDefinition
|
||||
}
|
||||
|
||||
const menuLinks: MenuLink[] = [
|
||||
{ label: 'Home', href: '/', icon: faHome },
|
||||
{ label: 'Clanes', href: '/clanes', icon: trees },
|
||||
{ label: 'Equipo', href: '/equipo', icon: faUsers }
|
||||
]
|
||||
|
||||
const Menu: FC = () => {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||
const pathName = usePathname()
|
||||
const handleClickAway = useCallback(() => {
|
||||
setIsMenuOpen(false)
|
||||
}, [])
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
onClick={() => { setIsMenuOpen(!isMenuOpen) }}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBars} />
|
||||
</IconButton>
|
||||
<BackDrop
|
||||
onClickAway={handleClickAway}
|
||||
isOpen={isMenuOpen}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
right: 0,
|
||||
width: { base: '250px', smDown: '100%' },
|
||||
height: '100%',
|
||||
backgroundColor: 'surface',
|
||||
zIndex: 'modal'
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
height: '60px',
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'border',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'right',
|
||||
padding: '0 1rem'
|
||||
})}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={iconButton({
|
||||
color: 'danger'
|
||||
})}
|
||||
onClick={() => { setIsMenuOpen(!isMenuOpen) }}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTimes} />
|
||||
</button>
|
||||
</div>
|
||||
<nav>
|
||||
<ul
|
||||
className={css({
|
||||
listStyle: 'none',
|
||||
padding: 0,
|
||||
margin: 0
|
||||
})}
|
||||
>
|
||||
{menuLinks.map((menuLink, index) => (
|
||||
<li
|
||||
key={`menu-link-${menuLink.label}-${index.toString()}`}
|
||||
>
|
||||
<NextLink
|
||||
className={css({
|
||||
'display': 'flex',
|
||||
'alignItems': 'center',
|
||||
'justifyContent': 'left',
|
||||
'padding': '1rem',
|
||||
'textDecoration': 'none',
|
||||
'backgroundColor': 'transparent',
|
||||
'color': 'text',
|
||||
'transitionProperty': 'background-color',
|
||||
'transitionDuration': 'normal',
|
||||
'transitionTimingFunction': 'easeInOut',
|
||||
'willChange': 'background-color color',
|
||||
'&:hover': {
|
||||
backgroundColor: 'primary',
|
||||
color: 'primary.contrast'
|
||||
},
|
||||
'&[data-active=true]': {
|
||||
backgroundColor: 'info',
|
||||
color: 'info.contrast'
|
||||
}
|
||||
})}
|
||||
href={menuLink.href}
|
||||
data-active={pathName === menuLink.href}
|
||||
onClick={() => { setIsMenuOpen(false) }}
|
||||
>
|
||||
<FontAwesomeIcon icon={menuLink.icon} />
|
||||
|
||||
{menuLink.label}
|
||||
</NextLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</BackDrop>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default Menu
|
||||
@@ -0,0 +1,41 @@
|
||||
import { cx } from '@/styled-system/css'
|
||||
import { alert, type AlertVariantProps } from '@/styled-system/recipes/alert'
|
||||
import type { MergeOmitting } from '@/types/utilities'
|
||||
import { faTimes } from '@fortawesome/free-solid-svg-icons/faTimes'
|
||||
import { FontAwesomeIcon, type FontAwesomeIconProps } from '@fortawesome/react-fontawesome'
|
||||
import type { DetailedHTMLProps, FC, HTMLAttributes, ReactNode } from 'react'
|
||||
import IconButton, { type IconButtonProps } from './IconButton'
|
||||
|
||||
type ComposedAlertProps = MergeOmitting<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, AlertVariantProps>
|
||||
|
||||
const Alert: FC<ComposedAlertProps> = ({ className, children, ...props }) => {
|
||||
const [alertRecipeArgs, allOtherAlertProps] = alert.splitVariantProps(props)
|
||||
return (
|
||||
<div
|
||||
className={cx(alert(alertRecipeArgs).body, className)}
|
||||
{...allOtherAlertProps}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type ComposedAlertCloseButtonProps = MergeOmitting<IconButtonProps, AlertVariantProps> & {
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
export const AlertCloseButton: FC<ComposedAlertCloseButtonProps> = ({ children, className, ...props }) => {
|
||||
const [alertRecipeArgs, allOtherAlertProps] = alert.splitVariantProps(props)
|
||||
return (
|
||||
<IconButton
|
||||
className={cx(alert(alertRecipeArgs).closeButton, className)}
|
||||
{...allOtherAlertProps}
|
||||
>
|
||||
{children === undefined
|
||||
? <FontAwesomeIcon icon={faTimes as FontAwesomeIconProps['icon']} size="sm" />
|
||||
: children}
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
|
||||
export default Alert
|
||||
@@ -0,0 +1,45 @@
|
||||
import { css } from '@/styled-system/css'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { type FC, type ReactNode } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
export interface BackDropProps {
|
||||
children?: ReactNode
|
||||
isOpen: boolean
|
||||
onClickAway: () => void
|
||||
}
|
||||
|
||||
const BackDrop: FC<BackDropProps> = ({ isOpen, onClickAway, children }) => {
|
||||
if (typeof window === 'undefined') return null
|
||||
return createPortal((
|
||||
<AnimatePresence>
|
||||
{isOpen
|
||||
? (
|
||||
<motion.div
|
||||
className={css({
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
zIndex: 'modalBackdrop',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)'
|
||||
})}
|
||||
onClick={(event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
onClickAway()
|
||||
}
|
||||
}}
|
||||
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
)
|
||||
: undefined}
|
||||
</AnimatePresence>
|
||||
), document.body)
|
||||
}
|
||||
export default BackDrop
|
||||
@@ -0,0 +1,20 @@
|
||||
import { cx } from '@/styled-system/css'
|
||||
import { button, type ButtonVariantProps } from '@/styled-system/recipes/button'
|
||||
import type { MergeOmitting } from '@/types/utilities'
|
||||
import type { ButtonHTMLAttributes, DetailedHTMLProps, FC } from 'react'
|
||||
|
||||
export type ButtonProps = MergeOmitting<DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, ButtonVariantProps>
|
||||
|
||||
const Button: FC<ButtonProps> = ({ children, className, ...rest }) => {
|
||||
const [buttonRecipeArgs, allOtherButtonProps] = button.splitVariantProps(rest)
|
||||
return (
|
||||
<button
|
||||
className={cx(button(buttonRecipeArgs), className)}
|
||||
{...allOtherButtonProps}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default Button
|
||||
@@ -0,0 +1,20 @@
|
||||
import { cx } from '@/styled-system/css'
|
||||
import { buttonGroup, type ButtonGroupVariantProps } from '@/styled-system/recipes/button-group'
|
||||
import { type MergeOmitting } from '@/types/utilities'
|
||||
import { type DetailedHTMLProps, type FC, type HTMLAttributes } from 'react'
|
||||
|
||||
export type ButtonGroupProps = MergeOmitting<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, ButtonGroupVariantProps>
|
||||
|
||||
const ButtonGroup: FC<ButtonGroupProps> = ({ children, className, ...rest }) => {
|
||||
const [buttonGroupRecipeArgs, allOtherButtonGroupProps] = buttonGroup.splitVariantProps(rest)
|
||||
return (
|
||||
<div
|
||||
className={cx(buttonGroup(buttonGroupRecipeArgs), className)}
|
||||
{...allOtherButtonGroupProps}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ButtonGroup
|
||||
@@ -0,0 +1,19 @@
|
||||
import { cx } from '@/styled-system/css'
|
||||
import { chip, type ChipVariantProps } from '@/styled-system/recipes/chip'
|
||||
import { type MergeOmitting } from '@/types/utilities'
|
||||
import { type DetailedHTMLProps, type FC, type HTMLAttributes } from 'react'
|
||||
|
||||
export type ChipProps = MergeOmitting<DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, ChipVariantProps>
|
||||
|
||||
const Chip: FC<ChipProps> = ({ children, className, ...rest }) => {
|
||||
const [chipRecipeArgs, allOtherChipProps] = chip.splitVariantProps(rest)
|
||||
return (
|
||||
<span
|
||||
className={cx(chip(chipRecipeArgs), className)}
|
||||
{...allOtherChipProps}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
export default Chip
|
||||
@@ -0,0 +1,36 @@
|
||||
import { cx } from '@/styled-system/css'
|
||||
import { collapse, type CollapseVariantProps } from '@/styled-system/recipes/collapse'
|
||||
import { type MergeOmitting } from '@/types/utilities'
|
||||
import { type DetailedHTMLProps, type DetailsHTMLAttributes, type FC, type HTMLAttributes, type ReactNode } from 'react'
|
||||
|
||||
export type CollapseProps = MergeOmitting<DetailedHTMLProps<DetailsHTMLAttributes<HTMLDetailsElement>, HTMLDetailsElement>, CollapseVariantProps> & {
|
||||
children?: ReactNode
|
||||
contentProps?: HTMLAttributes<HTMLElement>
|
||||
summary: ReactNode
|
||||
summaryProps?: HTMLAttributes<HTMLElement>
|
||||
}
|
||||
|
||||
const Collapse: FC<CollapseProps> = ({ children, className, summary, summaryProps, contentProps, ...rest }) => {
|
||||
const [collapseRecipeArgs, allOtherCollapseProps] = collapse.splitVariantProps(rest)
|
||||
return (
|
||||
<details
|
||||
className={cx(collapse(collapseRecipeArgs).root, className)}
|
||||
{...allOtherCollapseProps}
|
||||
>
|
||||
<summary
|
||||
{...summaryProps}
|
||||
className={cx(collapse(collapseRecipeArgs).summary, summaryProps?.className)}
|
||||
>
|
||||
{summary}
|
||||
</summary>
|
||||
<div
|
||||
{...contentProps}
|
||||
className={cx(collapse(collapseRecipeArgs).content, contentProps?.className)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</details>
|
||||
)
|
||||
}
|
||||
|
||||
export default Collapse
|
||||
@@ -0,0 +1,20 @@
|
||||
import { cx } from '@/styled-system/css'
|
||||
import { iconButton, type IconButtonVariantProps } from '@/styled-system/recipes/icon-button'
|
||||
import type { MergeOmitting } from '@/types/utilities'
|
||||
import type { ButtonHTMLAttributes, DetailedHTMLProps, FC } from 'react'
|
||||
|
||||
export type IconButtonProps = MergeOmitting<DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, IconButtonVariantProps>
|
||||
|
||||
const IconButton: FC<IconButtonProps> = ({ children, className, ...rest }) => {
|
||||
const [iconButtonRecipeArgs, allOtherIconButtonProps] = iconButton.splitVariantProps(rest)
|
||||
return (
|
||||
<button
|
||||
className={cx(iconButton(iconButtonRecipeArgs), className)}
|
||||
{...allOtherIconButtonProps}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default IconButton
|
||||
@@ -0,0 +1,20 @@
|
||||
import { cx } from '@/styled-system/css'
|
||||
import { listGroup, type ListGroupVariantProps } from '@/styled-system/recipes/list-group'
|
||||
import { type MergeOmitting } from '@/types/utilities'
|
||||
import { type DetailedHTMLProps, type FC, type HTMLAttributes } from 'react'
|
||||
|
||||
type ComposedInputProps = MergeOmitting<DetailedHTMLProps<HTMLAttributes<HTMLUListElement>, HTMLUListElement>, ListGroupVariantProps>
|
||||
|
||||
const ListGroup: FC<ComposedInputProps> = ({ children, className, ...rest }) => {
|
||||
const [listGroupRecipeArgs, allOtherListGroupProps] = listGroup.splitVariantProps(rest)
|
||||
return (
|
||||
<ul
|
||||
className={cx(listGroup(listGroupRecipeArgs).root, className)}
|
||||
{...allOtherListGroupProps}
|
||||
>
|
||||
{children}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export default ListGroup
|
||||
@@ -0,0 +1,19 @@
|
||||
import { cx } from '@/styled-system/css'
|
||||
import { listGroup, type ListGroupVariantProps } from '@/styled-system/recipes/list-group'
|
||||
import { type MergeOmitting } from '@/types/utilities'
|
||||
import { type DetailedHTMLProps, type FC, type LiHTMLAttributes } from 'react'
|
||||
|
||||
type ComposedInputProps = MergeOmitting<DetailedHTMLProps<LiHTMLAttributes<HTMLLIElement>, HTMLLIElement>, ListGroupVariantProps>
|
||||
|
||||
const ListGroupItem: FC<ComposedInputProps> = ({ children, className, ...rest }) => {
|
||||
const [listGroupRecipeArgs, allOtherListGroupProps] = listGroup.splitVariantProps(rest)
|
||||
return (
|
||||
<li
|
||||
className={cx(listGroup(listGroupRecipeArgs).item, className)}
|
||||
{...allOtherListGroupProps}
|
||||
>
|
||||
{children}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
export default ListGroupItem
|
||||
@@ -0,0 +1,18 @@
|
||||
import { cx } from '@/styled-system/css'
|
||||
import { skeleton, type SkeletonVariantProps } from '@/styled-system/recipes/skeleton'
|
||||
import { type MergeOmitting } from '@/types/utilities'
|
||||
import { type DetailedHTMLProps, type FC, type HTMLAttributes } from 'react'
|
||||
|
||||
export type SkeletonProps = MergeOmitting<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, SkeletonVariantProps & { children?: never }>
|
||||
|
||||
const Skeleton: FC<SkeletonProps> = ({ children: _, className, ...props }) => {
|
||||
const [skeletonRecipeArgs, allOtherSkeletonProps] = skeleton.splitVariantProps(props)
|
||||
return (
|
||||
<div
|
||||
{...allOtherSkeletonProps}
|
||||
className={cx(skeleton(skeletonRecipeArgs), className)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Skeleton
|
||||
@@ -0,0 +1,112 @@
|
||||
import { cx } from '@/styled-system/css'
|
||||
import { table, type TableVariantProps } from '@/styled-system/recipes/table'
|
||||
import { type MergeOmitting } from '@/types/utilities'
|
||||
import { type DetailedHTMLProps, type FC, type HTMLAttributes } from 'react'
|
||||
|
||||
type TableContainerProps = MergeOmitting<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, TableVariantProps>
|
||||
|
||||
export const TableContainer: FC<TableContainerProps> = ({ children, className, ...props }) => {
|
||||
const [tableRecipeArgs, allOtherTableProps] = table.splitVariantProps(props)
|
||||
return (
|
||||
<div
|
||||
{...allOtherTableProps}
|
||||
className={cx(table(tableRecipeArgs).container, className)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type TableProps = MergeOmitting<DetailedHTMLProps<HTMLAttributes<HTMLTableElement>, HTMLTableElement>, TableVariantProps>
|
||||
|
||||
export const Table: FC<TableProps> = ({ children, className, ...props }) => {
|
||||
const [tableRecipeArgs, allOtherTableProps] = table.splitVariantProps(props)
|
||||
return (
|
||||
<table
|
||||
{...allOtherTableProps}
|
||||
className={cx(table(tableRecipeArgs).table, className)}
|
||||
>
|
||||
{children}
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
type TableHeadProps = MergeOmitting<DetailedHTMLProps<HTMLAttributes<HTMLTableSectionElement>, HTMLTableSectionElement>, TableVariantProps>
|
||||
|
||||
export const TableHead: FC<TableHeadProps> = ({ children, className, ...props }) => {
|
||||
const [tableRecipeArgs, allOtherTableProps] = table.splitVariantProps(props)
|
||||
return (
|
||||
<thead
|
||||
{...allOtherTableProps}
|
||||
className={cx(table(tableRecipeArgs).thead, className)}
|
||||
>
|
||||
{children}
|
||||
</thead>
|
||||
)
|
||||
}
|
||||
|
||||
type TableBodyProps = MergeOmitting<DetailedHTMLProps<HTMLAttributes<HTMLTableSectionElement>, HTMLTableSectionElement>, TableVariantProps>
|
||||
|
||||
export const TableBody: FC<TableBodyProps> = ({ children, className, ...props }) => {
|
||||
const [tableRecipeArgs, allOtherTableProps] = table.splitVariantProps(props)
|
||||
return (
|
||||
<tbody
|
||||
{...allOtherTableProps}
|
||||
className={cx(table(tableRecipeArgs).tbody, className)}
|
||||
>
|
||||
{children}
|
||||
</tbody>
|
||||
)
|
||||
}
|
||||
|
||||
type TableFootProps = MergeOmitting<DetailedHTMLProps<HTMLAttributes<HTMLTableSectionElement>, HTMLTableSectionElement>, TableVariantProps>
|
||||
|
||||
export const TableFoot: FC<TableFootProps> = ({ children, className, ...props }) => {
|
||||
const [tableRecipeArgs, allOtherTableProps] = table.splitVariantProps(props)
|
||||
return (
|
||||
<tfoot
|
||||
{...allOtherTableProps}
|
||||
className={cx(table(tableRecipeArgs).tfoot, className)}
|
||||
>
|
||||
{children}
|
||||
</tfoot>
|
||||
)
|
||||
}
|
||||
|
||||
type TableRowProps = MergeOmitting<DetailedHTMLProps<HTMLAttributes<HTMLTableRowElement>, HTMLTableRowElement>, TableVariantProps>
|
||||
|
||||
export const TableRow: FC<TableRowProps> = ({ children, className, ...props }) => {
|
||||
const [tableRecipeArgs, allOtherTableProps] = table.splitVariantProps(props)
|
||||
return (
|
||||
<tr
|
||||
{...allOtherTableProps}
|
||||
className={cx(table(tableRecipeArgs).tr, className)}
|
||||
>
|
||||
{children}
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
type TableCellProps = DetailedHTMLProps<HTMLAttributes<HTMLTableCellElement>, HTMLTableCellElement>
|
||||
|
||||
export const TableCell: FC<TableCellProps> = ({ children, ...props }) => {
|
||||
return (
|
||||
<td
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</td>
|
||||
)
|
||||
}
|
||||
|
||||
type TableHeadCellProps = DetailedHTMLProps<HTMLAttributes<HTMLTableCellElement>, HTMLTableCellElement>
|
||||
|
||||
export const TableHeadCell: FC<TableHeadCellProps> = ({ children, ...props }) => {
|
||||
return (
|
||||
<th
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</th>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { cx } from '@/styled-system/css'
|
||||
import { tooltip, type TooltipVariantProps } from '@/styled-system/recipes/tooltip'
|
||||
import { type MergeOmitting } from '@/types/utilities'
|
||||
import { type DetailedHTMLProps, type FC, type HTMLAttributes, type ReactNode } from 'react'
|
||||
|
||||
type ComposedTooltipProps = MergeOmitting<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, TooltipVariantProps> & {
|
||||
title: ReactNode
|
||||
}
|
||||
|
||||
const Tooltip: FC<ComposedTooltipProps> = ({ children, className, title, ...rest }) => {
|
||||
const [tooltipRecipeArgs, allOtherTooltipProps] = tooltip.splitVariantProps(rest)
|
||||
return (
|
||||
<div
|
||||
className={cx(tooltip(tooltipRecipeArgs), className)}
|
||||
{...allOtherTooltipProps}
|
||||
>
|
||||
{children}
|
||||
<span
|
||||
className="tooltip__content"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tooltip
|
||||
@@ -0,0 +1,53 @@
|
||||
import { cx } from '@/styled-system/css'
|
||||
import { typography, type TypographyVariantProps } from '@/styled-system/recipes/typography'
|
||||
import type { MergeOmitting } from '@/types/utilities'
|
||||
import { type ElementType, type FC, type HTMLAttributes, createElement } from 'react'
|
||||
|
||||
type ComposedTypographyProps = MergeOmitting<HTMLAttributes<HTMLElement>, TypographyVariantProps>
|
||||
|
||||
export interface TypographyProps extends ComposedTypographyProps {
|
||||
component?: ElementType
|
||||
}
|
||||
|
||||
const variantToComponent = (variant: TypographyVariantProps['variant']): ElementType => {
|
||||
switch (variant) {
|
||||
case 'h1':
|
||||
case 'h2':
|
||||
case 'h3':
|
||||
case 'h4':
|
||||
case 'h5':
|
||||
case 'h6':
|
||||
return variant
|
||||
case 'subtitle1':
|
||||
case 'subtitle2':
|
||||
return 'div'
|
||||
case 'button':
|
||||
case 'overline':
|
||||
case 'srOnly':
|
||||
case 'caption':
|
||||
return 'span'
|
||||
case 'body1':
|
||||
case 'body2':
|
||||
default:
|
||||
return 'p'
|
||||
}
|
||||
}
|
||||
|
||||
const Typography: FC<TypographyProps> = ({ children, className, component, ...rest }) => {
|
||||
const [typographyRecipeArgs, allOtherTypographyProps] = typography.splitVariantProps(rest)
|
||||
typographyRecipeArgs.color = typographyRecipeArgs.color ?? (
|
||||
typeof typographyRecipeArgs.variant === 'string' && typographyRecipeArgs.variant.startsWith('h')
|
||||
? 'primary'
|
||||
: 'inherit'
|
||||
)
|
||||
return createElement(
|
||||
component ?? variantToComponent(typographyRecipeArgs.variant),
|
||||
{
|
||||
className: cx(typography(typographyRecipeArgs), className),
|
||||
...allOtherTypographyProps
|
||||
},
|
||||
children
|
||||
)
|
||||
}
|
||||
|
||||
export default Typography
|
||||
@@ -0,0 +1,36 @@
|
||||
import Input, { type InputProps } from '@/components/ui/form/Input'
|
||||
import { useEffect, useState, type FC } from 'react'
|
||||
|
||||
interface DebouncedInputProps extends Omit<InputProps, 'value' | 'onChange'> {
|
||||
value: string | number
|
||||
onChange: (value: string | number) => void
|
||||
debounce?: number
|
||||
}
|
||||
|
||||
const DebouncedInput: FC<DebouncedInputProps> = ({ value: initialValue, onChange, debounce = 500, ...props }) => {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
|
||||
useEffect(() => {
|
||||
setValue(initialValue)
|
||||
}, [initialValue])
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
onChange(value)
|
||||
}, debounce)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}, [value, onChange, debounce])
|
||||
|
||||
return (
|
||||
<Input
|
||||
{...props}
|
||||
value={value}
|
||||
onChange={(e) => { setValue(e.target.value) }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default DebouncedInput
|
||||
@@ -0,0 +1,23 @@
|
||||
import { css, cx } from '@/styled-system/css'
|
||||
import { type DetailedHTMLProps, type FC, type HTMLAttributes, type ReactNode } from 'react'
|
||||
|
||||
export interface FormGroupProps extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const FormGroup: FC<FormGroupProps> = ({ children, className, ...props }) => {
|
||||
return (
|
||||
<div
|
||||
className={cx(css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 'small',
|
||||
paddingBlock: 'small'
|
||||
}), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default FormGroup
|
||||
@@ -0,0 +1,18 @@
|
||||
import { cx } from '@/styled-system/css'
|
||||
import { input, type InputVariantProps } from '@/styled-system/recipes/input'
|
||||
import type { MergeOmitting } from '@/types/utilities'
|
||||
import type { DetailedHTMLProps, FC, InputHTMLAttributes } from 'react'
|
||||
|
||||
export type InputProps = MergeOmitting<DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, InputVariantProps>
|
||||
|
||||
const Input: FC<InputProps> = ({ className, ...rest }) => {
|
||||
const [inputRecipeArgs, allOtherInputProps] = input.splitVariantProps(rest)
|
||||
return (
|
||||
<input
|
||||
className={cx(input(inputRecipeArgs), className)}
|
||||
{...allOtherInputProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Input
|
||||
@@ -0,0 +1,57 @@
|
||||
import IconButton from '@/components/ui/IconButton'
|
||||
import Tooltip from '@/components/ui/Tooltip'
|
||||
import { css, cx } from '@/styled-system/css'
|
||||
import { input, type InputVariantProps } from '@/styled-system/recipes/input'
|
||||
import type { MergeOmitting } from '@/types/utilities'
|
||||
import { faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { useState, type FC, type InputHTMLAttributes } from 'react'
|
||||
|
||||
export type InputProps = MergeOmitting<Omit<InputHTMLAttributes<HTMLInputElement>, 'type'>, InputVariantProps>
|
||||
|
||||
const PasswordInput: FC<InputProps> = ({ className, ...props }) => {
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [inputCss, rest] = input.splitVariantProps(props)
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
position: 'relative'
|
||||
})}
|
||||
>
|
||||
<input
|
||||
className={cx(
|
||||
input(inputCss),
|
||||
css({ fontSize: 'medium' }),
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
/>
|
||||
<div
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
right: '16px',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)'
|
||||
})}
|
||||
>
|
||||
<Tooltip
|
||||
title={showPassword ? 'Ocultar contraseña' : 'Mostrar contraseña'}
|
||||
position="top"
|
||||
>
|
||||
<IconButton
|
||||
type="button"
|
||||
color="primary"
|
||||
size="small"
|
||||
onClick={() => { setShowPassword(!showPassword) }}
|
||||
>
|
||||
<FontAwesomeIcon icon={showPassword ? faEyeSlash : faEye} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default PasswordInput
|
||||
@@ -0,0 +1,32 @@
|
||||
import { css, cx } from '@/styled-system/css'
|
||||
import { input, type InputVariantProps } from '@/styled-system/recipes/input'
|
||||
import { type MergeOmitting } from '@/types/utilities'
|
||||
import { type DetailedHTMLProps, type FC, type TextareaHTMLAttributes } from 'react'
|
||||
|
||||
export type InputProps = MergeOmitting<DetailedHTMLProps<TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>, InputVariantProps>
|
||||
|
||||
const TextArea: FC<InputProps> = ({ className, onChange, ...props }) => {
|
||||
const [textAreaCss, rest] = input.splitVariantProps(props)
|
||||
return (
|
||||
<textarea
|
||||
className={cx(css({
|
||||
resize: 'none',
|
||||
overflow: 'auto'
|
||||
}), input(textAreaCss), className)}
|
||||
onChange={(event) => {
|
||||
if (event.target.value.length > 0) {
|
||||
event.target.style.height = 'auto'
|
||||
event.target.style.height = `${event.target.scrollHeight}px`
|
||||
} else {
|
||||
event.target.style.height = 'auto'
|
||||
}
|
||||
if (onChange !== undefined) {
|
||||
onChange(event)
|
||||
}
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default TextArea
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { AppDispatch } from '@/state/store'
|
||||
import { useDispatch } from 'react-redux'
|
||||
|
||||
// Use throughout your app instead of plain `useDispatch` and `useSelector`
|
||||
export const useAppDispatch: () => AppDispatch = useDispatch
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { RootState } from '@/state/store'
|
||||
import type { TypedUseSelectorHook } from 'react-redux'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
|
||||
@@ -0,0 +1,41 @@
|
||||
import { addAlert } from '@/state/feedbackSlice'
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { AppwriteException } from 'appwrite'
|
||||
import { useAppDispatch } from './useAppDispatch'
|
||||
|
||||
type ManageError = (error: unknown, errorTitle: string, defaultMessage?: string, severity?: 'error') => void
|
||||
|
||||
type UseManageError = () => {
|
||||
manageError: ManageError
|
||||
}
|
||||
|
||||
const useManageError: UseManageError = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
const manageError: ManageError = (error, errorTitle, defaultMessage, severity): void => {
|
||||
if (error instanceof AppwriteException) {
|
||||
dispatch(addAlert({
|
||||
id: nanoid(),
|
||||
title: errorTitle,
|
||||
message: error.message ?? defaultMessage,
|
||||
severity: severity ?? 'error'
|
||||
}))
|
||||
} else if (error instanceof Error) {
|
||||
dispatch(addAlert({
|
||||
id: nanoid(),
|
||||
title: errorTitle,
|
||||
message: error.message ?? defaultMessage,
|
||||
severity: severity ?? 'error'
|
||||
}))
|
||||
} else {
|
||||
dispatch(addAlert({
|
||||
id: nanoid(),
|
||||
title: errorTitle,
|
||||
message: 'Error desconocido',
|
||||
severity: severity ?? 'error'
|
||||
}))
|
||||
}
|
||||
}
|
||||
return { manageError }
|
||||
}
|
||||
|
||||
export default useManageError
|
||||
@@ -0,0 +1,26 @@
|
||||
import { type SessionState } from '@/state/sessionSlice'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useAppSelector } from './useAppSelector'
|
||||
|
||||
type UseSession = (redirect?: string) => SessionState & { belongToClan: (clanId: string) => boolean }
|
||||
|
||||
const useSession: UseSession = (redirect?: string) => {
|
||||
const { status, session, user, clanes } = useAppSelector((state) => state.session)
|
||||
const router = useRouter()
|
||||
|
||||
const belongToClan = useCallback((clanId: string): boolean => {
|
||||
if (session === undefined || clanes === undefined || clanes.total === 0) return false
|
||||
return clanes.teams.some((team) => team.$id === clanId) ?? false
|
||||
}, [clanes])
|
||||
|
||||
useEffect(() => {
|
||||
if (status === 'idle' && session === undefined) {
|
||||
router.push(redirect ?? '/')
|
||||
}
|
||||
}, [status, session])
|
||||
|
||||
return { status, session, user, clanes, belongToClan }
|
||||
}
|
||||
|
||||
export default useSession
|
||||
@@ -0,0 +1,33 @@
|
||||
import { type Alert } from '@/types/feedback'
|
||||
import { createSlice, type PayloadAction } from '@reduxjs/toolkit'
|
||||
|
||||
interface FeedbackState {
|
||||
alerts: Alert[]
|
||||
}
|
||||
|
||||
const initialState: FeedbackState = {
|
||||
alerts: []
|
||||
}
|
||||
|
||||
const feedbackSlice = createSlice({
|
||||
name: 'feedback',
|
||||
initialState,
|
||||
reducers: {
|
||||
addAlert(state, action: PayloadAction<Alert>) {
|
||||
return {
|
||||
...state,
|
||||
alerts: [...state.alerts, action.payload]
|
||||
}
|
||||
},
|
||||
removeAlert(state, action: PayloadAction<string>) {
|
||||
return {
|
||||
...state,
|
||||
alerts: state.alerts.filter((alert) => alert.id !== action.payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const { addAlert, removeAlert } = feedbackSlice.actions
|
||||
|
||||
export default feedbackSlice
|
||||
@@ -0,0 +1,51 @@
|
||||
import { createSlice, type PayloadAction } from '@reduxjs/toolkit'
|
||||
import type { Models } from 'appwrite'
|
||||
import type { ClanList } from 'entgamers-database/frontend/clanes'
|
||||
import type { User } from 'entgamers-database/frontend/session'
|
||||
|
||||
export type SessionState
|
||||
= | {
|
||||
status: 'idle' | 'loading' | 'initializing'
|
||||
session?: Models.Session
|
||||
user?: User
|
||||
clanes?: ClanList
|
||||
}
|
||||
|
||||
const initialState: SessionState = {
|
||||
status: 'initializing'
|
||||
}
|
||||
|
||||
const sessionSlice = createSlice({
|
||||
name: 'session',
|
||||
initialState,
|
||||
reducers: {
|
||||
setStatus: (state, action: PayloadAction<SessionState['status']>) => {
|
||||
return {
|
||||
...state,
|
||||
status: action.payload
|
||||
}
|
||||
},
|
||||
setSession: (state, action: PayloadAction<SessionState['session']>) => {
|
||||
return {
|
||||
...state,
|
||||
session: action.payload
|
||||
}
|
||||
},
|
||||
setCurrentUser: (state, action: PayloadAction<SessionState['user']>) => {
|
||||
return {
|
||||
...state,
|
||||
user: action.payload
|
||||
}
|
||||
},
|
||||
setClanes: (state, action: PayloadAction<SessionState['clanes']>) => {
|
||||
return {
|
||||
...state,
|
||||
clanes: action.payload
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const { setStatus, setSession, setCurrentUser, setClanes } = sessionSlice.actions
|
||||
|
||||
export default sessionSlice
|
||||
@@ -0,0 +1,15 @@
|
||||
import feedbackSlice from '@/state/feedbackSlice'
|
||||
import sessionSlice from '@/state/sessionSlice'
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
feedback: feedbackSlice.reducer,
|
||||
session: sessionSlice.reducer
|
||||
}
|
||||
})
|
||||
|
||||
export default store
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
@@ -0,0 +1,15 @@
|
||||
import { type IconDefinition } from '@fortawesome/fontawesome-svg-core'
|
||||
|
||||
export type UserPreferences = Record<string, string>
|
||||
|
||||
export interface TeamMember {
|
||||
image: string
|
||||
name: string
|
||||
role: 'moderator' | 'administrator' | 'collaborator'
|
||||
description: string
|
||||
socialNetworks: Array<{
|
||||
label: string
|
||||
url: string
|
||||
icon: IconDefinition
|
||||
}>
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface PaginationOptions {
|
||||
skip?: number
|
||||
take?: number
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface Alert {
|
||||
id: string
|
||||
title: string
|
||||
message: string
|
||||
severity: 'success' | 'info' | 'warning' | 'error'
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export type MergeOmitting<ReplaceableType, ReplacerType> = Omit<ReplaceableType, keyof ReplacerType> & ReplacerType
|
||||
@@ -0,0 +1,14 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { AppwriteException } from 'node-appwrite'
|
||||
import { ValidationError } from 'yup'
|
||||
|
||||
export const handleError = (error: unknown): Response => {
|
||||
if (error instanceof ValidationError) {
|
||||
return NextResponse.json({ error: error.message }, { status: 400 })
|
||||
} else if (error instanceof AppwriteException) {
|
||||
return NextResponse.json({ error: error.message }, { status: error.code ?? 500 })
|
||||
} else {
|
||||
console.error('Unknown error:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { format, formatDistance, setDefaultOptions } from 'date-fns'
|
||||
import { es } from 'date-fns/locale'
|
||||
|
||||
setDefaultOptions({ locale: es })
|
||||
|
||||
const DATE_FORMAT = 'yyyy-MM-dd HH:mm'
|
||||
|
||||
export const formatDate = (date: Date): string => format(date, DATE_FORMAT)
|
||||
|
||||
export const formatDistanceDate = (date: Date): string => formatDistance(date, new Date(), { locale: es })
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { TeamApplication, TeamApplicationList } from 'entgamers-database/types/teamApplications'
|
||||
import { object, string, type ObjectSchema } from 'yup'
|
||||
|
||||
export interface TeamApplicationDynamicParams {
|
||||
id: string
|
||||
}
|
||||
|
||||
export { type TeamApplication, type TeamApplicationList }
|
||||
|
||||
export const teamApplicationDataSchema = object({
|
||||
name: string().required('El nombre es obligatorio'),
|
||||
email: string().email('Invalid email').required('El email es obligatorio'),
|
||||
discord: string().required('El discord es obligatorio'),
|
||||
message: string().required('El mensaje es obligatorio').max(4096, 'El mensaje debe ser menor a 4096 caracteres'),
|
||||
role: string().oneOf(['Admin', 'Moderator', 'Collaborator'], 'Role inválido').required('El rol es obligatorio'),
|
||||
status: string().default('Pending').oneOf(['Pending', 'Accepted', 'Rejected'], 'Status inválido')
|
||||
})
|
||||
|
||||
export const teamApplicationParamsSchema: ObjectSchema<TeamApplicationDynamicParams> = object({
|
||||
id: string().required('El id es obligatorio')
|
||||
})
|
||||
Reference in New Issue
Block a user