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

@@ -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>
)
}