mirror of
https://github.com/jorgev259/soc_site-astro.git
synced 2025-06-29 07:57:41 +00:00
Login/Logout/Forgor flows
This commit is contained in:
parent
5d04693a09
commit
af4c629047
18 changed files with 298 additions and 129 deletions
|
|
@ -1,8 +1,10 @@
|
|||
import type { PropsWithChildren } from 'react'
|
||||
import type { PropsWithChildren, JSX } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { BarsRotateFade } from 'react-svg-spinners'
|
||||
|
||||
export default function Button(props: PropsWithChildren<{ className?: string; loading?: boolean }>) {
|
||||
export default function Button(
|
||||
props: PropsWithChildren<{ className?: string; loading?: boolean }> & JSX.IntrinsicElements['button']
|
||||
) {
|
||||
const { children, className, loading = false, ...restProps } = props
|
||||
return (
|
||||
<button
|
||||
|
|
|
|||
59
src/components/ForgorForm.tsx
Normal file
59
src/components/ForgorForm.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { useState, type FormEvent } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import * as m from 'paraglide/messages.js'
|
||||
import Button from './Button'
|
||||
import Modal from './Modal'
|
||||
import { resetPassword } from 'utils/auth-client'
|
||||
|
||||
export default function ForgorForm(props: { token: string }) {
|
||||
const { token } = props
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
async function handleSubmit(ev: FormEvent<HTMLFormElement>) {
|
||||
ev.preventDefault()
|
||||
const formData = new FormData(ev.currentTarget)
|
||||
const password = formData.get('password')
|
||||
|
||||
if (!password) return
|
||||
|
||||
setLoading(true)
|
||||
const { data, error } = await resetPassword({ newPassword: password as string, token })
|
||||
setLoading(false)
|
||||
|
||||
if (error) {
|
||||
console.error(error)
|
||||
toast.error(error.message || 'Unknown Error')
|
||||
return
|
||||
}
|
||||
|
||||
toast.success(m.passwordResetSuccesful())
|
||||
window.location.replace('/')
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className='w-[500px]'>
|
||||
<div className='flex flex-col'>
|
||||
<label htmlFor='password' className='font-medium text-black'>
|
||||
{m.newPassword()}:
|
||||
</label>
|
||||
<input type='password' name='password' className='bg-zinc-200 rounded p-2 mt-2 mb-3 text-black' required />
|
||||
</div>
|
||||
<div className='flex flex-col'>
|
||||
<label htmlFor='password2' className='font-medium text-black'>
|
||||
{m.newPasswordRetype()}:
|
||||
</label>
|
||||
<input type='password' name='password2' className='bg-zinc-200 rounded p-2 mt-2 mb-3 text-black' required />
|
||||
</div>
|
||||
<div className='mx-auto'>
|
||||
<Button type='submit' loading={loading} disabled={loading}>
|
||||
{m.savePassword()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ const { value: bannerPosition } = (await prismaClient.config.findUnique({ where:
|
|||
height={150}
|
||||
/>
|
||||
</div>
|
||||
<div class='relative px-20 size-full'>
|
||||
<div class='relative px-20 size-full flex'>
|
||||
<a href='/'>
|
||||
<Image
|
||||
src={Astro.currentLocale === 'es' ? logoEs : logo}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { useEffect, type KeyboardEvent, type PropsWithChildren } from 'react'
|
||||
import type { SetState } from 'types'
|
||||
|
||||
export default function Modal(props: PropsWithChildren<{ setOpen: SetState<boolean> }>) {
|
||||
export default function Modal(props: PropsWithChildren<{ setOpen?: SetState<boolean> }>) {
|
||||
const { children, setOpen } = props
|
||||
|
||||
useEffect(() => {
|
||||
const handleEsc = (ev: KeyboardEvent) => {
|
||||
if (ev.code === 'Escape') setOpen(false)
|
||||
const handleEsc = (ev: any) => {
|
||||
if ((ev as KeyboardEvent).code === 'Escape' && setOpen) setOpen(false)
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleEsc)
|
||||
|
|
@ -17,11 +17,13 @@ export default function Modal(props: PropsWithChildren<{ setOpen: SetState<boole
|
|||
|
||||
return (
|
||||
<div
|
||||
className='fixed size-full flex bg-black bg-opacity-50 left-0 top-0 z-50 justify-center items-center'
|
||||
onClick={() => setOpen(false)}
|
||||
className='fixed size-full flex bg-black bg-opacity-50 left-0 top-0 z-50 p-4 justify-center items-center'
|
||||
onClick={() => {
|
||||
if (setOpen) setOpen(false)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className='bg-white rounded-lg overflow-hidden shadow-xl transform transition-all m-8'
|
||||
className='bg-white max-w-lg p-4 rounded-lg overflow-hidden shadow-xl transform transition-all m-8'
|
||||
role='dialog'
|
||||
aria-modal='true'
|
||||
onClick={(ev) => ev.stopPropagation()}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import NavButton from './NavButton.astro'
|
|||
---
|
||||
|
||||
<nav-dropdown>
|
||||
<NavButton class='dropdown-btn group relative'>
|
||||
<NavButton class='dropdown-btn group relative px-0'>
|
||||
<div class='after:content-["▼"] after:text-xs'><slot /></div>
|
||||
<div
|
||||
class='dropdown-items md:absolute flex-col bg-gray hidden md:group-hover:flex top-full left-0 py-1 min-w-36 rounded-md md:rounded-t-none mt-1.5 md:mt-0'
|
||||
|
|
|
|||
|
|
@ -1,35 +1,17 @@
|
|||
import { useState, type FormEvent, type SyntheticEvent } from 'react'
|
||||
|
||||
import Button from 'components/Button'
|
||||
import * as m from 'paraglide/messages.js'
|
||||
import Modal from 'components/Modal'
|
||||
import apolloClient from '@/graphql/apolloClient'
|
||||
import { useEffect, useState, type FormEvent } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
const loginMutation = gql(`
|
||||
mutation Login($username: String!, $password: String!) {
|
||||
login(username: $username, password: $password)
|
||||
}
|
||||
`)
|
||||
import * as m from 'paraglide/messages.js'
|
||||
import Button from 'components/Button'
|
||||
import Modal from 'components/Modal'
|
||||
import { forgetPassword, signIn } from 'utils/auth-client'
|
||||
import type { SetState } from 'types'
|
||||
|
||||
type FormOptions = 'login' | 'forgor'
|
||||
|
||||
export default function LoginBtn() {
|
||||
const [currentForm, setForm] = useState<FormOptions>('login')
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleSubmit = (ev: FormEvent) => {
|
||||
ev.preventDefault()
|
||||
const formData = new FormData(ev.target)
|
||||
const variables = Object.fromEntries(formData)
|
||||
|
||||
mutate({ variables })
|
||||
.then((res) => {
|
||||
// toast.success(m.emailSuccess())
|
||||
setModalOpen(false)
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -38,32 +20,123 @@ export default function LoginBtn() {
|
|||
</Button>
|
||||
{modalOpen ? (
|
||||
<Modal setOpen={setModalOpen}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className='bg-white px-4 pt-5 pb-4 gap-x-4 flex flex-col'>
|
||||
<div className='flex gap-x-4'>
|
||||
<div className='flex flex-col'>
|
||||
<label htmlFor='username' className='font-medium text-black'>
|
||||
{m.username()}:
|
||||
</label>
|
||||
<input type='text' name='username' className='bg-zinc-200 rounded p-2 mt-2 mb-3 text-black' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col'>
|
||||
<label htmlFor='password' className='font-medium text-black'>
|
||||
{m.password()}:
|
||||
</label>
|
||||
<input type='password' name='password' className='bg-zinc-200 rounded p-2 mt-2 mb-3 text-black' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='bg-zinc-200 px-4 py-3 text-right gap-x-2 flex justify-end'>
|
||||
<Button className='bg-zinc-500 hover:bg-zinc-600'>{m.close()}</Button>
|
||||
<Button loading={loading} disabled={loading}>
|
||||
{m.login()}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
{currentForm === 'login' ? <LoginForm setForm={setForm} setModalOpen={setModalOpen} /> : null}
|
||||
{currentForm === 'forgor' ? <CreateForgorForm setForm={setForm} setModalOpen={setModalOpen} /> : null}
|
||||
</Modal>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function LoginForm(props: { setForm: SetState<FormOptions>; setModalOpen: SetState<boolean> }) {
|
||||
const { setForm, setModalOpen } = props
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
async function handleSubmit(ev: FormEvent<HTMLFormElement>) {
|
||||
ev.preventDefault()
|
||||
const formData = new FormData(ev.currentTarget)
|
||||
const variables = Object.fromEntries(formData)
|
||||
|
||||
setLoading(true)
|
||||
const { error } = await signIn.username({
|
||||
username: variables.username as string,
|
||||
password: variables.password as string
|
||||
})
|
||||
setLoading(false)
|
||||
|
||||
if (error) {
|
||||
console.error(error)
|
||||
toast.error(error.message || 'Unknown Error')
|
||||
return
|
||||
}
|
||||
|
||||
setModalOpen(false)
|
||||
location.reload()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form method='post' onSubmit={handleSubmit}>
|
||||
<div className='flex gap-x-4'>
|
||||
<div className='flex flex-col'>
|
||||
<label htmlFor='username' className='font-medium text-black'>
|
||||
{m.username()}:
|
||||
</label>
|
||||
<input type='text' name='username' className='bg-zinc-200 rounded p-2 mt-2 mb-3 text-black' required />
|
||||
</div>
|
||||
<div className='flex flex-col'>
|
||||
<label htmlFor='password' className='font-medium text-black'>
|
||||
{m.password()}:
|
||||
</label>
|
||||
<input type='password' name='password' className='bg-zinc-200 rounded p-2 mt-2 mb-3 text-black' required />
|
||||
</div>
|
||||
</div>
|
||||
<div className='mx-auto'>
|
||||
<Button loading={loading} disabled={loading}>
|
||||
{m.login()}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className='mx-auto'>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setForm('forgor')
|
||||
}}
|
||||
>
|
||||
{m.recoverPassword()}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function CreateForgorForm(props: { setForm: SetState<FormOptions>; setModalOpen: SetState<boolean> }) {
|
||||
const { setForm, setModalOpen } = props
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setForm('login')
|
||||
}
|
||||
}, [])
|
||||
|
||||
async function handleSubmit(ev: FormEvent<HTMLFormElement>) {
|
||||
ev.preventDefault()
|
||||
const formData = new FormData(ev.currentTarget)
|
||||
const email = formData.get('email')
|
||||
|
||||
if (!email) return
|
||||
|
||||
setLoading(true)
|
||||
const { error } = await forgetPassword({ email: email as string, redirectTo: '/forgor' })
|
||||
setLoading(false)
|
||||
|
||||
if (error) {
|
||||
console.error(error)
|
||||
toast.error(error.message || 'Unknown Error')
|
||||
return
|
||||
}
|
||||
|
||||
toast.success(m.emailSuccess())
|
||||
setModalOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className='w-[500px]'>
|
||||
<div className='flex flex-col'>
|
||||
<label htmlFor='email' className='font-medium text-black'>
|
||||
{m.email()}:
|
||||
</label>
|
||||
<input type='email' name='email' className='bg-zinc-200 rounded p-2 mt-2 mb-3 text-black' required />
|
||||
</div>
|
||||
<div className='mx-auto'>
|
||||
<Button loading={loading} disabled={loading}>
|
||||
{m.recoverPassword()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import LogoutBtn from './LogoutButton'
|
|||
const session = Astro.locals.session
|
||||
---
|
||||
|
||||
<div class='absolute top-0 right-0 space-x-2 mr-10'>
|
||||
<div class='ms-auto'>
|
||||
{
|
||||
session ? (
|
||||
<LogoutBtn client:only='react' />
|
||||
|
|
|
|||
|
|
@ -1,23 +1,10 @@
|
|||
// import toast from 'react-hot-toast'
|
||||
|
||||
import Button from 'components/Button'
|
||||
import * as m from 'paraglide/messages.js'
|
||||
import Button from 'components/Button'
|
||||
import { signOut } from 'utils/auth-client'
|
||||
|
||||
export default function LogoutBtn() {
|
||||
const handleClick = (ev) => {
|
||||
ev.preventDefault()
|
||||
|
||||
/* mutate()
|
||||
.then(() => {
|
||||
window.refresh()
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message)
|
||||
}) */
|
||||
}
|
||||
|
||||
return (
|
||||
<Button className='rounded-t-none' onClick={handleClick}>
|
||||
<Button className='rounded-t-none' onClick={() => signOut()}>
|
||||
{m.logout()}
|
||||
</Button>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -25,10 +25,10 @@ import 'styles/global.css'
|
|||
</head>
|
||||
<body class='flex flex-col min-h-full m-0 color-'>
|
||||
<Header />
|
||||
<main class='flex-1'>
|
||||
<main class='flex-1 bg-soc-green'>
|
||||
<Toaster client:only='react' toastOptions={{ duration: 6000 }} />
|
||||
<slot />
|
||||
</main>
|
||||
<footer>This is the footer</footer>
|
||||
<footer class='bg-soc-green'>This is the footer</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
9
src/pages/forgor/index.astro
Normal file
9
src/pages/forgor/index.astro
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
import ForgorForm from 'components/ForgorForm'
|
||||
import BaseLayout from 'layouts/base.astro'
|
||||
const token = Astro.url.searchParams.get('token')
|
||||
|
||||
if (!token) return Astro.redirect('/404')
|
||||
---
|
||||
|
||||
<BaseLayout><ForgorForm token={token} client:only='react' /></BaseLayout>
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
---
|
||||
import { languageTag } from '../paraglide/runtime'
|
||||
|
||||
import BaseLayout from 'layouts/base.astro'
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -4,4 +4,4 @@ import { usernameClient } from 'better-auth/client/plugins'
|
|||
export const authClient = createAuthClient({
|
||||
plugins: [usernameClient()]
|
||||
})
|
||||
export const { useSession, signIn, signUp, forgetPassword, resetPassword } = authClient
|
||||
export const { useSession, signIn, signUp, signOut, forgetPassword, resetPassword } = authClient
|
||||
|
|
|
|||
3
src/utils/types.ts
Normal file
3
src/utils/types.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import type { Dispatch, SetStateAction } from 'react'
|
||||
|
||||
export type SetState<T> = Dispatch<SetStateAction<T>>
|
||||
Loading…
Add table
Add a link
Reference in a new issue