feat: session

This commit is contained in:
2024-01-11 20:57:17 -06:00
parent 4f37fd4734
commit 8d8b5e1646
17 changed files with 659 additions and 26 deletions
BIN
View File
Binary file not shown.
+2 -2
View File
@@ -19,7 +19,7 @@
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@reduxjs/toolkit": "^2.0.1", "@reduxjs/toolkit": "^2.0.1",
"appwrite": "^13.0.1", "appwrite": "^13.0.1",
"entgamers-database": "^0.0.5", "entgamers-database": "^0.0.7",
"entgamers-panda-preset": "0.1.1", "entgamers-panda-preset": "0.1.1",
"formik": "^2.4.5", "formik": "^2.4.5",
"framer-motion": "^10.17.6", "framer-motion": "^10.17.6",
@@ -54,4 +54,4 @@
"husky": "^8.0.3", "husky": "^8.0.3",
"typescript": "*" "typescript": "*"
} }
} }
+36
View File
@@ -0,0 +1,36 @@
'use client'
import { useAppDispatch } from '@/hooks/useAppDispatch'
import { useAppSelector } from '@/hooks/useAppSelector'
import { setSession, setStatus } from '@/state/sessionSlice'
import { AppwriteException } from 'appwrite'
import { getSession } from 'entgamers-database/frontend/session'
import { useEffect, type FC } from 'react'
const SessionConsumer: FC = () => {
const session = useAppSelector((state) => state.session)
const dispatch = useAppDispatch()
useEffect(() => {
if (session.status === 'initializing' && session.session === undefined) {
dispatch(setStatus('loading'))
getSession('current')
.then((session) => {
dispatch(setSession(session))
})
.catch((error) => {
if (error instanceof AppwriteException) {
console.error(error)
}
})
.finally(() => {
dispatch(setStatus('idle'))
})
}
}, [])
return (
<>
</>
)
}
export default SessionConsumer
+129
View File
@@ -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,
isInitialValid: false
})
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
+160
View File
@@ -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,
isInitialValid: false
})
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
+105
View File
@@ -0,0 +1,105 @@
'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 { 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 } = 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: '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 nombre',
severity: 'error'
}))
} else {
dispatch(addAlert({
id: nanoid(),
message: 'Error desconocido',
title: 'Error mientras se actualizaba el nombre',
severity: 'error'
}))
}
}
},
validationSchema: UpdateUserNameSchema,
isInitialValid: false
})
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
+18
View File
@@ -0,0 +1,18 @@
import UpdateEmail from '@/app/cuenta/UpdateEmail'
import UpdatePassword from '@/app/cuenta/UpdatePassword'
import UpdateUserName from '@/app/cuenta/UpdateUserName'
import Typography from '@/components/ui/Typography'
import { Container } from '@/styled-system/jsx'
import { type FC } from 'react'
const CuentaPage: FC = () => {
return (
<Container>
<Typography variant="h1" align="center">Cuenta</Typography>
<UpdateUserName />
<UpdatePassword />
<UpdateEmail />
</Container>
)
}
export default CuentaPage
+2
View File
@@ -11,6 +11,7 @@ import '@fortawesome/fontawesome-svg-core/styles.css'
import { type Metadata } from 'next' import { type Metadata } from 'next'
import { type FC, type ReactNode } from 'react' import { type FC, type ReactNode } from 'react'
import FeedbackConsumer from './FeedbackConsumer' import FeedbackConsumer from './FeedbackConsumer'
import SessionConsumer from './SessionConsumer'
import StateProvider from './StateProvider' import StateProvider from './StateProvider'
config.autoAddCss = false config.autoAddCss = false
@@ -40,6 +41,7 @@ const RootLayout: FC<RootLayoutProps> = ({ children }) => {
</main> </main>
<Footer /> <Footer />
<FeedbackConsumer /> <FeedbackConsumer />
<SessionConsumer />
</StateProvider> </StateProvider>
</body> </body>
</html> </html>
+44 -3
View File
@@ -4,8 +4,16 @@ import Typography from '@/components/ui/Typography'
import FormGroup from '@/components/ui/form/FormGroup' import FormGroup from '@/components/ui/form/FormGroup'
import Input from '@/components/ui/form/Input' import Input from '@/components/ui/form/Input'
import PasswordInput from '@/components/ui/form/PasswordInput' import PasswordInput from '@/components/ui/form/PasswordInput'
import { useAppDispatch } from '@/hooks/useAppDispatch'
import { useAppSelector } from '@/hooks/useAppSelector'
import { addAlert } from '@/state/feedbackSlice'
import { setSession, setStatus } from '@/state/sessionSlice'
import { nanoid } from '@reduxjs/toolkit'
import { AppwriteException } from 'appwrite'
import { login } from 'entgamers-database/frontend/session'
import { useFormik } from 'formik' import { useFormik } from 'formik'
import { type FC } from 'react' import { useRouter } from 'next/navigation'
import { useEffect, type FC } from 'react'
import { object, string } from 'yup' import { object, string } from 'yup'
interface LoginData { interface LoginData {
@@ -19,16 +27,49 @@ const loginSchema = object({
}) })
const LoginForm: FC = () => { const LoginForm: FC = () => {
const dispatch = useAppDispatch()
const session = useAppSelector((state) => state.session)
const router = useRouter()
const formik = useFormik<LoginData>({ const formik = useFormik<LoginData>({
initialValues: { initialValues: {
email: '', email: '',
password: '' password: ''
}, },
onSubmit: (values) => { onSubmit: async ({ email, password }) => {
console.log(values) dispatch(setStatus('loading'))
try {
const session = await login(email, password)
dispatch(setSession(session))
} catch (error) {
if (error instanceof AppwriteException) {
dispatch(addAlert({
id: nanoid(),
message: error.message,
title: 'Error mientras se iniciaba sesión',
severity: 'error'
}))
} else {
dispatch(addAlert({
id: nanoid(),
message: 'Error desconocido',
title: 'Error mientras se iniciaba sesión',
severity: 'error'
}))
}
} finally {
dispatch(setStatus('idle'))
}
}, },
validationSchema: loginSchema validationSchema: loginSchema
}) })
useEffect(() => {
if (session.status === 'idle' && session.session !== undefined) {
router.push('/')
}
}, [session])
return ( return (
<form <form
onSubmit={formik.handleSubmit} onSubmit={formik.handleSubmit}
+1 -1
View File
@@ -42,7 +42,7 @@ const LoginPage: FC = () => {
> >
<LoginForm /> <LoginForm />
<Typography variant="caption" align="center" > <Typography variant="caption" align="center" >
No tienes una cuenta? <NextLink href="/register">Regístrate</NextLink> ¿No tienes una cuenta? <NextLink href="/register">Regístrate</NextLink>
</Typography> </Typography>
</div> </div>
</div> </div>
+35 -2
View File
@@ -4,6 +4,11 @@ import Typography from '@/components/ui/Typography'
import FormGroup from '@/components/ui/form/FormGroup' import FormGroup from '@/components/ui/form/FormGroup'
import Input from '@/components/ui/form/Input' import Input from '@/components/ui/form/Input'
import PasswordInput from '@/components/ui/form/PasswordInput' import PasswordInput from '@/components/ui/form/PasswordInput'
import { useAppDispatch } from '@/hooks/useAppDispatch'
import { addAlert } from '@/state/feedbackSlice'
import { nanoid } from '@reduxjs/toolkit'
import { AppwriteException } from 'appwrite'
import { register } from 'entgamers-database/frontend/session'
import { useFormik } from 'formik' import { useFormik } from 'formik'
import { type FC } from 'react' import { type FC } from 'react'
import { object, ref, string } from 'yup' import { object, ref, string } from 'yup'
@@ -26,17 +31,45 @@ const RegisterSchema = object({
}) })
const RegisterForm: FC = () => { const RegisterForm: FC = () => {
const dispatch = useAppDispatch()
const formik = useFormik<RegisterData>({ const formik = useFormik<RegisterData>({
initialValues: { initialValues: {
email: '', email: '',
password: '', password: '',
passwordConfirmation: '' passwordConfirmation: ''
}, },
onSubmit: (values) => { onSubmit: async ({ email, password }) => {
console.log(values) try {
await register(email, password)
dispatch(addAlert({
id: nanoid(),
title: 'Registro completado',
message: 'Ahora puedes iniciar sesión',
severity: 'success'
}))
formik.resetForm()
} catch (error) {
if (error instanceof AppwriteException) {
dispatch(addAlert({
id: nanoid(),
message: error.message,
title: 'Error mientras se registraba',
severity: 'error'
}))
} else {
dispatch(addAlert({
id: nanoid(),
message: 'Error desconocido',
title: 'Error mientras se registraba',
severity: 'error'
}))
}
}
}, },
validationSchema: RegisterSchema validationSchema: RegisterSchema
}) })
return ( return (
<form <form
onSubmit={formik.handleSubmit} onSubmit={formik.handleSubmit}
+1 -1
View File
@@ -43,7 +43,7 @@ const RegisterPage: FC = () => {
> >
<RegisterForm /> <RegisterForm />
<Typography variant="caption" align="center" > <Typography variant="caption" align="center" >
Ya tienes una cuenta? <NextLink href="/login">Inicia sesión</NextLink> ¿Ya tienes una cuenta? <NextLink href="/login">Inicia sesión</NextLink>
</Typography> </Typography>
</div> </div>
</div> </div>
+71 -14
View File
@@ -1,16 +1,27 @@
'use client' 'use client'
import EntGamers from '@/assets/logos/EntGamers' import EntGamers from '@/assets/logos/EntGamers'
import Menu from '@/components/layout/Menu' import Menu from '@/components/layout/Menu'
import IconButton from '@/components/ui/IconButton'
import Tooltip from '@/components/ui/Tooltip' import Tooltip from '@/components/ui/Tooltip'
import { useAppDispatch } from '@/hooks/useAppDispatch'
import { useAppSelector } from '@/hooks/useAppSelector'
import { addAlert } from '@/state/feedbackSlice'
import { setSession } from '@/state/sessionSlice'
import { css } from '@/styled-system/css' import { css } from '@/styled-system/css'
import { Container } from '@/styled-system/jsx' import { Container } from '@/styled-system/jsx'
import { iconButton } from '@/styled-system/recipes' import { iconButton } from '@/styled-system/recipes'
import { faUser } from '@fortawesome/free-solid-svg-icons' import { faRightFromBracket, faUser } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { nanoid } from '@reduxjs/toolkit'
import { AppwriteException } from 'appwrite'
import { logout } from 'entgamers-database/frontend/session'
import NextLink from 'next/link' import NextLink from 'next/link'
import { useCallback, useEffect, useState, type FC } from 'react' import { useCallback, useEffect, useState, type FC } from 'react'
const Header: FC = () => { const Header: FC = () => {
const session = useAppSelector(state => state.session)
const dispatch = useAppDispatch()
const [isScrolled, setIsScrolled] = useState(typeof window !== 'undefined' ? window.scrollY > 0 : false) const [isScrolled, setIsScrolled] = useState(typeof window !== 'undefined' ? window.scrollY > 0 : false)
const handleScroll = useCallback(() => { const handleScroll = useCallback(() => {
if (typeof window === 'undefined') return if (typeof window === 'undefined') return
@@ -68,19 +79,65 @@ const Header: FC = () => {
</NextLink> </NextLink>
</div> </div>
<div> <div>
<Tooltip {session.status === 'idle' && typeof session.session !== 'undefined'
title="Próximamente" ? (
position="bottom" <>
> <Tooltip
<NextLink title="Cuenta"
href="/login" position="bottom"
className={ >
iconButton() <NextLink
} href="/cuenta"
> className={
<FontAwesomeIcon icon={faUser} fixedWidth /> iconButton()
</NextLink> }
</Tooltip> >
<FontAwesomeIcon icon={faUser} fixedWidth />
</NextLink>
</Tooltip>
<Tooltip
title="Cerrar sesión"
position="bottom"
>
<IconButton
onClick={() => {
logout('current')
.then(() => {
dispatch(setSession(undefined))
})
.catch((error) => {
if (error instanceof AppwriteException) {
dispatch(addAlert({
id: nanoid(),
message: error.message,
title: 'Error mientras se cerraba sesión',
severity: 'error'
}))
}
})
}}
>
<FontAwesomeIcon icon={faRightFromBracket} fixedWidth />
</IconButton>
</Tooltip>
</>
)
: (
<Tooltip
title="Iniciar sesión"
position="bottom"
>
<NextLink
href="/login"
className={
iconButton()
}
>
<FontAwesomeIcon icon={faUser} fixedWidth />
</NextLink>
</Tooltip>
)
}
<Menu /> <Menu />
</div> </div>
</Container> </Container>
+21
View File
@@ -0,0 +1,21 @@
import { type SessionState } from '@/state/sessionSlice'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
import { useAppSelector } from './useAppSelector'
type UseSession = (redirect?: string) => SessionState
const useSession: UseSession = (redirect?: string) => {
const { status, session } = useAppSelector((state) => state.session)
const router = useRouter()
useEffect(() => {
if (status === 'idle' && session === undefined) {
router.push(redirect ?? '/')
}
}, [status, session])
return { status, session }
}
export default useSession
+29
View File
@@ -0,0 +1,29 @@
import { createSlice, type PayloadAction } from '@reduxjs/toolkit'
import { type Models } from 'appwrite'
export interface SessionState {
status: 'idle' | 'loading' | 'initializing'
session?: Models.Session
}
const initialState: SessionState = {
status: 'initializing',
session: undefined
}
const sessionSlice = createSlice({
name: 'session',
initialState,
reducers: {
setStatus: (state, action: PayloadAction<SessionState['status']>) => {
state.status = action.payload
},
setSession: (state, action: PayloadAction<SessionState['session']>) => {
state.session = action.payload
}
}
})
export const { setStatus, setSession } = sessionSlice.actions
export default sessionSlice
+3 -1
View File
@@ -1,9 +1,11 @@
import feedbackSlice from '@/state/feedbackSlice' import feedbackSlice from '@/state/feedbackSlice'
import sessionSlice from '@/state/sessionSlice'
import { configureStore } from '@reduxjs/toolkit' import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({ const store = configureStore({
reducer: { reducer: {
feedback: feedbackSlice.reducer feedback: feedbackSlice.reducer,
session: sessionSlice.reducer
} }
}) })
+2 -2
View File
@@ -12,8 +12,8 @@
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"noEmit": true, "noEmit": true,
"esModuleInterop": true, "esModuleInterop": true,
"module": "esnext", "module": "Node16",
"moduleResolution": "node", "moduleResolution": "Node16",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "preserve",