diff --git a/apps/saladeaula.digital/app/routes/api.ts b/apps/saladeaula.digital/app/routes/api.ts index b2c71cb..8607dbe 100644 --- a/apps/saladeaula.digital/app/routes/api.ts +++ b/apps/saladeaula.digital/app/routes/api.ts @@ -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' diff --git a/apps/saladeaula.digital/app/routes/certs.tsx b/apps/saladeaula.digital/app/routes/certs.tsx index 40727ff..b80c91d 100644 --- a/apps/saladeaula.digital/app/routes/certs.tsx +++ b/apps/saladeaula.digital/app/routes/certs.tsx @@ -1,4 +1,4 @@ -import type { Route } from './+types' +import type { Route } from './+types/certs' import { Link } from 'react-router' diff --git a/apps/saladeaula.digital/app/routes/konviva.ts b/apps/saladeaula.digital/app/routes/konviva.ts index 572403b..d885b4c 100644 --- a/apps/saladeaula.digital/app/routes/konviva.ts +++ b/apps/saladeaula.digital/app/routes/konviva.ts @@ -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' diff --git a/apps/saladeaula.digital/app/routes/layout.tsx b/apps/saladeaula.digital/app/routes/layout.tsx index 3d282b8..c3b6ea8 100644 --- a/apps/saladeaula.digital/app/routes/layout.tsx +++ b/apps/saladeaula.digital/app/routes/layout.tsx @@ -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' diff --git a/apps/saladeaula.digital/app/routes/orders.tsx b/apps/saladeaula.digital/app/routes/orders.tsx index 4bf36df..aa813ab 100644 --- a/apps/saladeaula.digital/app/routes/orders.tsx +++ b/apps/saladeaula.digital/app/routes/orders.tsx @@ -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' diff --git a/apps/saladeaula.digital/app/routes/player.tsx b/apps/saladeaula.digital/app/routes/player.tsx index 6a1d354..7b55ac5 100644 --- a/apps/saladeaula.digital/app/routes/player.tsx +++ b/apps/saladeaula.digital/app/routes/player.tsx @@ -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' diff --git a/apps/saladeaula.digital/app/routes/proxy.tsx b/apps/saladeaula.digital/app/routes/proxy.tsx index 5852687..80a4850 100644 --- a/apps/saladeaula.digital/app/routes/proxy.tsx +++ b/apps/saladeaula.digital/app/routes/proxy.tsx @@ -1,4 +1,4 @@ -import type { Route } from './+types' +import type { Route } from './+types/proxy' // Mapping of extensions to MIME types const mimeTypes: Record = { diff --git a/apps/saladeaula.digital/app/routes/settings/add-email.tsx b/apps/saladeaula.digital/app/routes/settings/add-email.tsx new file mode 100644 index 0000000..22fc999 --- /dev/null +++ b/apps/saladeaula.digital/app/routes/settings/add-email.tsx @@ -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 + +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 ( +
+ +
+

Adicionar email

+ +
+ ( + + + + + + + )} + /> + + +
+
+
+ + ) +} diff --git a/apps/saladeaula.digital/app/routes/settings/emails.tsx b/apps/saladeaula.digital/app/routes/settings/emails.tsx index 8e8748e..9115afd 100644 --- a/apps/saladeaula.digital/app/routes/settings/emails.tsx +++ b/apps/saladeaula.digital/app/routes/settings/emails.tsx @@ -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 ( - }> - - {({ items = [] }) => ( - - - Emails - - 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. - - - - {items.map(({ sk, ...props }) => { - const [, email] = sk.split('#') as [string, string] +
+ }> + + {({ items = [] }) => ( + + + Emails + + 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. + + + + {items.map(({ sk, email_primary, email_verified }) => { + const [, email] = sk.split('#') as [string, string] - return ( - - - {email} - ... - - - - - - - - ) - })} - - - )} - - + return ( + + + + {email}{' '} + {email_primary && ( + + Principal + + )} + {email_verified ? ( + + Verificado + + ) : ( + + Não verificado + + )} + + + + + + + + + ) + })} + + + + + + + )} + + +
) } function ActionMenu() { + const { revalidate } = useRevalidator() + + const onSuccess = () => { + revalidate() + } + return ( @@ -122,10 +162,8 @@ function ActionMenu() { - - Reenviar email de verificação - - + + ) @@ -135,9 +173,40 @@ type ItemProps = ComponentProps & { 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 ( + + {loading ? : } Reenviar email de verificação + + ) +} + 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 ( diff --git a/apps/saladeaula.digital/app/routes/settings/password.tsx b/apps/saladeaula.digital/app/routes/settings/password.tsx index 00c96a5..8355dcb 100644 --- a/apps/saladeaula.digital/app/routes/settings/password.tsx +++ b/apps/saladeaula.digital/app/routes/settings/password.tsx @@ -1,4 +1,4 @@ -import type { Route } from './+types/emails' +import type { Route } from './+types/password' export default function Route({}: Route.ComponentProps) { return <>