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

@@ -173,6 +173,9 @@ def primary(
now_ = now()
expr = 'SET email_primary = :email_primary, updated_at = :now'
if new_email == old_email:
return JSONResponse(status_code=HTTPStatus.NO_CONTENT)
with dyn.transact_writer() as transact:
# Set the old email as non-primary
transact.update(

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,13 +75,14 @@ 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.
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">
@@ -137,9 +132,12 @@ export default function Route({ loaderData: { data } }: Route.ComponentProps) {
</CardContent>
<CardContent>
<AddEmail />
<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) {

View File

@@ -76,7 +76,7 @@ export function NavUser({
<Avatar className="size-10">
<AvatarFallback>{initials(user.name)}</AvatarFallback>
</Avatar>
<ChevronDown className="size-3.5 absolute -bottom-px -right-px bg-neutral-700 border border-background rounded-full px-px" />
<ChevronDown className="size-3.5 absolute -bottom-px -right-px bg-gray-200 dark:bg-neutral-700 border border-background rounded-full px-px" />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent

View File

@@ -18,7 +18,7 @@ Para proteger sua conta na EDUSEG, precisamos apenas verificar seu
endereço de email: {email}.<br/><br/>
<a href="https://saladeaula.digital/settings/emails/{code}/verify">
👉 Verificar endereço de email
👉 Clique aqui para verificar endereço de email
</a>
"""

View File

@@ -17,7 +17,7 @@ Oi {first_name}, tudo bem?<br/><br/>
Sua conta foi criada na EDUSEG pela empresa <b>{org_name}</b>.<br/><br/>
<a href="https://id.saladeaula.digital/signup?uid={user_id}&code={code}">
👉 Faça agora seu primeiro acesso
👉 Clique aqui para fazer seu primeiro acesso
</a>
"""