add scorm

This commit is contained in:
2025-11-13 11:25:18 -03:00
parent b5e0684de7
commit 16bcf4b07f
14 changed files with 104 additions and 59 deletions

View File

@@ -53,7 +53,7 @@ export async function loader({ context, request, params }: Route.LoaderArgs) {
}).then((r) => r.json())
return {
data: Promise.all([users, new Promise((r) => setTimeout(r, 5000))])
data: users
}
}
@@ -69,7 +69,7 @@ export default function Route({ loaderData: { data } }) {
<Suspense fallback={<Skeleton />}>
<Await resolve={data}>
{([{ items }, _]) => {
{({ items }) => {
return (
<div className="grid gap-4 lg:gap-8 md:grid-cols-2 lg:grid-cols-3">
{items.map(({ sk, name, email }: Admin) => {

View File

@@ -6,6 +6,12 @@ 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 {
@@ -34,7 +40,6 @@ import {
FormMessage
} from '@repo/ui/components/ui/form'
import { Input } from '@repo/ui/components/ui/input'
import { Label } from '@repo/ui/components/ui/label'
import { Spinner } from '@repo/ui/components/ui/spinner'
import { useWorksapce } from '@/components/workspace-switcher'
@@ -49,10 +54,11 @@ export const formSchema = z.object({
.trim()
.nonempty('Digite um nome')
.refine(isName, { message: 'Nome inválido' }),
email: z.email('Email inválido').trim().toLowerCase(),
email: z.email('Email inválido').trim().toLowerCase().optional(),
cpf: z
.string('CPF obrigatório')
.refine(isValidCPF, { message: 'CPF inválido' })
.refine(isValidCPF, { message: 'CPF inválido' }),
given_email: z.coerce.boolean()
})
export type Schema = z.infer<typeof formSchema>
@@ -87,7 +93,8 @@ export default function Route() {
const form = useForm({
resolver: zodResolver(formSchema)
})
const { handleSubmit, control, formState, reset } = form
const { handleSubmit, control, formState, reset, watch } = form
const givenEmail = watch('given_email') as boolean
const onSubmit = async (user: Schema) => {
await fetcher.submit(JSON.stringify({ user, org: activeWorkspace }), {
@@ -108,6 +115,8 @@ export default function Route() {
}
}, [fetcher.data])
// console.log(randomEmail())
return (
<div className="space-y-2.5">
<Breadcrumb>
@@ -157,6 +166,7 @@ export default function Route() {
<FormField
control={control}
name="email"
disabled={givenEmail}
defaultValue=""
render={({ field }) => (
<FormItem>
@@ -168,12 +178,27 @@ export default function Route() {
</FormItem>
)}
/>
<div className="flex items-center gap-2">
<Checkbox id="terms" tabIndex={-1} />
<Label htmlFor="terms">
<FormField
control={control}
name="given_email"
defaultValue={false}
render={({ field: { value, onChange, ...field } }) => (
<FormItem className="flex items-center gap-2">
<FormControl>
<Checkbox
checked={Boolean(value)}
onCheckedChange={onChange}
tabIndex={-1}
{...field}
/>
</FormControl>
<FormLabel className="cursor-pointer">
Usar um email fornecido pela plataforma.
</Label>
</div>
</FormLabel>
</FormItem>
)}
/>
</div>
<FormField
@@ -219,3 +244,14 @@ export default function Route() {
</div>
)
}
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`
}

View File

@@ -28,6 +28,7 @@
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router": "^7.9.5",
"unique-names-generator": "^4.7.1",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"zod": "^4.1.12"
},

View File

@@ -0,0 +1,14 @@
import { cn } from '@repo/ui/lib/utils'
type ContainerProps = {
children: React.ReactNode
className?: string
}
export function Container({ children, className }: ContainerProps) {
return (
<main>
<div className={cn('container mx-auto', className)}>{children}</div>
</main>
)
}

View File

@@ -1,20 +0,0 @@
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function initials(s: string): string {
const initials = s
.split(' ')
.map((word) => word.charAt(0).toUpperCase()) as string[]
if (initials.length == 0) {
return ''
}
const first = initials[0]
const last = initials[initials.length - 1]
return first + last
}

View File

@@ -17,7 +17,7 @@ export const middleware: Route.MiddlewareFunction[] = [loggingMiddleware]
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="pt-br" suppressHydrationWarning>
<html lang="pt-br" className="h-full" suppressHydrationWarning>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -26,7 +26,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
<Meta />
<Links />
</head>
<body>
<body className="h-full">
<ThemeProvider
attribute="class"
defaultTheme="system"

View File

@@ -11,13 +11,15 @@ import {
BreadcrumbSeparator
} from '@repo/ui/components/ui/breadcrumb'
import { Container } from '@/components/container'
export function meta({}: Route.MetaArgs) {
return [{ title: 'Certificados' }]
}
export default function Component() {
return (
<div className="space-y-2.5">
<Container className="space-y-2.5">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
@@ -39,6 +41,6 @@ export default function Component() {
acompanhe seus cursos concluídos.
</p>
</div>
</div>
</Container>
)
}

View File

@@ -27,6 +27,7 @@ import { Await, useSearchParams } from 'react-router'
import placeholder from '@/assets/placeholder.webp'
import { createSearch } from '@/lib/meili'
import { Container } from '@/components/container'
import type { User } from '@repo/auth/auth'
import { userContext } from '@repo/auth/context'
import { FacetedFilter } from '@repo/ui/components/faceted-filter'
@@ -102,7 +103,7 @@ export default function Component({
const term = searchParams.get('term') as string
return (
<div className="space-y-4">
<Container className="space-y-4">
<div className="space-y-0.5 mb-8">
<h1 className="text-2xl font-bold tracking-tight">Meus cursos</h1>
<p className="text-muted-foreground">
@@ -164,7 +165,7 @@ export default function Component({
</Await>
</div>
</Suspense>
</div>
</Container>
)
}

View File

@@ -50,7 +50,7 @@ export default function Component({ loaderData }: Route.ComponentProps) {
const [isOpen, { toggle }] = useToggle()
return (
<div className="relative flex flex-col flex-1 min-w-0">
<div className="relative flex flex-col flex-1 min-w-0 h-full">
<header
className="bg-background/15 backdrop-blur-sm
px-4 py-2 lg:py-4 sticky top-0 z-5"
@@ -118,11 +118,7 @@ export default function Component({ loaderData }: Route.ComponentProps) {
</div>
</header>
<main className="p-4">
<div className="container mx-auto">
<Outlet />
</div>
</main>
</div>
)
}

View File

@@ -11,13 +11,15 @@ import {
BreadcrumbSeparator
} from '@repo/ui/components/ui/breadcrumb'
import { Container } from '@/components/container'
export function meta({}: Route.MetaArgs) {
return [{ title: 'Histórico de compras' }]
}
export default function Component() {
return (
<div className="space-y-2.5">
<Container className="space-y-2.5">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
@@ -41,6 +43,6 @@ export default function Component() {
o controle financeiro.
</p>
</div>
</div>
</Container>
)
}

View File

@@ -4,16 +4,18 @@ import { ScormPlayer } from '@/components/scorm-player'
import { useLocalStorage } from '@/hooks/useLocalStorage'
import SHA256 from 'crypto-js/sha256'
import { data } from './index'
export function meta({ params }: Route.MetaArgs) {
const course = data.find((course) => course.id === params.course)
return [{ title: course.courseName }]
return [{ title: '' }]
}
export default function Home({ params }: Route.ComponentProps) {
const course = data.find((course) => course.id === params.course)
export default function Route({ params }: Route.ComponentProps) {
const course = {
id: 'fbad867a-0022-4605-814f-db8ebe2b17fb',
courseName: 'All Golf',
scormContentPath:
'nr-33-espacos-confinados-conteudo-de-demonstracao-scorm12/scormdriver/indexAPI.html'
}
// const course = data.find((course) => course.id === params.course)
const hash = SHA256(course.scormContentPath).toString()
const [scormState] = useLocalStorage(`scormState.${hash}`, {})

View File

@@ -4,6 +4,7 @@ import { Link } from 'react-router'
import { request as req } from '@/lib/request'
import { Container } from '@/components/container'
import type { User } from '@repo/auth/auth'
import { userContext } from '@repo/auth/context'
import {
@@ -48,7 +49,7 @@ export async function loader({ context, request }: Route.ActionArgs) {
return { user: await r.json() }
}
export default function Component({ loaderData: { user } }) {
export default function Route({ loaderData: { user } }) {
const form = useForm({ defaultValues: user })
const { handleSubmit, control, formState } = form
@@ -57,7 +58,7 @@ export default function Component({ loaderData: { user } }) {
}
return (
<div className="space-y-2.5">
<Container className="space-y-2.5">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
@@ -144,7 +145,7 @@ export default function Component({ loaderData: { user } }) {
<div className="flex justify-end">
<Button
type="submit"
className="bg-lime-400 cursor-pointer"
className="cursor-pointer"
disabled={formState.isSubmitting}
>
{formState.isSubmitting && <Spinner />}
@@ -154,6 +155,6 @@ export default function Component({ loaderData: { user } }) {
</form>
</Form>
</div>
</div>
</Container>
)
}

10
package-lock.json generated
View File

@@ -41,6 +41,7 @@
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router": "^7.9.5",
"unique-names-generator": "^4.7.1",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"zod": "^4.1.12"
},
@@ -7247,6 +7248,15 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/unique-names-generator": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/unique-names-generator/-/unique-names-generator-4.7.1.tgz",
"integrity": "sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",

View File

@@ -17,7 +17,7 @@ Globals:
Architectures:
- x86_64
Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:79
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:100
Environment:
Variables:
TZ: America/Sao_Paulo
@@ -110,7 +110,7 @@ Resources:
InvocationType: RequestResponse
- S3Action:
BucketName: !Ref BucketName
ObjectKeyPrefix: "mailbox"
ObjectKeyPrefix: 'mailbox'
ScanEnabled: true
EventAddTenantFunction: