diff --git a/api.saladeaula.digital/app/routes/courses/__init__.py b/api.saladeaula.digital/app/routes/courses/__init__.py index f30a8c4..bb8d29c 100644 --- a/api.saladeaula.digital/app/routes/courses/__init__.py +++ b/api.saladeaula.digital/app/routes/courses/__init__.py @@ -33,6 +33,14 @@ def get_course(course_id: str): ) +@router.post('//scormset/') +def get_scormset(course_id: str, scormset_id: str): + return dyn.collection.get_item( + KeyPair(course_id, f'SCORMSET#{scormset_id}'), + exc_cls=NotFoundError, + ) + + class Cert(BaseModel): exp_interval: int | None = None s3_uri: str | None = None @@ -47,6 +55,7 @@ class Course(BaseModel): access_period: int cert: Cert draft: bool = False + demo: bool = False rawfile: bytes | None = None diff --git a/api.saladeaula.digital/app/routes/enrollments/__init__.py b/api.saladeaula.digital/app/routes/enrollments/__init__.py index e09b929..5899927 100644 --- a/api.saladeaula.digital/app/routes/enrollments/__init__.py +++ b/api.saladeaula.digital/app/routes/enrollments/__init__.py @@ -1,7 +1,16 @@ +from http import HTTPStatus +from os import rename +from typing import Annotated + from aws_lambda_powertools import Logger from aws_lambda_powertools.event_handler.api_gateway import Router -from layercake.dynamodb import DynamoDBPersistenceLayer, SortKey, TransactKey +from aws_lambda_powertools.event_handler.exceptions import NotFoundError +from aws_lambda_powertools.event_handler.openapi.params import Body +from glom import T, glom +from layercake.dateutils import now +from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey, TransactKey +from api_gateway import JSONResponse from boto3clients import dynamodb_client from config import ENROLLMENT_TABLE @@ -20,5 +29,62 @@ dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) @router.get('/') def get_enrollment(enrollment_id: str): return dyn.collection.get_items( - TransactKey(enrollment_id) + SortKey('0') + SortKey('ORG') + SortKey('LOCK'), + TransactKey(enrollment_id) + + SortKey('0') + + SortKey('ORG') + + SortKey('LOCK') + + SortKey('SCORM_COMMIT#LAST', rename_key='last_commit'), ) + + +@router.get('//scorm') +def get_scorm(enrollment_id: str): + enrollment = dyn.collection.get_items( + TransactKey(enrollment_id) + SortKey('0') + SortKey('SCORM_COMMIT#LAST') + ) + + if not enrollment: + raise NotFoundError('Enrollment not found') + + spec = {'id': 'course.id', 'scormset': 'course.scormset'} + course_id, scormset_id = glom(enrollment, spec).values() + scormset = dyn.collection.get_item( + KeyPair(course_id, f'SCORMSET#{scormset_id}'), + raise_on_error=False, + default={}, + ) + + return enrollment | {'scormset': scormset} + + +@router.post('/') +def post_enrollment( + enrollment_id: str, + cmi: Annotated[dict, Body(embed=True)], +): + now_ = now() + + with dyn.transact_writer() as transact: + transact.condition( + key=KeyPair(enrollment_id, '0'), + cond_expr='attribute_exists(sk)', + exc_cls=NotFoundError, + ) + transact.put( + item={ + 'id': enrollment_id, + 'sk': 'SCORM_COMMIT#LAST', + 'cmi': cmi, + 'created_at': now_, + } + ) + transact.put( + item={ + 'id': f'SCORM_COMMIT#{enrollment_id}', + 'sk': now_.isoformat(), + 'cmi': cmi, + 'created_at': now_, + } + ) + + return JSONResponse(HTTPStatus.NO_CONTENT) diff --git a/api.saladeaula.digital/tests/routes/test_courses.py b/api.saladeaula.digital/tests/routes/test_courses.py index a12f2a8..5c3e33f 100644 --- a/api.saladeaula.digital/tests/routes/test_courses.py +++ b/api.saladeaula.digital/tests/routes/test_courses.py @@ -102,3 +102,19 @@ def test_template( lambda_context, ) assert r['statusCode'] == HTTPStatus.OK + + +def test_get_scormset( + app, + seeds, + http_api_proxy: HttpApiProxy, + lambda_context: LambdaContext, +): + r = app.lambda_handler( + http_api_proxy( + raw_path='/courses/c27d1b4f-575c-4b6b-82a1-9b91ff369e0b/scormset/76c75561-d972-43ef-9818-497d8fc6edbe', + method=HTTPMethod.GET, + ), + lambda_context, + ) + assert r['statusCode'] == HTTPStatus.OK diff --git a/api.saladeaula.digital/tests/routes/test_enrollments.py b/api.saladeaula.digital/tests/routes/test_enrollments.py index d231747..bf2830f 100644 --- a/api.saladeaula.digital/tests/routes/test_enrollments.py +++ b/api.saladeaula.digital/tests/routes/test_enrollments.py @@ -1,6 +1,8 @@ import json from http import HTTPMethod, HTTPStatus +from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair + from ..conftest import HttpApiProxy, LambdaContext @@ -22,3 +24,73 @@ def test_get_enrollment( body = json.loads(r['body']) assert 'user' in body assert 'course' in body + + +def test_get_scormset( + app, + seeds, + http_api_proxy: HttpApiProxy, + lambda_context: LambdaContext, +): + r = app.lambda_handler( + http_api_proxy( + raw_path='/enrollments/9c166c5e-890f-4e77-9855-769c29aaeb2e/scorm', + method=HTTPMethod.GET, + ), + lambda_context, + ) + assert r['statusCode'] == HTTPStatus.OK + + body = json.loads(r['body']) + print(body) + + +def test_post_scormset( + app, + seeds, + dynamodb_persistence_layer: DynamoDBPersistenceLayer, + http_api_proxy: HttpApiProxy, + lambda_context: LambdaContext, +): + scormbody = { + 'suspend_data': '{"v":2,"d":[123,34,112,114,111,103,114,101,115,115,34,58,256,108,263,115,111,110,265,267,34,48,266,256,112,266,49,53,44,34,105,278,276,287,99,281,284,286,275,277,275,290,58,49,125,300,284,49,289,291,285,287,295,256,297,299,302,304,298,125,284,50,313,299,301,34,317,275,293,123,320,51,287,324,320,52,328,278,320,53,332,267,320,54,336,325,315,34,55,340,320,56,345,342,57,348,302,308,306,337,342,49,303,323,333,356,322,256,329,300,365,125],"cpv":"_lnxccXW"}', + 'launch_data': '', + 'comments': '', + 'comments_from_lms': '', + 'core': { + 'student_id': '', + 'student_name': '', + 'lesson_location': '', + 'credit': '', + 'lesson_status': 'incomplete', + 'entry': '', + 'lesson_mode': 'normal', + 'exit': 'suspend', + 'session_time': '00:00:00', + 'score': {'raw': '', 'min': '', 'max': '100'}, + 'total_time': '00:00:00', + }, + 'objectives': {}, + 'student_data': { + 'mastery_score': '', + 'max_time_allowed': '', + 'time_limit_action': '', + }, + 'student_preference': {'audio': '', 'language': '', 'speed': '', 'text': ''}, + 'interactions': {}, + } + + r = app.lambda_handler( + http_api_proxy( + raw_path='/enrollments/578ec87f-94c7-4840-8780-bb4839cc7e64', + method=HTTPMethod.POST, + body=scormbody, + ), + lambda_context, + ) + assert r['statusCode'] == HTTPStatus.NO_CONTENT + + r = dynamodb_persistence_layer.collection.get_item( + KeyPair('578ec87f-94c7-4840-8780-bb4839cc7e64', 'SCORM_COMMIT#LAST') + ) + assert r['cmi']['suspend_data'] == scormbody['suspend_data'] diff --git a/api.saladeaula.digital/tests/seeds.jsonl b/api.saladeaula.digital/tests/seeds.jsonl index f2efdd4..1a45add 100644 --- a/api.saladeaula.digital/tests/seeds.jsonl +++ b/api.saladeaula.digital/tests/seeds.jsonl @@ -9,6 +9,7 @@ // Enrollments {"id": "578ec87f-94c7-4840-8780-bb4839cc7e64", "sk": "0", "course": {"id": "af3258f0-bccf-4781-aec6-d4c618d234a7", "name": "pytest", "access_period": 180}, "user": {"id": "068b4600-cc36-4b55-b832-bb620021705a", "name": "Benjamin Burnley", "email": "burnley@breakingbenjamin.com"}} +{"id": "9c166c5e-890f-4e77-9855-769c29aaeb2e", "sk": "0", "course": {"id": "c27d1b4f-575c-4b6b-82a1-9b91ff369e0b", "name": "pytest", "access_period": 180, "scormset": "76c75561-d972-43ef-9818-497d8fc6edbe"}, "user": {"id": "068b4600-cc36-4b55-b832-bb620021705a", "name": "Layne Staley", "email": "layne@aliceinchains.com"}} // Orgs {"id": "2a8963fc-4694-4fe2-953a-316d1b10f1f5", "sk": "0", "name": "pytest", "cnpj": "04978826000180"} @@ -28,4 +29,7 @@ {"id": "cpf", "sk": "07879819908", "user_id": "15bacf02-1535-4bee-9022-19d106fd7518"} // Emails -{"id": "email", "sk": "sergio@somosbeta.com.br", "user_id": "15bacf02-1535-4bee-9022-19d106fd7518"} \ No newline at end of file +{"id": "email", "sk": "sergio@somosbeta.com.br", "user_id": "15bacf02-1535-4bee-9022-19d106fd7518"} + +// Course scormset +{"id": "c27d1b4f-575c-4b6b-82a1-9b91ff369e0b", "sk": "SCORMSET#76c75561-d972-43ef-9818-497d8fc6edbe", "packages": [{"version": "1.2", "scormdriver": "s3://saladeaula.digital/scorm/nr-33-espacos-confinados-conteudo-de-demonstracao-scorm12/scormdriver/indexAPI.html"}]} \ No newline at end of file diff --git a/apps/admin.saladeaula.digital/app/components/nav-main.tsx b/apps/admin.saladeaula.digital/app/components/nav-main.tsx index 8edd5bf..3c72278 100644 --- a/apps/admin.saladeaula.digital/app/components/nav-main.tsx +++ b/apps/admin.saladeaula.digital/app/components/nav-main.tsx @@ -64,7 +64,7 @@ function SidebarMenuItemLink({ title, url, icon: Icon }: NavItem) { return ( - {({ isActive }) => ( + {({ isActive, isPending }) => ( - {Icon && } + {Icon ? : null} {title} diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.admins._index/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.admins._index/route.tsx index 1025ffb..04c0c77 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.admins._index/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.admins._index/route.tsx @@ -11,7 +11,7 @@ import { Await, NavLink, useParams, useRevalidator } from 'react-router' import { toast } from 'sonner' import { Abbr } from '@/components/abbr' -import { request as req } from '@/lib/request' + import { Skeleton } from '@repo/ui/components/skeleton' import { AlertDialog, @@ -34,6 +34,7 @@ import { } from '@repo/ui/components/ui/dropdown-menu' import { Spinner } from '@repo/ui/components/ui/spinner' import { initials } from '@repo/ui/lib/utils' +import { request as req } from '@repo/util/request' type Admin = { sk: string diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.courses._index/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.courses._index/route.tsx index 23f0f6c..9a69470 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.courses._index/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.courses._index/route.tsx @@ -6,8 +6,6 @@ import { Suspense, useMemo } from 'react' import { Await, useSearchParams } from 'react-router' import placeholder from '@/assets/placeholder.webp' -import { createSearch } from '@/lib/meili' -import { request as req } from '@/lib/request' import { SearchForm } from '@repo/ui/components/search-form' import { Skeleton } from '@repo/ui/components/skeleton' @@ -26,6 +24,8 @@ import { } from '@repo/ui/components/ui/empty' import { Kbd } from '@repo/ui/components/ui/kbd' import { cn } from '@repo/ui/lib/utils' +import { createSearch } from '@repo/util/meili' +import { request as req } from '@repo/util/request' type Cert = { exp_interval: number diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments._index/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments._index/route.tsx index 9d0de68..74020d9 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments._index/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments._index/route.tsx @@ -18,7 +18,6 @@ import * as XLSX from 'xlsx' import { DataTable, DataTableViewOptions } from '@/components/data-table' import { RangeCalendarFilter } from '@/components/range-calendar-filter' -import { createSearch } from '@/lib/meili' import { FacetedFilter } from '@repo/ui/components/faceted-filter' import { SearchForm } from '@repo/ui/components/search-form' @@ -32,6 +31,7 @@ import { DropdownMenuTrigger } from '@repo/ui/components/ui/dropdown-menu' import { Kbd } from '@repo/ui/components/ui/kbd' +import { createSearch } from '@repo/util/meili' import { columns, type Enrollment } from './columns' import { headers, sortings, statuses } from './data' diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.orders._index/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.orders._index/route.tsx index 07a3e99..0221c51 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.orders._index/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.orders._index/route.tsx @@ -4,8 +4,9 @@ import { Suspense } from 'react' import { Await } from 'react-router' import { DataTable } from '@/components/data-table' -import { createSearch } from '@/lib/meili' + import { Skeleton } from '@repo/ui/components/skeleton' +import { createSearch } from '@repo/util/meili' import { columns, type Order } from './columns' export function meta({}: Route.MetaArgs) { diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.scheduled/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.scheduled/route.tsx index 197a7ed..604a6b8 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.scheduled/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.scheduled/route.tsx @@ -1,11 +1,11 @@ import type { Route } from './+types' import { Suspense } from 'react' - -import { request as req } from '@/lib/request' -import { Skeleton } from '@repo/ui/components/skeleton' import { Await } from 'react-router' +import { Skeleton } from '@repo/ui/components/skeleton' +import { request as req } from '@repo/util/request' + export function meta({}: Route.MetaArgs) { return [{ title: 'Matrículas agendadas' }] } diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.$id.emails/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.$id.emails/route.tsx index 18ab5e0..f4d79aa 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.$id.emails/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.$id.emails/route.tsx @@ -3,8 +3,6 @@ import type { Route } from './+types' import { Suspense } from 'react' import { Await, useOutletContext } from 'react-router' -import { request as req } from '@/lib/request' - import { Skeleton } from '@repo/ui/components/skeleton' import { Card, @@ -18,6 +16,7 @@ import { NativeSelect, NativeSelectOption } from '@repo/ui/components/ui/native-select' +import { request as req } from '@repo/util/request' import { Button } from '@repo/ui/components/ui/button' import type { User } from '../_.$orgid.users.$id/route' @@ -64,9 +63,13 @@ export default function Route({ loaderData: { data } }) { Email principal - {user.email} será - usado para mensagens e pode ser usado para redefinições de - senha. + + + {user.email} + + {' '} + será usado para mensagens e pode ser usado para redefinições + de senha. diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.$id/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.$id/route.tsx index 51a8350..ba54c39 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.$id/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.$id/route.tsx @@ -7,8 +7,6 @@ import { type ShouldRevalidateFunctionArgs } from 'react-router' -import { request as req } from '@/lib/request' - import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar' import { Breadcrumb, @@ -20,6 +18,7 @@ import { } from '@repo/ui/components/ui/breadcrumb' import { Tabs, TabsList, TabsTrigger } from '@repo/ui/components/ui/tabs' import { initials } from '@repo/ui/lib/utils' +import { request as req } from '@repo/util/request' export function meta() { return [ @@ -92,7 +91,9 @@ export default function Route({
  • {user.name}
  • -
  • {user.email}
  • +
  • + {user.email} +
diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users._index/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users._index/route.tsx index 223c973..0a964cf 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users._index/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users._index/route.tsx @@ -5,13 +5,13 @@ import { Suspense } from 'react' import { Await, Link, useSearchParams } from 'react-router' import { DataTable } from '@/components/data-table' -import { createSearch } from '@/lib/meili' import { columns, type User } from './columns' import { SearchForm } from '@repo/ui/components/search-form' import { Skeleton } from '@repo/ui/components/skeleton' import { Button } from '@repo/ui/components/ui/button' import { Kbd } from '@repo/ui/components/ui/kbd' +import { createSearch } from '@repo/util/meili' export function meta({}: Route.MetaArgs) { return [ diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.add/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.add/route.tsx index 4b8231b..d88f201 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.add/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.add/route.tsx @@ -43,7 +43,7 @@ import { Input } from '@repo/ui/components/ui/input' import { Spinner } from '@repo/ui/components/ui/spinner' import { useWorksapce } from '@/components/workspace-switcher' -import { HttpMethod, request as req } from '@/lib/request' +import { HttpMethod, request as req } from '@repo/util/request' import { useEffect } from 'react' const isName = (name: string) => name && name.includes(' ') diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid/route.tsx index 38f0b65..ac33f23 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid/route.tsx @@ -4,7 +4,6 @@ import * as cookie from 'cookie' import { Outlet, type ShouldRevalidateFunctionArgs } from 'react-router' import { AppSidebar } from '@/components/app-sidebar' -import { request as req } from '@/lib/request' import { WorkspaceProvider } from '@/components/workspace-switcher' import { userContext } from '@repo/auth/context' @@ -17,6 +16,7 @@ import { SidebarTrigger } from '@repo/ui/components/ui/sidebar' import { Toaster } from '@repo/ui/components/ui/sonner' +import { request as req } from '@repo/util/request' export const middleware: Route.MiddlewareFunction[] = [authMiddleware] diff --git a/apps/admin.saladeaula.digital/app/routes/_index/route.tsx b/apps/admin.saladeaula.digital/app/routes/_index/route.tsx index a489612..11b33ec 100644 --- a/apps/admin.saladeaula.digital/app/routes/_index/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_index/route.tsx @@ -2,9 +2,9 @@ import type { Route } from './+types' import { redirect } from 'react-router' -import { request as req } from '@/lib/request' import { userContext } from '@repo/auth/context' import { authMiddleware } from '@repo/auth/middleware/auth' +import { request as req } from '@repo/util/request' export const middleware: Route.MiddlewareFunction[] = [authMiddleware] diff --git a/apps/admin.saladeaula.digital/package.json b/apps/admin.saladeaula.digital/package.json index 323acc8..38df53c 100644 --- a/apps/admin.saladeaula.digital/package.json +++ b/apps/admin.saladeaula.digital/package.json @@ -15,6 +15,7 @@ "@react-router/fs-routes": "^7.9.5", "@repo/auth": "*", "@repo/ui": "*", + "@repo/util": "^0.0.0", "@tanstack/react-table": "^8.21.3", "cookie": "^1.0.2", "date-fns": "^4.1.0", diff --git a/apps/saladeaula.digital/app/@types/window.d.ts b/apps/saladeaula.digital/app/@types/window.d.ts index 9061f84..745d642 100644 --- a/apps/saladeaula.digital/app/@types/window.d.ts +++ b/apps/saladeaula.digital/app/@types/window.d.ts @@ -1,7 +1,8 @@ -import { type Scorm12API } from 'scorm-again/scorm12' +import type { Scorm12API, Scorm2004API } from 'scorm-again' declare global { interface Window { API: Scorm12API + API_1484_11: Scorm2004API } } diff --git a/apps/saladeaula.digital/app/components/container.tsx b/apps/saladeaula.digital/app/components/container.tsx index 0066359..815339f 100644 --- a/apps/saladeaula.digital/app/components/container.tsx +++ b/apps/saladeaula.digital/app/components/container.tsx @@ -7,7 +7,7 @@ type ContainerProps = { export function Container({ children, className }: ContainerProps) { return ( -
+
{children}
) diff --git a/apps/saladeaula.digital/app/components/faceted-filter.tsx b/apps/saladeaula.digital/app/components/faceted-filter.tsx deleted file mode 100644 index f67089c..0000000 --- a/apps/saladeaula.digital/app/components/faceted-filter.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { Check, PlusCircle } from 'lucide-react' -import { useState } from 'react' - -import { Badge } from '@repo/ui/components/ui/badge' -import { Button } from '@repo/ui/components/ui/button' -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, - CommandSeparator -} from '@repo/ui/components/ui/command' -import { - Popover, - PopoverContent, - PopoverTrigger -} from '@repo/ui/components/ui/popover' -import { Separator } from '@repo/ui/components/ui/separator' -import { cn } from '@/lib/utils' - -interface FacetedFilterProps { - value?: string[] - title?: string - options: { - label: string - value: string - icon?: React.ComponentType<{ className?: string }> - }[] - onChange?: (values: string[]) => void -} - -export function FacetedFilter({ - value = [], - title, - options, - onChange -}: FacetedFilterProps) { - const [selectedValues, setSelectedValues] = useState(new Set(value)) - - return ( - - - - - - - - - Nenhum resultado encontrado. - - {options.map((option) => { - const isSelected = selectedValues.has(option.value) - - return ( - { - if (isSelected) { - selectedValues.delete(option.value) - } else { - selectedValues.add(option.value) - } - - setSelectedValues(selectedValues) - onChange?.(Array.from(selectedValues)) - }} - > -
- -
- {option.icon && ( - - )} - {option.label} -
- ) - })} -
- {selectedValues.size > 0 && ( - <> - - - { - setSelectedValues(new Set()) - onChange?.([]) - }} - className="justify-center text-center" - > - Limpar filtros - - - - )} -
-
-
-
- ) -} diff --git a/apps/saladeaula.digital/app/components/scorm-player.tsx b/apps/saladeaula.digital/app/components/scorm-player.tsx index 8f1400c..de1c5c5 100644 --- a/apps/saladeaula.digital/app/components/scorm-player.tsx +++ b/apps/saladeaula.digital/app/components/scorm-player.tsx @@ -1,42 +1,56 @@ 'use client' -import { useLocalStorage } from '@/hooks/useLocalStorage' -import SHA256 from 'crypto-js/sha256' +import { omit } from 'ramda' import { useEffect, useRef, useState } from 'react' -import { Scorm12API } from 'scorm-again/scorm12' +import { Scorm12API, Scorm2004API } from 'scorm-again' -const settings = { - autocommit: true - // mastery_override: true - // logLevel: 2, +export type ScormVersion = '1.2' | '2004' + +export type ScormPlayerProps = { + settings: Record + scormVersion: ScormVersion + scormState: Record + scormContentPath: string + className?: string + onCommit?: (value: any) => void + setValue?: (element: string, value: any) => void } +// https://scorm.com/scorm-explained/technical-scorm/run-time/run-time-reference export function ScormPlayer({ + settings, + scormVersion, scormState, scormContentPath, - className -}: { - scormState: object - scormContentPath: string - className: string -}) { + className, + onCommit +}: ScormPlayerProps) { const [iframeLoaded, setIframeLoaded] = useState(false) - const scormApiRef = useRef(null) - const hash = SHA256(scormContentPath).toString() - const [_, setScormState] = useLocalStorage(`scormState.${hash}`, {}) + const scormApiRef = useRef(null) useEffect(() => { - const scormApi = new Scorm12API(settings) - scormApi.loadFromFlattenedJSON(scormState) - - scormApi.on('LMSCommit', function () { - console.log('Committed') - - setScormState(scormApi.renderCommitCMI(true)) - }) - + const cls = scormVersion === '1.2' ? Scorm12API : Scorm2004API + const scormApi = new cls(settings) + scormApi.loadFromJSON(omit(['interactions'], scormState)) + // scormApi.loadFromFlattenedJSON(scormState) scormApiRef.current = scormApi - window.API = scormApi + + if (scormApi instanceof Scorm12API) { + window.API = scormApi + + scormApi.on('LMSCommit', function () { + onCommit?.(scormApi.renderCommitCMI(true)) + }) + } + + if (scormApi instanceof Scorm2004API) { + window.API_1484_11 = scormApi + + scormApi.on('Commit', function () { + onCommit?.(scormApi.renderCommitCMI(true)) + }) + } + setIframeLoaded(true) return () => { @@ -54,21 +68,30 @@ export function ScormPlayer({ function unload() { if (unloaded || scormApi.isTerminated()) { - return + return false + } + + if (scormApi instanceof Scorm12API) { + scormApi.LMSSetValue('cmi.core.exit', 'suspend') + scormApi.LMSCommit() + scormApi.LMSFinish() + } + + if (scormApi instanceof Scorm2004API) { + scormApi.SetValue('cmi.exit', 'suspend') + scormApi.Commit() + scormApi.Terminate() } - scormApi.LMSSetValue('cmi.core.exit', 'suspend') - scormApi.LMSCommit() - scormApi.LMSFinish() unloaded = true } window.addEventListener('beforeunload', unload) - window.addEventListener('unload', unload) + window.addEventListener('pagehide', unload) return () => { window.removeEventListener('beforeunload', unload) - window.removeEventListener('unload', unload) + window.removeEventListener('pagehide', unload) } }, []) diff --git a/apps/saladeaula.digital/app/components/scorm12-player.tsx b/apps/saladeaula.digital/app/components/scorm12-player.tsx new file mode 100644 index 0000000..60e2ae9 --- /dev/null +++ b/apps/saladeaula.digital/app/components/scorm12-player.tsx @@ -0,0 +1,71 @@ +'use client' + +import { useEffect, useRef, useState } from 'react' +import { Scorm12API } from 'scorm-again/scorm12' +import { settings, type ScormPlayerProps } from './scorm-player' + +// https://scorm.com/scorm-explained/technical-scorm/run-time/run-time-reference/#section-2 +export function Scorm12Player({ + scormState, + scormContentPath, + className, + onCommit, + setValue +}: ScormPlayerProps) { + const [iframeLoaded, setIframeLoaded] = useState(false) + const scormApiRef = useRef(null) + + useEffect(() => { + const scormApi = new Scorm12API(settings) + scormApi.loadFromFlattenedJSON(scormState) + + scormApi.on('LMSCommit', function () { + onCommit?.(scormApi.renderCommitCMI(true)) + }) + + scormApi.on('LMSSetValue.*', function (element: any, value: any) { + setValue?.(element, value) + }) + + scormApiRef.current = scormApi + window.API = scormApi + setIframeLoaded(true) + + return () => { + scormApiRef.current = null + } + }, []) + + useEffect(() => { + if (!scormApiRef.current) { + return + } + + const scormApi = scormApiRef.current + let unloaded = false + + function unload() { + if (unloaded || scormApi.isTerminated()) { + return false + } + + scormApi.LMSSetValue('cmi.core.exit', 'suspend') + scormApi.LMSCommit() + scormApi.LMSFinish() + + unloaded = true + } + + window.addEventListener('beforeunload', unload) + window.addEventListener('unload', unload) + + return () => { + window.removeEventListener('beforeunload', unload) + window.removeEventListener('unload', unload) + } + }, []) + + if (iframeLoaded) { + return