add scorm
This commit is contained in:
@@ -53,7 +53,7 @@ export async function loader({ context, request, params }: Route.LoaderArgs) {
|
|||||||
}).then((r) => r.json())
|
}).then((r) => r.json())
|
||||||
|
|
||||||
return {
|
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 />}>
|
<Suspense fallback={<Skeleton />}>
|
||||||
<Await resolve={data}>
|
<Await resolve={data}>
|
||||||
{([{ items }, _]) => {
|
{({ items }) => {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-4 lg:gap-8 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 lg:gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{items.map(({ sk, name, email }: Admin) => {
|
{items.map(({ sk, name, email }: Admin) => {
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ import { useForm } from 'react-hook-form'
|
|||||||
import { PatternFormat } from 'react-number-format'
|
import { PatternFormat } from 'react-number-format'
|
||||||
import { Link, useFetcher } from 'react-router'
|
import { Link, useFetcher } from 'react-router'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
import {
|
||||||
|
adjectives,
|
||||||
|
colors,
|
||||||
|
NumberDictionary,
|
||||||
|
uniqueNamesGenerator
|
||||||
|
} from 'unique-names-generator'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -34,7 +40,6 @@ import {
|
|||||||
FormMessage
|
FormMessage
|
||||||
} from '@repo/ui/components/ui/form'
|
} from '@repo/ui/components/ui/form'
|
||||||
import { Input } from '@repo/ui/components/ui/input'
|
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 { Spinner } from '@repo/ui/components/ui/spinner'
|
||||||
|
|
||||||
import { useWorksapce } from '@/components/workspace-switcher'
|
import { useWorksapce } from '@/components/workspace-switcher'
|
||||||
@@ -49,10 +54,11 @@ export const formSchema = z.object({
|
|||||||
.trim()
|
.trim()
|
||||||
.nonempty('Digite um nome')
|
.nonempty('Digite um nome')
|
||||||
.refine(isName, { message: 'Nome inválido' }),
|
.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
|
cpf: z
|
||||||
.string('CPF obrigatório')
|
.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>
|
export type Schema = z.infer<typeof formSchema>
|
||||||
@@ -87,7 +93,8 @@ export default function Route() {
|
|||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(formSchema)
|
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) => {
|
const onSubmit = async (user: Schema) => {
|
||||||
await fetcher.submit(JSON.stringify({ user, org: activeWorkspace }), {
|
await fetcher.submit(JSON.stringify({ user, org: activeWorkspace }), {
|
||||||
@@ -108,6 +115,8 @@ export default function Route() {
|
|||||||
}
|
}
|
||||||
}, [fetcher.data])
|
}, [fetcher.data])
|
||||||
|
|
||||||
|
// console.log(randomEmail())
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2.5">
|
<div className="space-y-2.5">
|
||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
@@ -157,6 +166,7 @@ export default function Route() {
|
|||||||
<FormField
|
<FormField
|
||||||
control={control}
|
control={control}
|
||||||
name="email"
|
name="email"
|
||||||
|
disabled={givenEmail}
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
@@ -168,12 +178,27 @@ export default function Route() {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Checkbox id="terms" tabIndex={-1} />
|
<FormField
|
||||||
<Label htmlFor="terms">
|
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.
|
Usar um email fornecido pela plataforma.
|
||||||
</Label>
|
</FormLabel>
|
||||||
</div>
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
@@ -219,3 +244,14 @@ export default function Route() {
|
|||||||
</div>
|
</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`
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-router": "^7.9.5",
|
"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",
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
|
|||||||
14
apps/saladeaula.digital/app/components/container.tsx
Normal file
14
apps/saladeaula.digital/app/components/container.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,7 @@ export const middleware: Route.MiddlewareFunction[] = [loggingMiddleware]
|
|||||||
|
|
||||||
export function Layout({ children }: { children: React.ReactNode }) {
|
export function Layout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<html lang="pt-br" suppressHydrationWarning>
|
<html lang="pt-br" className="h-full" suppressHydrationWarning>
|
||||||
<head>
|
<head>
|
||||||
<meta charSet="utf-8" />
|
<meta charSet="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
@@ -26,7 +26,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
<Meta />
|
<Meta />
|
||||||
<Links />
|
<Links />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body className="h-full">
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
attribute="class"
|
attribute="class"
|
||||||
defaultTheme="system"
|
defaultTheme="system"
|
||||||
|
|||||||
@@ -11,13 +11,15 @@ import {
|
|||||||
BreadcrumbSeparator
|
BreadcrumbSeparator
|
||||||
} from '@repo/ui/components/ui/breadcrumb'
|
} from '@repo/ui/components/ui/breadcrumb'
|
||||||
|
|
||||||
|
import { Container } from '@/components/container'
|
||||||
|
|
||||||
export function meta({}: Route.MetaArgs) {
|
export function meta({}: Route.MetaArgs) {
|
||||||
return [{ title: 'Certificados' }]
|
return [{ title: 'Certificados' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Component() {
|
export default function Component() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2.5">
|
<Container className="space-y-2.5">
|
||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
<BreadcrumbList>
|
<BreadcrumbList>
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
@@ -39,6 +41,6 @@ export default function Component() {
|
|||||||
acompanhe seus cursos concluídos.
|
acompanhe seus cursos concluídos.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { Await, useSearchParams } from 'react-router'
|
|||||||
import placeholder from '@/assets/placeholder.webp'
|
import placeholder from '@/assets/placeholder.webp'
|
||||||
import { createSearch } from '@/lib/meili'
|
import { createSearch } from '@/lib/meili'
|
||||||
|
|
||||||
|
import { Container } from '@/components/container'
|
||||||
import type { User } from '@repo/auth/auth'
|
import type { User } from '@repo/auth/auth'
|
||||||
import { userContext } from '@repo/auth/context'
|
import { userContext } from '@repo/auth/context'
|
||||||
import { FacetedFilter } from '@repo/ui/components/faceted-filter'
|
import { FacetedFilter } from '@repo/ui/components/faceted-filter'
|
||||||
@@ -102,7 +103,7 @@ export default function Component({
|
|||||||
const term = searchParams.get('term') as string
|
const term = searchParams.get('term') as string
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<Container className="space-y-4">
|
||||||
<div className="space-y-0.5 mb-8">
|
<div className="space-y-0.5 mb-8">
|
||||||
<h1 className="text-2xl font-bold tracking-tight">Meus cursos</h1>
|
<h1 className="text-2xl font-bold tracking-tight">Meus cursos</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
@@ -164,7 +165,7 @@ export default function Component({
|
|||||||
</Await>
|
</Await>
|
||||||
</div>
|
</div>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export default function Component({ loaderData }: Route.ComponentProps) {
|
|||||||
const [isOpen, { toggle }] = useToggle()
|
const [isOpen, { toggle }] = useToggle()
|
||||||
|
|
||||||
return (
|
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
|
<header
|
||||||
className="bg-background/15 backdrop-blur-sm
|
className="bg-background/15 backdrop-blur-sm
|
||||||
px-4 py-2 lg:py-4 sticky top-0 z-5"
|
px-4 py-2 lg:py-4 sticky top-0 z-5"
|
||||||
@@ -118,11 +118,7 @@ export default function Component({ loaderData }: Route.ComponentProps) {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="p-4">
|
|
||||||
<div className="container mx-auto">
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,13 +11,15 @@ import {
|
|||||||
BreadcrumbSeparator
|
BreadcrumbSeparator
|
||||||
} from '@repo/ui/components/ui/breadcrumb'
|
} from '@repo/ui/components/ui/breadcrumb'
|
||||||
|
|
||||||
|
import { Container } from '@/components/container'
|
||||||
|
|
||||||
export function meta({}: Route.MetaArgs) {
|
export function meta({}: Route.MetaArgs) {
|
||||||
return [{ title: 'Histórico de compras' }]
|
return [{ title: 'Histórico de compras' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Component() {
|
export default function Component() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2.5">
|
<Container className="space-y-2.5">
|
||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
<BreadcrumbList>
|
<BreadcrumbList>
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
@@ -41,6 +43,6 @@ export default function Component() {
|
|||||||
o controle financeiro.
|
o controle financeiro.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,16 +4,18 @@ import { ScormPlayer } from '@/components/scorm-player'
|
|||||||
import { useLocalStorage } from '@/hooks/useLocalStorage'
|
import { useLocalStorage } from '@/hooks/useLocalStorage'
|
||||||
import SHA256 from 'crypto-js/sha256'
|
import SHA256 from 'crypto-js/sha256'
|
||||||
|
|
||||||
import { data } from './index'
|
|
||||||
|
|
||||||
export function meta({ params }: Route.MetaArgs) {
|
export function meta({ params }: Route.MetaArgs) {
|
||||||
const course = data.find((course) => course.id === params.course)
|
return [{ title: '' }]
|
||||||
|
|
||||||
return [{ title: course.courseName }]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Home({ params }: Route.ComponentProps) {
|
export default function Route({ params }: Route.ComponentProps) {
|
||||||
const course = data.find((course) => course.id === params.course)
|
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 hash = SHA256(course.scormContentPath).toString()
|
||||||
const [scormState] = useLocalStorage(`scormState.${hash}`, {})
|
const [scormState] = useLocalStorage(`scormState.${hash}`, {})
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Link } from 'react-router'
|
|||||||
|
|
||||||
import { request as req } from '@/lib/request'
|
import { request as req } from '@/lib/request'
|
||||||
|
|
||||||
|
import { Container } from '@/components/container'
|
||||||
import type { User } from '@repo/auth/auth'
|
import type { User } from '@repo/auth/auth'
|
||||||
import { userContext } from '@repo/auth/context'
|
import { userContext } from '@repo/auth/context'
|
||||||
import {
|
import {
|
||||||
@@ -48,7 +49,7 @@ export async function loader({ context, request }: Route.ActionArgs) {
|
|||||||
return { user: await r.json() }
|
return { user: await r.json() }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Component({ loaderData: { user } }) {
|
export default function Route({ loaderData: { user } }) {
|
||||||
const form = useForm({ defaultValues: user })
|
const form = useForm({ defaultValues: user })
|
||||||
const { handleSubmit, control, formState } = form
|
const { handleSubmit, control, formState } = form
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ export default function Component({ loaderData: { user } }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2.5">
|
<Container className="space-y-2.5">
|
||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
<BreadcrumbList>
|
<BreadcrumbList>
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
@@ -144,7 +145,7 @@ export default function Component({ loaderData: { user } }) {
|
|||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="bg-lime-400 cursor-pointer"
|
className="cursor-pointer"
|
||||||
disabled={formState.isSubmitting}
|
disabled={formState.isSubmitting}
|
||||||
>
|
>
|
||||||
{formState.isSubmitting && <Spinner />}
|
{formState.isSubmitting && <Spinner />}
|
||||||
@@ -154,6 +155,6 @@ export default function Component({ loaderData: { user } }) {
|
|||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -41,6 +41,7 @@
|
|||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-router": "^7.9.5",
|
"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",
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
@@ -7247,6 +7248,15 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/universalify": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Globals:
|
|||||||
Architectures:
|
Architectures:
|
||||||
- x86_64
|
- x86_64
|
||||||
Layers:
|
Layers:
|
||||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:79
|
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:100
|
||||||
Environment:
|
Environment:
|
||||||
Variables:
|
Variables:
|
||||||
TZ: America/Sao_Paulo
|
TZ: America/Sao_Paulo
|
||||||
@@ -110,7 +110,7 @@ Resources:
|
|||||||
InvocationType: RequestResponse
|
InvocationType: RequestResponse
|
||||||
- S3Action:
|
- S3Action:
|
||||||
BucketName: !Ref BucketName
|
BucketName: !Ref BucketName
|
||||||
ObjectKeyPrefix: "mailbox"
|
ObjectKeyPrefix: 'mailbox'
|
||||||
ScanEnabled: true
|
ScanEnabled: true
|
||||||
|
|
||||||
EventAddTenantFunction:
|
EventAddTenantFunction:
|
||||||
|
|||||||
Reference in New Issue
Block a user