updat users
This commit is contained in:
@@ -173,6 +173,9 @@ def primary(
|
|||||||
now_ = now()
|
now_ = now()
|
||||||
expr = 'SET email_primary = :email_primary, updated_at = :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:
|
with dyn.transact_writer() as transact:
|
||||||
# Set the old email as non-primary
|
# Set the old email as non-primary
|
||||||
transact.update(
|
transact.update(
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export default [
|
|||||||
route('orders', 'routes/orders.tsx'),
|
route('orders', 'routes/orders.tsx'),
|
||||||
route('settings', 'routes/settings/layout.tsx', [
|
route('settings', 'routes/settings/layout.tsx', [
|
||||||
index('routes/settings/profile.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('password', 'routes/settings/password.tsx')
|
||||||
]),
|
]),
|
||||||
route('konviva', 'routes/konviva.ts'),
|
route('konviva', 'routes/konviva.ts'),
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ import { useOutletContext } from 'react-router'
|
|||||||
import type { User } from '@repo/ui/routes/users/data'
|
import type { User } from '@repo/ui/routes/users/data'
|
||||||
import { useRevalidator } from 'react-router'
|
import { useRevalidator } from 'react-router'
|
||||||
|
|
||||||
const formSchema = z.object({
|
export const formSchema = z.object({
|
||||||
email: z.email('Email inválido').trim().toLowerCase()
|
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 { revalidate } = useRevalidator()
|
||||||
const { user } = useOutletContext() as { user: User }
|
const { user } = useOutletContext() as { user: User }
|
||||||
const { runAsync } = useRequest(
|
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 && (
|
{formState.isSubmitting && (
|
||||||
<div className="absolute inset-0 bg-lime-500 flex items-center justify-center">
|
<div className="absolute inset-0 bg-lime-500 flex items-center justify-center">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { isValidCPF } from '@brazilian-utils/brazilian-utils'
|
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(' ')
|
const isName = (name: string) => name && name.includes(' ')
|
||||||
|
|
||||||
export const formSchema = z.object({
|
export const formSchema = z.object({
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Route } from './+types/emails'
|
import type { Route } from './+types/index'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Suspense,
|
Suspense,
|
||||||
@@ -7,8 +7,7 @@ import {
|
|||||||
createContext,
|
createContext,
|
||||||
use
|
use
|
||||||
} from 'react'
|
} from 'react'
|
||||||
|
import { Await, useRevalidator } from 'react-router'
|
||||||
import { Await } from 'react-router'
|
|
||||||
import { EllipsisIcon, CircleXIcon, SendIcon } from 'lucide-react'
|
import { EllipsisIcon, CircleXIcon, SendIcon } from 'lucide-react'
|
||||||
import { useRequest, useToggle } from 'ahooks'
|
import { useRequest, useToggle } from 'ahooks'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
@@ -51,16 +50,11 @@ import {
|
|||||||
ItemContent,
|
ItemContent,
|
||||||
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 { Badge } from '@repo/ui/components/ui/badge'
|
||||||
import { useRevalidator } from 'react-router'
|
|
||||||
|
|
||||||
type Email = {
|
import { type Email } from './data'
|
||||||
sk: string
|
import { Add } from './add'
|
||||||
email: string
|
import { Primary } from './primary'
|
||||||
email_verified: boolean
|
|
||||||
email_primary: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const ActionMenuContext = createContext<Email | null>(null)
|
const ActionMenuContext = createContext<Email | null>(null)
|
||||||
|
|
||||||
@@ -81,13 +75,14 @@ export default function Route({ loaderData: { data } }: Route.ComponentProps) {
|
|||||||
<Suspense fallback={<Skeleton />}>
|
<Suspense fallback={<Skeleton />}>
|
||||||
<Await resolve={data}>
|
<Await resolve={data}>
|
||||||
{({ items = [] }) => (
|
{({ items = [] }) => (
|
||||||
|
<>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-lg">Emails</CardTitle>
|
<CardTitle className="text-lg">Emails</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Você pode associar vários emails a sua conta. É possível usar
|
Você pode associar vários emails a sua conta. É possível
|
||||||
qualquer email para recuperar a senha, mas apenas o email
|
usar qualquer email para recuperar a senha, mas apenas o
|
||||||
principal receberá as mensagens.
|
email principal receberá as mensagens.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-2.5">
|
<CardContent className="flex flex-col gap-2.5">
|
||||||
@@ -137,9 +132,12 @@ export default function Route({ loaderData: { data } }: Route.ComponentProps) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<AddEmail />
|
<Add />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Primary items={items} />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Await>
|
</Await>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
@@ -174,10 +172,21 @@ type ItemProps = ComponentProps<typeof DropdownMenuItem> & {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ResendItem({ onSuccess }: ItemProps) {
|
function ResendItem({ onSuccess }: ItemProps) {
|
||||||
|
const { user } = useOutletContext() as { user: User }
|
||||||
const { email, email_verified } = use(ActionMenuContext) as Email
|
const { email, email_verified } = use(ActionMenuContext) as Email
|
||||||
const { runAsync, loading } = useRequest(
|
const { runAsync, loading } = useRequest(
|
||||||
async () => {
|
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
|
manual: true
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ import { Input } from '@repo/ui/components/ui/input'
|
|||||||
import { Spinner } from '@repo/ui/components/ui/spinner'
|
import { Spinner } from '@repo/ui/components/ui/spinner'
|
||||||
import { type User } from '@repo/ui/routes/users/data'
|
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'
|
import { useFetcher } from 'react-router'
|
||||||
|
|
||||||
export async function action({ request }: Route.ActionArgs) {
|
export async function action({ request }: Route.ActionArgs) {
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export function NavUser({
|
|||||||
<Avatar className="size-10">
|
<Avatar className="size-10">
|
||||||
<AvatarFallback>{initials(user.name)}</AvatarFallback>
|
<AvatarFallback>{initials(user.name)}</AvatarFallback>
|
||||||
</Avatar>
|
</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>
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Para proteger sua conta na EDUSEG, precisamos apenas verificar seu
|
|||||||
endereço de email: {email}.<br/><br/>
|
endereço de email: {email}.<br/><br/>
|
||||||
|
|
||||||
<a href="https://saladeaula.digital/settings/emails/{code}/verify">
|
<a href="https://saladeaula.digital/settings/emails/{code}/verify">
|
||||||
👉 Verificar endereço de email
|
👉 Clique aqui para verificar endereço de email
|
||||||
</a>
|
</a>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -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/>
|
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}">
|
<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>
|
</a>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user