add email
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { Route } from './+types'
|
||||
import type { Route } from './+types/api'
|
||||
|
||||
import type { User } from '@repo/auth/auth'
|
||||
import { userContext } from '@repo/auth/context'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Route } from './+types'
|
||||
import type { Route } from './+types/certs'
|
||||
|
||||
import { Link } from 'react-router'
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Route } from './+types/konviva'
|
||||
|
||||
import { redirect } from 'react-router'
|
||||
import type { Route } from './+types'
|
||||
|
||||
import { userContext } from '@repo/auth/context'
|
||||
import type { User } from '@repo/auth/auth'
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import type { Route } from './+types/layout'
|
||||
import { useToggle } from 'ahooks'
|
||||
import { MenuIcon } from 'lucide-react'
|
||||
import { Link, NavLink, Outlet } from 'react-router'
|
||||
import { Toaster } from '@repo/ui/components/ui/sonner'
|
||||
|
||||
import { Toaster } from '@repo/ui/components/ui/sonner'
|
||||
import { userContext } from '@repo/auth/context'
|
||||
import { authMiddleware } from '@repo/auth/middleware/auth'
|
||||
import { ModeToggle, ThemedImage } from '@repo/ui/components/dark-mode'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Route } from '../+types'
|
||||
import type { Route } from './+types/orders'
|
||||
|
||||
import { MeiliSearchFilterBuilder } from 'meilisearch-helper'
|
||||
import { Await, Link } from 'react-router'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Route } from './+types'
|
||||
import type { Route } from './+types/player'
|
||||
|
||||
import { HttpMethod, request as req } from '@repo/util/request'
|
||||
import { useFetcher } from 'react-router'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Route } from './+types'
|
||||
import type { Route } from './+types/proxy'
|
||||
|
||||
// Mapping of extensions to MIME types
|
||||
const mimeTypes: Record<string, string> = {
|
||||
|
||||
101
apps/saladeaula.digital/app/routes/settings/add-email.tsx
Normal file
101
apps/saladeaula.digital/app/routes/settings/add-email.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useRequest } from 'ahooks'
|
||||
import { z } from 'zod'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { Button } from '@repo/ui/components/ui/button'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage
|
||||
} from '@repo/ui/components/ui/form'
|
||||
import { Input } from '@repo/ui/components/ui/input'
|
||||
import { Spinner } from '@repo/ui/components/ui/spinner'
|
||||
import { useOutletContext } from 'react-router'
|
||||
import type { User } from '@repo/ui/routes/users/data'
|
||||
import { useRevalidator } from 'react-router'
|
||||
|
||||
const formSchema = z.object({
|
||||
email: z.email('Email inválido').trim().toLowerCase()
|
||||
})
|
||||
|
||||
type Schema = z.infer<typeof formSchema>
|
||||
|
||||
export function AddEmail() {
|
||||
const { revalidate } = useRevalidator()
|
||||
const { user } = useOutletContext() as { user: User }
|
||||
const { runAsync } = useRequest(
|
||||
async (data) => {
|
||||
const r = await fetch(`/api/users/${user.id}/emails`, {
|
||||
method: 'POST',
|
||||
headers: new Headers({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
throw await r.json()
|
||||
}
|
||||
},
|
||||
{ manual: true }
|
||||
)
|
||||
const form = useForm({ resolver: zodResolver(formSchema) })
|
||||
const { handleSubmit, control, formState, reset } = form
|
||||
|
||||
const onSubmit = async ({ email }: Schema) => {
|
||||
try {
|
||||
await runAsync({ email })
|
||||
|
||||
reset()
|
||||
revalidate()
|
||||
toast.success(
|
||||
`Enviamos um email de verificação para ${email}. Por favor, siga as instruções que constam nele.`
|
||||
)
|
||||
} catch (err) {
|
||||
// @ts-ignore
|
||||
if (err?.type === 'EmailConflictError') {
|
||||
toast.error(`O endereço de email ${email} já está em uso.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<fieldset
|
||||
disabled={formState.isSubmitting}
|
||||
className="space-y-2.5 lg:w-2/3"
|
||||
>
|
||||
<h2 className="font-semibold">Adicionar email</h2>
|
||||
|
||||
<div className="flex gap-2.5">
|
||||
<FormField
|
||||
control={control}
|
||||
name="email"
|
||||
defaultValue=""
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormControl>
|
||||
<Input placeholder="seu@email.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit" className="relative overflow-hidden">
|
||||
{formState.isSubmitting && (
|
||||
<div className="absolute inset-0 bg-lime-500 flex items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
Adicionar
|
||||
</Button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
@@ -49,9 +49,11 @@ import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemTitle
|
||||
} from '@repo/ui/components/ui/item'
|
||||
import { AddEmail } from './add-email'
|
||||
import { Badge } from '@repo/ui/components/ui/badge'
|
||||
import { useRevalidator } from 'react-router'
|
||||
|
||||
type Email = {
|
||||
sk: string
|
||||
@@ -75,45 +77,83 @@ export async function loader({ request, context }: Route.LoaderArgs) {
|
||||
|
||||
export default function Route({ loaderData: { data } }: Route.ComponentProps) {
|
||||
return (
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
<Await resolve={data}>
|
||||
{({ items = [] }) => (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Emails</CardTitle>
|
||||
<CardDescription>
|
||||
Podem ser associados vários emails a sua conta. É possível usar
|
||||
qualquer email para recuperar a senha, mas apenas o email
|
||||
principal receberá as mensagens.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-2.5">
|
||||
{items.map(({ sk, ...props }) => {
|
||||
const [, email] = sk.split('#') as [string, string]
|
||||
<div className="space-y-2.5">
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
<Await resolve={data}>
|
||||
{({ items = [] }) => (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Emails</CardTitle>
|
||||
<CardDescription>
|
||||
Você pode associar vários emails a sua conta. É possível usar
|
||||
qualquer email para recuperar a senha, mas apenas o email
|
||||
principal receberá as mensagens.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-2.5">
|
||||
{items.map(({ sk, email_primary, email_verified }) => {
|
||||
const [, email] = sk.split('#') as [string, string]
|
||||
|
||||
return (
|
||||
<Item key={email} variant="outline">
|
||||
<ItemContent>
|
||||
<ItemTitle>{email}</ItemTitle>
|
||||
<ItemDescription>...</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<ActionMenuContext value={{ ...props, sk, email }}>
|
||||
<ActionMenu />
|
||||
</ActionMenuContext>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
)
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</Await>
|
||||
</Suspense>
|
||||
return (
|
||||
<Item key={email} variant="outline">
|
||||
<ItemContent>
|
||||
<ItemTitle>
|
||||
{email}{' '}
|
||||
{email_primary && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-blue-500 text-blue-400 lowercase text-xs"
|
||||
>
|
||||
Principal
|
||||
</Badge>
|
||||
)}
|
||||
{email_verified ? (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-green-600 text-green-500 lowercase text-xs"
|
||||
>
|
||||
Verificado
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-yellow-600 text-yellow-500 lowercase text-xs"
|
||||
>
|
||||
Não verificado
|
||||
</Badge>
|
||||
)}
|
||||
</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<ActionMenuContext
|
||||
value={{ sk, email, email_primary, email_verified }}
|
||||
>
|
||||
<ActionMenu />
|
||||
</ActionMenuContext>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
)
|
||||
})}
|
||||
</CardContent>
|
||||
|
||||
<CardContent>
|
||||
<AddEmail />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</Await>
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ActionMenu() {
|
||||
const { revalidate } = useRevalidator()
|
||||
|
||||
const onSuccess = () => {
|
||||
revalidate()
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -122,10 +162,8 @@ function ActionMenu() {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="*:cursor-pointer">
|
||||
<DropdownMenuItem>
|
||||
<SendIcon /> Reenviar email de verificação
|
||||
</DropdownMenuItem>
|
||||
<RemoveItem />
|
||||
<ResendItem onSuccess={onSuccess} />
|
||||
<RemoveItem onSuccess={onSuccess} />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
@@ -135,9 +173,40 @@ type ItemProps = ComponentProps<typeof DropdownMenuItem> & {
|
||||
onSuccess?: () => void
|
||||
}
|
||||
|
||||
function ResendItem({ onSuccess }: ItemProps) {
|
||||
const { email, email_verified } = use(ActionMenuContext) as Email
|
||||
const { runAsync, loading } = useRequest(
|
||||
async () => {
|
||||
return await new Promise((r) => setTimeout(r, 3000))
|
||||
},
|
||||
{
|
||||
manual: true
|
||||
}
|
||||
)
|
||||
|
||||
const resend = async (e: Event) => {
|
||||
e.preventDefault()
|
||||
|
||||
try {
|
||||
await runAsync()
|
||||
|
||||
toast.success(
|
||||
`Enviamos um email de verificação para ${email}. Por favor, siga as instruções contidas nele.`
|
||||
)
|
||||
onSuccess?.()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenuItem onSelect={resend} disabled={email_verified}>
|
||||
{loading ? <Spinner /> : <SendIcon />} Reenviar email de verificação
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
function RemoveItem({ onSuccess, ...props }: ItemProps) {
|
||||
const { user } = useOutletContext() as { user: User }
|
||||
const { email } = use(ActionMenuContext) as Email
|
||||
const { email, email_primary } = use(ActionMenuContext) as Email
|
||||
const [open, { set: setOpen }] = useToggle(false)
|
||||
const { runAsync, loading } = useRequest(
|
||||
async () => {
|
||||
@@ -160,15 +229,20 @@ function RemoveItem({ onSuccess, ...props }: ItemProps) {
|
||||
try {
|
||||
await runAsync()
|
||||
|
||||
toast.success('O email foi removido')
|
||||
toast.info(`O endereço de email ${email} foi removido.`)
|
||||
onSuccess?.()
|
||||
setOpen(false)
|
||||
} catch (err) {
|
||||
// @ts-ignore
|
||||
if (err?.type === 'EmailConflictError') {
|
||||
toast.error('O email não pode ser removido')
|
||||
toast.error(`O endereço de email ${email} não pode ser removido.`)
|
||||
}
|
||||
}
|
||||
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
if (email_primary) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Route } from './+types/emails'
|
||||
import type { Route } from './+types/password'
|
||||
|
||||
export default function Route({}: Route.ComponentProps) {
|
||||
return <></>
|
||||
|
||||
Reference in New Issue
Block a user