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 type { User } from '@repo/auth/auth'
|
||||||
import { userContext } from '@repo/auth/context'
|
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'
|
import { Link } from 'react-router'
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import type { Route } from './+types/konviva'
|
||||||
|
|
||||||
import { redirect } from 'react-router'
|
import { redirect } from 'react-router'
|
||||||
import type { Route } from './+types'
|
|
||||||
import { userContext } from '@repo/auth/context'
|
import { userContext } from '@repo/auth/context'
|
||||||
import type { User } from '@repo/auth/auth'
|
import type { User } from '@repo/auth/auth'
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import type { Route } from './+types/layout'
|
|||||||
import { useToggle } from 'ahooks'
|
import { useToggle } from 'ahooks'
|
||||||
import { MenuIcon } from 'lucide-react'
|
import { MenuIcon } from 'lucide-react'
|
||||||
import { Link, NavLink, Outlet } from 'react-router'
|
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 { userContext } from '@repo/auth/context'
|
||||||
import { authMiddleware } from '@repo/auth/middleware/auth'
|
import { authMiddleware } from '@repo/auth/middleware/auth'
|
||||||
import { ModeToggle, ThemedImage } from '@repo/ui/components/dark-mode'
|
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 { MeiliSearchFilterBuilder } from 'meilisearch-helper'
|
||||||
import { Await, Link } from 'react-router'
|
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 { HttpMethod, request as req } from '@repo/util/request'
|
||||||
import { useFetcher } from 'react-router'
|
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
|
// Mapping of extensions to MIME types
|
||||||
const mimeTypes: Record<string, string> = {
|
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,
|
Item,
|
||||||
ItemActions,
|
ItemActions,
|
||||||
ItemContent,
|
ItemContent,
|
||||||
ItemDescription,
|
|
||||||
ItemTitle
|
ItemTitle
|
||||||
} from '@repo/ui/components/ui/item'
|
} 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 = {
|
type Email = {
|
||||||
sk: string
|
sk: string
|
||||||
@@ -75,6 +77,7 @@ export async function loader({ request, context }: Route.LoaderArgs) {
|
|||||||
|
|
||||||
export default function Route({ loaderData: { data } }: Route.ComponentProps) {
|
export default function Route({ loaderData: { data } }: Route.ComponentProps) {
|
||||||
return (
|
return (
|
||||||
|
<div className="space-y-2.5">
|
||||||
<Suspense fallback={<Skeleton />}>
|
<Suspense fallback={<Skeleton />}>
|
||||||
<Await resolve={data}>
|
<Await resolve={data}>
|
||||||
{({ items = [] }) => (
|
{({ items = [] }) => (
|
||||||
@@ -82,23 +85,49 @@ export default function Route({ loaderData: { data } }: Route.ComponentProps) {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-lg">Emails</CardTitle>
|
<CardTitle className="text-lg">Emails</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Podem ser associados vários emails a sua conta. É possível usar
|
Você pode associar vários emails a sua conta. É possível usar
|
||||||
qualquer email para recuperar a senha, mas apenas o email
|
qualquer email para recuperar a senha, mas apenas o email
|
||||||
principal receberá as mensagens.
|
principal receberá as mensagens.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-2.5">
|
<CardContent className="flex flex-col gap-2.5">
|
||||||
{items.map(({ sk, ...props }) => {
|
{items.map(({ sk, email_primary, email_verified }) => {
|
||||||
const [, email] = sk.split('#') as [string, string]
|
const [, email] = sk.split('#') as [string, string]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Item key={email} variant="outline">
|
<Item key={email} variant="outline">
|
||||||
<ItemContent>
|
<ItemContent>
|
||||||
<ItemTitle>{email}</ItemTitle>
|
<ItemTitle>
|
||||||
<ItemDescription>...</ItemDescription>
|
{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>
|
</ItemContent>
|
||||||
<ItemActions>
|
<ItemActions>
|
||||||
<ActionMenuContext value={{ ...props, sk, email }}>
|
<ActionMenuContext
|
||||||
|
value={{ sk, email, email_primary, email_verified }}
|
||||||
|
>
|
||||||
<ActionMenu />
|
<ActionMenu />
|
||||||
</ActionMenuContext>
|
</ActionMenuContext>
|
||||||
</ItemActions>
|
</ItemActions>
|
||||||
@@ -106,14 +135,25 @@ export default function Route({ loaderData: { data } }: Route.ComponentProps) {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
<AddEmail />
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</Await>
|
</Await>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ActionMenu() {
|
function ActionMenu() {
|
||||||
|
const { revalidate } = useRevalidator()
|
||||||
|
|
||||||
|
const onSuccess = () => {
|
||||||
|
revalidate()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu modal={false}>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
@@ -122,10 +162,8 @@ function ActionMenu() {
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="*:cursor-pointer">
|
<DropdownMenuContent align="end" className="*:cursor-pointer">
|
||||||
<DropdownMenuItem>
|
<ResendItem onSuccess={onSuccess} />
|
||||||
<SendIcon /> Reenviar email de verificação
|
<RemoveItem onSuccess={onSuccess} />
|
||||||
</DropdownMenuItem>
|
|
||||||
<RemoveItem />
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)
|
)
|
||||||
@@ -135,9 +173,40 @@ type ItemProps = ComponentProps<typeof DropdownMenuItem> & {
|
|||||||
onSuccess?: () => void
|
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) {
|
function RemoveItem({ onSuccess, ...props }: ItemProps) {
|
||||||
const { user } = useOutletContext() as { user: User }
|
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 [open, { set: setOpen }] = useToggle(false)
|
||||||
const { runAsync, loading } = useRequest(
|
const { runAsync, loading } = useRequest(
|
||||||
async () => {
|
async () => {
|
||||||
@@ -160,15 +229,20 @@ function RemoveItem({ onSuccess, ...props }: ItemProps) {
|
|||||||
try {
|
try {
|
||||||
await runAsync()
|
await runAsync()
|
||||||
|
|
||||||
toast.success('O email foi removido')
|
toast.info(`O endereço de email ${email} foi removido.`)
|
||||||
onSuccess?.()
|
onSuccess?.()
|
||||||
setOpen(false)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (err?.type === 'EmailConflictError') {
|
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 (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Route } from './+types/emails'
|
import type { Route } from './+types/password'
|
||||||
|
|
||||||
export default function Route({}: Route.ComponentProps) {
|
export default function Route({}: Route.ComponentProps) {
|
||||||
return <></>
|
return <></>
|
||||||
|
|||||||
Reference in New Issue
Block a user