This commit is contained in:
2025-11-26 15:14:29 -03:00
parent 0d3d9ac7d3
commit d3ccfb4775
32 changed files with 496 additions and 474 deletions

View File

@@ -1,11 +1,7 @@
import type { Route } from './+types'
import { useToggle } from 'ahooks'
import {
EllipsisVerticalIcon,
PencilIcon,
UserRoundMinusIcon
} from 'lucide-react'
import { EllipsisIcon, PencilIcon, UserRoundMinusIcon } from 'lucide-react'
import { Suspense } from 'react'
import { Await, NavLink, useParams, useRevalidator } from 'react-router'
import { toast } from 'sonner'
@@ -123,7 +119,7 @@ function ActionMenu({ id }: { id: string }) {
className="data-[state=open]:bg-muted text-muted-foreground cursor-pointer absolute z-1 right-4 top-4"
size="icon-sm"
>
<EllipsisVerticalIcon />
<EllipsisIcon />
<span className="sr-only">Abrir menu</span>
</Button>
</DropdownMenuTrigger>
@@ -185,12 +181,12 @@ function RevokeItem({ id }: { id: string }) {
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="*:cursor-pointer">
<AlertDialogCancel>Cancelar</AlertDialogCancel>
<AlertDialogAction asChild>
<Button onClick={revoke} disabled={loading} variant="destructive">
{loading ? <Spinner /> : null} Continuar
</Button>
</AlertDialogAction>
<AlertDialogCancel>Cancelar</AlertDialogCancel>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>

View File

@@ -5,7 +5,7 @@ import type { ComponentProps, MouseEvent } from 'react'
import { useRequest, useToggle } from 'ahooks'
import {
CircleXIcon,
EllipsisVerticalIcon,
EllipsisIcon,
FileBadgeIcon,
LockOpenIcon
} from 'lucide-react'
@@ -99,7 +99,7 @@ function ActionMenu({ row }: { row: any }) {
className="data-[state=open]:bg-muted text-muted-foreground cursor-pointer"
size="icon-sm"
>
<EllipsisVerticalIcon />
<EllipsisIcon />
<span className="sr-only">Abrir menu</span>
</Button>
</DropdownMenuTrigger>
@@ -238,12 +238,12 @@ function RemoveDedupItem({
)}
</AlertDialogHeader>
<AlertDialogFooter className="*:cursor-pointer">
<AlertDialogCancel>Cancelar</AlertDialogCancel>
<AlertDialogAction asChild>
<Button onClick={cancel} disabled={loading} variant="destructive">
{loading ? <Spinner /> : null} Continuar
</Button>
</AlertDialogAction>
<AlertDialogCancel>Cancelar</AlertDialogCancel>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
@@ -305,12 +305,12 @@ function CancelItem({
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="*:cursor-pointer">
<AlertDialogCancel>Cancelar</AlertDialogCancel>
<AlertDialogAction asChild>
<Button onClick={cancel} disabled={loading} variant="destructive">
{loading ? <Spinner /> : null} Continuar
</Button>
</AlertDialogAction>
<AlertDialogCancel>Cancelar</AlertDialogCancel>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>

View File

@@ -7,7 +7,6 @@ import {
DataTableColumnSelect
} from '@repo/ui/components/data-table'
import { columns as columns_, type Order } from '@repo/ui/routes/orders/columns'
import { Abbr } from '@repo/ui/components/abbr'
export type { Order }

View File

@@ -3,6 +3,7 @@ import type { Route } from './+types/route'
import { zodResolver } from '@hookform/resolvers/zod'
import { PatternFormat } from 'react-number-format'
import { Link, useOutletContext } from 'react-router'
import { useForm } from 'react-hook-form'
import { Button } from '@repo/ui/components/ui/button'
import {
@@ -22,15 +23,14 @@ import {
} from '@repo/ui/components/ui/form'
import { Input } from '@repo/ui/components/ui/input'
import { Spinner } from '@repo/ui/components/ui/spinner'
import { useForm } from 'react-hook-form'
import { type User } from '@repo/ui/routes/users/data'
import type { User } from '../_.$orgid.users.$id/route'
import { formSchema, type Schema } from '../_.$orgid.users.add/route'
import { formSchema, type Schema } from '../_.$orgid.users.add/data'
export default function Route({}: Route.ComponentProps) {
const { user } = useOutletContext() as { user: User }
const form = useForm({
defaultValues: user,
defaultValues: { ...user, given_email: false },
resolver: zodResolver(formSchema)
})
const { handleSubmit, control, formState } = form

View File

@@ -40,8 +40,8 @@ export async function loader({ params, request, context }: Route.LoaderArgs) {
throw new Response(null, { status: r.status })
}
const user: User = await r.json()
return { user }
const data = await r.json()
return { data } as { data: any }
}
export function shouldRevalidate({
@@ -51,18 +51,14 @@ export function shouldRevalidate({
return currentParams.id !== nextParams.id
}
export type User = {
name: string
email: string
cpf: string
}
const links = [
{ to: '', title: 'Perfil', end: true },
{ to: 'emails', title: 'Emails' }
]
export default function Route({ loaderData: { user } }: Route.ComponentProps) {
export default function Route({
loaderData: { data: user }
}: Route.ComponentProps) {
return (
<div className="space-y-2.5">
<Breadcrumb>

View File

@@ -2,11 +2,7 @@
import { type ColumnDef } from '@tanstack/react-table'
import { useToggle } from 'ahooks'
import {
EllipsisVerticalIcon,
PencilIcon,
UserRoundMinusIcon
} from 'lucide-react'
import { EllipsisIcon, PencilIcon, UserRoundMinusIcon } from 'lucide-react'
import { NavLink, useParams } from 'react-router'
import { toast } from 'sonner'
@@ -63,7 +59,7 @@ function ActionMenu({ row }: { row: any }) {
className="data-[state=open]:bg-muted text-muted-foreground cursor-pointer"
size="icon-sm"
>
<EllipsisVerticalIcon />
<EllipsisIcon />
<span className="sr-only">Abrir menu</span>
</Button>
</DropdownMenuTrigger>
@@ -127,12 +123,12 @@ function UnlinkItem({ id }: { id: string }) {
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="*:cursor-pointer">
<AlertDialogCancel>Cancelar</AlertDialogCancel>
<AlertDialogAction asChild>
<Button onClick={unlink} disabled={loading} variant="destructive">
{loading ? <Spinner /> : null} Continuar
</Button>
</AlertDialogAction>
<AlertDialogCancel>Cancelar</AlertDialogCancel>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>

View File

@@ -0,0 +1,55 @@
import { isValidCPF } from '@brazilian-utils/brazilian-utils'
import {
adjectives,
colors,
NumberDictionary,
uniqueNamesGenerator
} from 'unique-names-generator'
import { z } from 'zod'
const isName = (name: string) => name && name.includes(' ')
function randomEmail() {
const numberDict = NumberDictionary.generate({ min: 100, max: 999 })
const randomName: string = uniqueNamesGenerator({
dictionaries: [adjectives, colors, numberDict],
length: 3,
separator: '-'
})
return `${randomName}@users.noreply.saladeaula.digital`
}
export const formSchema = z
.object({
name: z
.string()
.trim()
.nonempty('Digite um nome')
.refine(isName, { message: 'Nome inválido' }),
email: z.string().trim().toLowerCase().optional(),
cpf: z
.string('CPF obrigatório')
.refine(isValidCPF, { message: 'CPF inválido' }),
given_email: z.coerce.boolean()
})
.refine(
({ given_email, email }) => {
if (given_email) {
return true
}
return email && z.email().safeParse(email).success
},
{
message: 'Email inválido',
path: ['email']
}
)
.transform((data) => {
if (data.given_email) {
return { ...data, email: randomEmail() }
}
return data
})
export type Schema = z.infer<typeof formSchema>

View File

@@ -1,18 +1,11 @@
import type { Route } from './+types/route'
import { isValidCPF } from '@brazilian-utils/brazilian-utils'
import { useEffect } from 'react'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { PatternFormat } from 'react-number-format'
import { Link, useFetcher } from 'react-router'
import { toast } from 'sonner'
import {
adjectives,
colors,
NumberDictionary,
uniqueNamesGenerator
} from 'unique-names-generator'
import { z } from 'zod'
import {
Breadcrumb,
@@ -44,54 +37,7 @@ import { Spinner } from '@repo/ui/components/ui/spinner'
import { useWorksapce } from '@/components/workspace-switcher'
import { HttpMethod, request as req } from '@repo/util/request'
import { useEffect } from 'react'
const isName = (name: string) => name && name.includes(' ')
function randomEmail() {
const numberDict = NumberDictionary.generate({ min: 100, max: 999 })
const randomName: string = uniqueNamesGenerator({
dictionaries: [adjectives, colors, numberDict],
length: 3,
separator: '-'
})
return `${randomName}@users.noreply.saladeaula.digital`
}
export const formSchema = z
.object({
name: z
.string()
.trim()
.nonempty('Digite um nome')
.refine(isName, { message: 'Nome inválido' }),
email: z.string().trim().toLowerCase().optional(),
cpf: z
.string('CPF obrigatório')
.refine(isValidCPF, { message: 'CPF inválido' }),
given_email: z.coerce.boolean()
})
.refine(
({ given_email, email }) => {
if (given_email) {
return true
}
return email && z.email().safeParse(email).success
},
{
message: 'Email inválido',
path: ['email']
}
)
.transform((data) => {
if (data.given_email) {
return { ...data, email: randomEmail() }
}
return data
})
export type Schema = z.infer<typeof formSchema>
import { formSchema, type Schema } from './data'
export function meta({}: Route.MetaArgs) {
return [{ title: 'Adicionar colaborador' }]

View File

@@ -2,10 +2,6 @@ import type { Route } from './+types'
import { parse } from 'cookie'
export const OK = 200
export const FOUND = 302
export const INTERNAL_SERVER_ERROR = 500
export async function loader({ request, context }: Route.LoaderArgs) {
const cookies = parse(request.headers.get('Cookie') || '')
const url = new URL(request.url)
@@ -16,7 +12,7 @@ export async function loader({ request, context }: Route.LoaderArgs) {
if (!cookies?.__session) {
return new Response(null, {
status: FOUND,
status: 302,
headers: {
Location: loginUrl.toString()
}
@@ -33,7 +29,7 @@ export async function loader({ request, context }: Route.LoaderArgs) {
redirect: 'manual'
})
if (r.status === FOUND) {
if (r.status === 302) {
return new Response(await r.text(), {
status: r.status,
headers: r.headers
@@ -47,7 +43,7 @@ export async function loader({ request, context }: Route.LoaderArgs) {
})
// Deny authorization if user lacks scopes requested by client
if (r.status === FOUND) {
if (r.status === 302) {
return new Response(null, {
status: r.status,
headers: {
@@ -57,13 +53,13 @@ export async function loader({ request, context }: Route.LoaderArgs) {
}
return new Response(null, {
status: FOUND,
status: 302,
headers: {
Location: loginUrl.toString()
}
})
} catch (error) {
console.error(error)
return new Response(null, { status: INTERNAL_SERVER_ERROR })
return new Response(null, { status: 500 })
}
}

View File

@@ -79,7 +79,7 @@ export async function action({ request, context }: Route.ActionArgs) {
})
} catch (error) {
console.error(error)
return Response.json({}, { status: INTERNAL_SERVER_ERROR })
return Response.json({}, { status: 500 })
}
}

View File

@@ -20,8 +20,8 @@ export default function Layout() {
aria-hidden="true"
className="absolute inset-0 grid grid-cols-2 opacity-20"
>
<div className="blur-[106px] h-56 bg-gradient-to-br to-lime-400 from-lime-700"></div>
<div className="blur-[106px] h-42 bg-gradient-to-r from-lime-400 to-lime-600"></div>
<div className="blur-[106px] h-56 bg-linear-to-br to-lime-400 from-lime-700"></div>
<div className="blur-[106px] h-42 bg-linear-to-r from-lime-400 to-lime-600"></div>
</div>
</div>
</div>

View File

@@ -2,7 +2,7 @@
import type { ColumnDef } from '@tanstack/react-table'
import { useToggle } from 'ahooks'
import { EllipsisVerticalIcon, FileBadgeIcon } from 'lucide-react'
import { EllipsisIcon, FileBadgeIcon } from 'lucide-react'
import type { ComponentProps } from 'react'
import { Button } from '@repo/ui/components/ui/button'
@@ -57,7 +57,7 @@ function ActionMenu({ row }: { row: any }) {
className="data-[state=open]:bg-muted text-muted-foreground cursor-pointer"
size="icon-sm"
>
<EllipsisVerticalIcon />
<EllipsisIcon />
<span className="sr-only">Abrir menu</span>
</Button>
</DropdownMenuTrigger>

View File

@@ -1,7 +1,7 @@
'use client'
import { type ColumnDef } from '@tanstack/react-table'
import { EllipsisVerticalIcon } from 'lucide-react'
import { EllipsisIcon } from 'lucide-react'
import { Button } from '@repo/ui/components/ui/button'
import {
@@ -81,7 +81,7 @@ function ActionMenu({ row }: { row: any }) {
className="data-[state=open]:bg-muted text-muted-foreground cursor-pointer"
size="icon-sm"
>
<EllipsisVerticalIcon />
<EllipsisIcon />
<span className="sr-only">Abrir menu</span>
</Button>
</DropdownMenuTrigger>

View File

@@ -1,7 +1,7 @@
'use client'
import { type ColumnDef } from '@tanstack/react-table'
import { EllipsisVerticalIcon } from 'lucide-react'
import { EllipsisIcon } from 'lucide-react'
import { Abbr } from '@repo/ui/components/abbr'
import { Button } from '@repo/ui/components/ui/button'
@@ -78,7 +78,7 @@ function ActionMenu({ row }: { row: any }) {
className="data-[state=open]:bg-muted text-muted-foreground cursor-pointer"
size="icon-sm"
>
<EllipsisVerticalIcon />
<EllipsisIcon />
<span className="sr-only">Abrir menu</span>
</Button>
</DropdownMenuTrigger>

View File

@@ -2,7 +2,7 @@
import { useBoolean } from 'ahooks'
import { type ColumnDef } from '@tanstack/react-table'
import { CheckIcon, CopyIcon, EllipsisVerticalIcon } from 'lucide-react'
import { CheckIcon, CopyIcon, EllipsisIcon } from 'lucide-react'
import {
DataTableColumnSelect,
@@ -46,7 +46,7 @@ function ActionMenu({ row }: { row: any }) {
className="data-[state=open]:bg-muted text-muted-foreground cursor-pointer"
size="icon-sm"
>
<EllipsisVerticalIcon />
<EllipsisIcon />
<span className="sr-only">Abrir menu</span>
</Button>
</DropdownMenuTrigger>

View File

@@ -10,7 +10,11 @@ export default [
index('routes/index.tsx'),
route('certs', 'routes/certs.tsx'),
route('orders', 'routes/orders.tsx'),
route('settings', 'routes/settings.tsx'),
route('settings', 'routes/settings/layout.tsx', [
index('routes/settings/profile.tsx'),
route('emails', 'routes/settings/emails.tsx'),
route('password', 'routes/settings/password.tsx')
]),
route('konviva', 'routes/konviva.ts'),
route('player/:id', 'routes/player.tsx'),
route('proxy/*', 'routes/proxy.tsx')

View File

@@ -1,160 +0,0 @@
import type { Route } from './+types'
import { useForm } from 'react-hook-form'
import { Link } from 'react-router'
import { Container } from '@/components/container'
import type { User } from '@repo/auth/auth'
import { userContext } from '@repo/auth/context'
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from '@repo/ui/components/ui/breadcrumb'
import { Button } from '@repo/ui/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from '@repo/ui/components/ui/card'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@repo/ui/components/ui/form'
import { Input } from '@repo/ui/components/ui/input'
import { Spinner } from '@repo/ui/components/ui/spinner'
import { request as req } from '@repo/util/request'
export function meta({}: Route.MetaArgs) {
return [{ title: 'Minha conta' }]
}
export async function loader({ context, request }: Route.ActionArgs) {
const user = context.get(userContext) as User
const r = await req({
url: `/users/${user.sub}`,
request,
context
})
return { user: await r.json() }
}
export default function Route({ loaderData: { user } }) {
const form = useForm({ defaultValues: user })
const { handleSubmit, control, formState } = form
const onSubmit = async (data) => {
console.log(data)
}
return (
<Container className="space-y-2.5">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to="..">Meus cursos</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Minha conta</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<div className="lg:max-w-2xl mx-auto space-y-2.5">
<Form {...form}>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="text-2xl">Minha conta</CardTitle>
<CardDescription>
Gerenciar as configurações da sua conta.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<FormField
control={control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Nome</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="email"
disabled={true}
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormLabel className="text-sm font-normal text-muted-foreground">
<span>
Para gerenciar os emails ou trocar o email principal,
use as{' '}
<Link
to="emails"
className="text-blue-400 underline hover:no-underline"
>
configurações de emails
</Link>
</span>
</FormLabel>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="cpf"
render={({ field }) => (
<FormItem>
<FormLabel>CPF</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
</Card>
<div className="flex justify-end">
<Button
type="submit"
className="cursor-pointer"
disabled={formState.isSubmitting}
>
{formState.isSubmitting && <Spinner />}
Editar
</Button>
</div>
</form>
</Form>
</div>
</Container>
)
}

View File

@@ -0,0 +1,18 @@
import { z } from 'zod'
import { isValidCPF } from '@brazilian-utils/brazilian-utils'
const isName = (name: string) => name && name.includes(' ')
export const formSchema = z.object({
name: z
.string()
.trim()
.nonempty('Digite um nome')
.refine(isName, { message: 'Nome inválido' }),
email: z.string().trim().toLowerCase(),
cpf: z
.string('CPF obrigatório')
.refine(isValidCPF, { message: 'CPF inválido' })
})
export type Schema = z.infer<typeof formSchema>

View File

@@ -0,0 +1,56 @@
import type { Route } from './+types/emails'
import { Suspense } from 'react'
import { Await } from 'react-router'
import { userContext } from '@repo/auth/context'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from '@repo/ui/components/ui/card'
import type { User } from '@repo/auth/auth'
import { request as req } from '@repo/util/request'
import { Skeleton } from '@repo/ui/components/skeleton'
export async function loader({ request, context }: Route.LoaderArgs) {
const user = context.get(userContext) as User
const data = req({
url: `/users/${user.sub}/emails`,
request,
context
}).then((r) => r.json())
return { data }
}
export default function Route({ loaderData: { data } }: Route.ComponentProps) {
return (
<Suspense fallback={<Skeleton />}>
<Await resolve={data}>
{({ items = [] }) => (
<Card>
<CardHeader>
<CardTitle className="text-lg">Emails</CardTitle>
<CardDescription>
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.
</CardDescription>
</CardHeader>
<CardContent>
<ul>
{items.map(({ sk }: { sk: string }, idx: number) => {
const [, email] = sk.split('#')
return <li key={idx}>{email}</li>
})}
</ul>
</CardContent>
</Card>
)}
</Await>
</Suspense>
)
}

View File

@@ -0,0 +1,100 @@
import type { Route } from './+types/layout'
import {
NavLink,
Outlet,
Link,
type ShouldRevalidateFunctionArgs
} from 'react-router'
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from '@repo/ui/components/ui/breadcrumb'
import { Tabs, TabsList, TabsTrigger } from '@repo/ui/components/ui/tabs'
import { request as req } from '@repo/util/request'
import { Container } from '@/components/container'
import { userContext } from '@repo/auth/context'
import type { User } from '@repo/auth/auth'
export function meta({}: Route.MetaArgs) {
return [{ title: 'Minha conta' }]
}
export async function loader({ request, context }: Route.LoaderArgs) {
const user = context.get(userContext) as User
const r = await req({
url: `/users/${user.sub}`,
request,
context
})
if (!r.ok) {
throw new Response(null, { status: r.status })
}
const data = await r.json()
return { data }
}
export function shouldRevalidate({}: ShouldRevalidateFunctionArgs) {
return false
}
const links = [
{ to: '', title: 'Perfil', end: true },
{ to: 'emails', title: 'Emails' },
{ to: 'password', title: 'Senha' }
]
export default function Layout({ loaderData: { data } }: Route.ComponentProps) {
return (
<Container className="space-y-2.5">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to="..">Meus cursos</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Minha conta</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<div className="lg:max-w-2xl mx-auto space-y-2.5">
<Tabs>
<TabsList>
{links.map(({ to, title, ...props }, idx) => (
<NavLink
to={to}
key={idx}
className="aria-[current=page]:pointer-events-none"
{...props}
>
{({ isActive }) => (
<TabsTrigger
data-state={isActive ? 'active' : ''}
value={title}
asChild
>
<span>{title}</span>
</TabsTrigger>
)}
</NavLink>
))}
</TabsList>
</Tabs>
<Outlet context={{ user: data }} />
</div>
</Container>
)
}

View File

@@ -0,0 +1,5 @@
import type { Route } from './+types/emails'
export default function Route({}: Route.ComponentProps) {
return <></>
}

View File

@@ -0,0 +1,145 @@
import type { Route } from './+types/profile'
import { useForm } from 'react-hook-form'
import { Link, useOutletContext } from 'react-router'
import { PatternFormat } from 'react-number-format'
import { zodResolver } from '@hookform/resolvers/zod'
import { Button } from '@repo/ui/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from '@repo/ui/components/ui/card'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@repo/ui/components/ui/form'
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 { useFetcher } from 'react-router'
export async function action({ request }: Route.ActionArgs) {
const body = await request.json()
console.log(body)
return { ok: true }
}
export default function Route({}: Route.ComponentProps) {
const { user } = useOutletContext() as { user: User }
const fetcher = useFetcher()
const form = useForm({
defaultValues: user,
resolver: zodResolver(formSchema)
})
const { handleSubmit, control, formState } = form
const onSubmit = async (data: Schema) => {
await fetcher.submit(JSON.stringify({ id: user.id, ...data }), {
method: 'post',
encType: 'application/json'
})
}
return (
<Form {...form}>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="text-2xl">Minha conta</CardTitle>
<CardDescription>
Gerenciar as configurações da sua conta.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<FormField
control={control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Nome</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="email"
disabled={true}
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormLabel className="text-sm font-normal text-muted-foreground">
<span>
Para gerenciar os emails ou trocar o email principal, use
as{' '}
<Link
to="emails"
className="text-blue-400 underline hover:no-underline"
>
configurações de emails
</Link>
</span>
</FormLabel>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="cpf"
render={({ field: { onChange, ref, ...props } }) => (
<FormItem>
<FormLabel>CPF</FormLabel>
<FormControl>
<PatternFormat
format="###.###.###-##"
mask="_"
placeholder="___.___.___-__"
customInput={Input}
getInputRef={ref}
onValueChange={({ value }) => {
onChange(value)
}}
{...props}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
</Card>
<div className="flex justify-end">
<Button
type="submit"
className="cursor-pointer"
disabled={formState.isSubmitting}
>
{formState.isSubmitting && <Spinner />}
Editar
</Button>
</div>
</form>
</Form>
)
}

View File

@@ -253,8 +253,8 @@ function Editing() {
<FormItem
className="flex flex-row items-center justify-between
rounded-lg border p-3 shadow-sm
dark:has-[[aria-checked=true]]:border-blue-900
dark:has-[[aria-checked=true]]:bg-blue-950"
dark:has-aria-checked:border-blue-900
dark:has-aria-checked:bg-blue-950"
>
<div className="space-y-1">
<FormLabel>Habilitar certificação</FormLabel>
@@ -383,7 +383,7 @@ function Editing() {
{...field}
/>
</FormControl>
<FormLabel>Ocultar o curso no catálogo.</FormLabel>
<FormLabel>Ocultar o curso no catálogo</FormLabel>
</FormItem>
)}
/>

View File

@@ -1,7 +1,13 @@
import type { Route } from './+types/index'
import Fuse from 'fuse.js'
import { AwardIcon, BanIcon, FileBadgeIcon, LaptopIcon } from 'lucide-react'
import {
AwardIcon,
BanIcon,
FileBadgeIcon,
HatGlassesIcon,
LaptopIcon
} from 'lucide-react'
import { Suspense, useMemo } from 'react'
import { Await, NavLink, useSearchParams } from 'react-router'
@@ -31,6 +37,7 @@ import {
import type { Course } from './edit'
import placeholder from '@/assets/placeholder.webp'
import { cn } from '@repo/ui/lib/utils'
export function meta({}: Route.MetaArgs) {
return [{ title: 'Gerenciar seus cursos' }]
@@ -131,7 +138,12 @@ function Course({ id, name, access_period, cert, draft }: Course) {
return (
<NavLink to={`/edit/${id}`} className="hover:scale-105 transition">
{({ isPending }) => (
<Card className="overflow-hidden relative h-96">
<Card
className={cn(
'overflow-hidden relative h-96',
draft && 'border-dashed'
)}
>
{isPending && (
<div className="absolute bottom-0 right-0 p-6 z-1">
<Spinner className="size-6" />
@@ -139,12 +151,7 @@ function Course({ id, name, access_period, cert, draft }: Course) {
)}
<CardHeader className="z-1 relative">
<CardTitle className="text-xl/6">
{name}{' '}
{draft ? (
<span className="text-muted-foreground">(rascunho)</span>
) : null}
</CardTitle>
<CardTitle className="text-xl/6">{name}</CardTitle>
</CardHeader>
<CardFooter className="text-gray-300 text-sm absolute z-1 bottom-6 w-full flex gap-1.5">
@@ -180,6 +187,12 @@ function Course({ id, name, access_period, cert, draft }: Course) {
<FileBadgeIcon className="size-4" />
</li>
)}
{draft && (
<li className="flex items-center">
<HatGlassesIcon className="size-4" />
</li>
)}
</ul>
</CardFooter>