updat users

This commit is contained in:
2025-11-28 11:41:06 -03:00
parent c8e323163d
commit 2b0efc654a
10 changed files with 196 additions and 78 deletions

View File

@@ -12,7 +12,7 @@ export default [
route('orders', 'routes/orders.tsx'),
route('settings', 'routes/settings/layout.tsx', [
index('routes/settings/profile.tsx'),
route('emails', 'routes/settings/emails.tsx'),
route('emails', 'routes/settings/emails/index.tsx'),
route('password', 'routes/settings/password.tsx')
]),
route('konviva', 'routes/konviva.ts'),

View File

@@ -18,13 +18,13 @@ import { useOutletContext } from 'react-router'
import type { User } from '@repo/ui/routes/users/data'
import { useRevalidator } from 'react-router'
const formSchema = z.object({
export const formSchema = z.object({
email: z.email('Email inválido').trim().toLowerCase()
})
type Schema = z.infer<typeof formSchema>
export type Schema = z.infer<typeof formSchema>
export function AddEmail() {
export function Add() {
const { revalidate } = useRevalidator()
const { user } = useOutletContext() as { user: User }
const { runAsync } = useRequest(
@@ -85,7 +85,10 @@ export function AddEmail() {
)}
/>
<Button type="submit" className="relative overflow-hidden">
<Button
type="submit"
className="relative overflow-hidden cursor-pointer"
>
{formState.isSubmitting && (
<div className="absolute inset-0 bg-lime-500 flex items-center justify-center">
<Spinner />

View File

@@ -1,6 +1,13 @@
import { z } from 'zod'
import { isValidCPF } from '@brazilian-utils/brazilian-utils'
export type Email = {
sk: string
email: string
email_verified: boolean
email_primary: boolean
}
const isName = (name: string) => name && name.includes(' ')
export const formSchema = z.object({

View File

@@ -1,4 +1,4 @@
import type { Route } from './+types/emails'
import type { Route } from './+types/index'
import {
Suspense,
@@ -7,8 +7,7 @@ import {
createContext,
use
} from 'react'
import { Await } from 'react-router'
import { Await, useRevalidator } from 'react-router'
import { EllipsisIcon, CircleXIcon, SendIcon } from 'lucide-react'
import { useRequest, useToggle } from 'ahooks'
import { toast } from 'sonner'
@@ -51,16 +50,11 @@ import {
ItemContent,
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
email: string
email_verified: boolean
email_primary: boolean
}
import { type Email } from './data'
import { Add } from './add'
import { Primary } from './primary'
const ActionMenuContext = createContext<Email | null>(null)
@@ -81,65 +75,69 @@ export default function Route({ loaderData: { data } }: Route.ComponentProps) {
<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]
<>
<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}{' '}
{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>
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>
<CardContent>
<Add />
</CardContent>
</Card>
<Primary items={items} />
</>
)}
</Await>
</Suspense>
@@ -174,10 +172,21 @@ type ItemProps = ComponentProps<typeof DropdownMenuItem> & {
}
function ResendItem({ onSuccess }: ItemProps) {
const { user } = useOutletContext() as { user: User }
const { email, email_verified } = use(ActionMenuContext) as Email
const { runAsync, loading } = useRequest(
async () => {
return await new Promise((r) => setTimeout(r, 3000))
const r = await fetch(
`/api/users/${user.id}/emails/${email}/request-verification`,
{
method: 'POST',
headers: new Headers({ 'Content-Type': 'application/json' })
}
)
if (!r.ok) {
throw await r.json()
}
},
{
manual: true

View File

@@ -0,0 +1,96 @@
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { useRequest } from 'ahooks'
import { useRevalidator } from 'react-router'
import { toast } from 'sonner'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from '@repo/ui/components/ui/card'
import { Button } from '@repo/ui/components/ui/button'
import { Kbd } from '@repo/ui/components/ui/kbd'
import {
NativeSelect,
NativeSelectOption
} from '@repo/ui/components/ui/native-select'
import { useOutletContext } from 'react-router'
import type { User } from '@repo/ui/routes/users/data'
import type { Email } from './data'
import { type Schema, formSchema } from './add'
export function Primary({ items = [] }: { items: Email[] }) {
const emails = items.map((props) => {
const [, email] = props.sk.split('#') as [string, string]
return { ...props, email }
}) as Email[]
const primary = emails.find((e) => e.email_primary === true)
const { revalidate } = useRevalidator()
const { user } = useOutletContext() as { user: User }
const { runAsync } = useRequest(
async ({ email }) => {
// Doesn't use `user` because the data could be outdated
const selected = emails.find((e) => e.email === email)
const r = await fetch(`/api/users/${user.id}/emails/primary`, {
method: 'PATCH',
headers: new Headers({ 'Content-Type': 'application/json' }),
body: JSON.stringify({
new_email: selected?.email,
old_email: primary?.email,
email_verified: selected?.email_verified
})
})
if (!r.ok) {
throw await r.json()
}
},
{ manual: true }
)
const { handleSubmit, register } = useForm({
resolver: zodResolver(formSchema)
})
const onSubmit = async ({ email }: Schema) => {
try {
await runAsync({ email })
toast.success(`O email principal foi alterado para: ${email}.`)
revalidate()
} catch {}
}
return (
<Card>
<CardHeader>
<CardTitle className="text-lg">Email principal</CardTitle>
<CardDescription>
<Kbd className="font-mono border">
<span className="truncate max-lg:max-w-62">{primary?.email}</span>
</Kbd>{' '}
será usado para mensagens e pode ser usado para redefinições de senha.
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit(onSubmit)} className="flex gap-1.5">
<NativeSelect defaultValue={primary?.email} {...register('email')}>
{emails.map(({ email }) => {
return (
<NativeSelectOption key={email} value={email}>
{email}
</NativeSelectOption>
)
})}
</NativeSelect>
<Button type="submit" className="overflow-hidden cursor-pointer">
Alterar
</Button>
</form>
</CardContent>
</Card>
)
}

View File

@@ -25,7 +25,7 @@ import { Input } from '@repo/ui/components/ui/input'
import { Spinner } from '@repo/ui/components/ui/spinner'
import { type User } from '@repo/ui/routes/users/data'
import { formSchema, type Schema } from './data'
import { formSchema, type Schema } from './emails/data'
import { useFetcher } from 'react-router'
export async function action({ request }: Route.ActionArgs) {