update
This commit is contained in:
@@ -33,6 +33,14 @@ def get_course(course_id: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post('/<course_id>/scormset/<scormset_id>')
|
||||||
|
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):
|
class Cert(BaseModel):
|
||||||
exp_interval: int | None = None
|
exp_interval: int | None = None
|
||||||
s3_uri: str | None = None
|
s3_uri: str | None = None
|
||||||
@@ -47,6 +55,7 @@ class Course(BaseModel):
|
|||||||
access_period: int
|
access_period: int
|
||||||
cert: Cert
|
cert: Cert
|
||||||
draft: bool = False
|
draft: bool = False
|
||||||
|
demo: bool = False
|
||||||
rawfile: bytes | None = None
|
rawfile: bytes | None = None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 import Logger
|
||||||
from aws_lambda_powertools.event_handler.api_gateway import Router
|
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 boto3clients import dynamodb_client
|
||||||
from config import ENROLLMENT_TABLE
|
from config import ENROLLMENT_TABLE
|
||||||
|
|
||||||
@@ -20,5 +29,62 @@ dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
|
|||||||
@router.get('/<enrollment_id>')
|
@router.get('/<enrollment_id>')
|
||||||
def get_enrollment(enrollment_id: str):
|
def get_enrollment(enrollment_id: str):
|
||||||
return dyn.collection.get_items(
|
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('/<enrollment_id>/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('/<enrollment_id>')
|
||||||
|
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)
|
||||||
|
|||||||
@@ -102,3 +102,19 @@ def test_template(
|
|||||||
lambda_context,
|
lambda_context,
|
||||||
)
|
)
|
||||||
assert r['statusCode'] == HTTPStatus.OK
|
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
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import json
|
import json
|
||||||
from http import HTTPMethod, HTTPStatus
|
from http import HTTPMethod, HTTPStatus
|
||||||
|
|
||||||
|
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
|
||||||
|
|
||||||
from ..conftest import HttpApiProxy, LambdaContext
|
from ..conftest import HttpApiProxy, LambdaContext
|
||||||
|
|
||||||
|
|
||||||
@@ -22,3 +24,73 @@ def test_get_enrollment(
|
|||||||
body = json.loads(r['body'])
|
body = json.loads(r['body'])
|
||||||
assert 'user' in body
|
assert 'user' in body
|
||||||
assert 'course' 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']
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
// Enrollments
|
// 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": "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
|
// Orgs
|
||||||
{"id": "2a8963fc-4694-4fe2-953a-316d1b10f1f5", "sk": "0", "name": "pytest", "cnpj": "04978826000180"}
|
{"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"}
|
{"id": "cpf", "sk": "07879819908", "user_id": "15bacf02-1535-4bee-9022-19d106fd7518"}
|
||||||
|
|
||||||
// Emails
|
// Emails
|
||||||
{"id": "email", "sk": "sergio@somosbeta.com.br", "user_id": "15bacf02-1535-4bee-9022-19d106fd7518"}
|
{"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"}]}
|
||||||
@@ -64,7 +64,7 @@ function SidebarMenuItemLink({ title, url, icon: Icon }: NavItem) {
|
|||||||
return (
|
return (
|
||||||
<SidebarMenuItem key={title} onClick={onToggle}>
|
<SidebarMenuItem key={title} onClick={onToggle}>
|
||||||
<NavLink to={`/${orgid}${url}`}>
|
<NavLink to={`/${orgid}${url}`}>
|
||||||
{({ isActive }) => (
|
{({ isActive, isPending }) => (
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
asChild
|
asChild
|
||||||
className="data-[active=true]:text-lime-500"
|
className="data-[active=true]:text-lime-500"
|
||||||
@@ -72,7 +72,7 @@ function SidebarMenuItemLink({ title, url, icon: Icon }: NavItem) {
|
|||||||
tooltip={title}
|
tooltip={title}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{Icon && <Icon />}
|
{Icon ? <Icon /> : null}
|
||||||
<span>{title}</span>
|
<span>{title}</span>
|
||||||
</span>
|
</span>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { Await, NavLink, useParams, useRevalidator } from 'react-router'
|
|||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
import { Abbr } from '@/components/abbr'
|
import { Abbr } from '@/components/abbr'
|
||||||
import { request as req } from '@/lib/request'
|
|
||||||
import { Skeleton } from '@repo/ui/components/skeleton'
|
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
} from '@repo/ui/components/ui/dropdown-menu'
|
} from '@repo/ui/components/ui/dropdown-menu'
|
||||||
import { Spinner } from '@repo/ui/components/ui/spinner'
|
import { Spinner } from '@repo/ui/components/ui/spinner'
|
||||||
import { initials } from '@repo/ui/lib/utils'
|
import { initials } from '@repo/ui/lib/utils'
|
||||||
|
import { request as req } from '@repo/util/request'
|
||||||
|
|
||||||
type Admin = {
|
type Admin = {
|
||||||
sk: string
|
sk: string
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import { Suspense, useMemo } from 'react'
|
|||||||
import { Await, useSearchParams } from 'react-router'
|
import { Await, useSearchParams } from 'react-router'
|
||||||
|
|
||||||
import placeholder from '@/assets/placeholder.webp'
|
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 { SearchForm } from '@repo/ui/components/search-form'
|
||||||
import { Skeleton } from '@repo/ui/components/skeleton'
|
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||||
@@ -26,6 +24,8 @@ import {
|
|||||||
} from '@repo/ui/components/ui/empty'
|
} from '@repo/ui/components/ui/empty'
|
||||||
import { Kbd } from '@repo/ui/components/ui/kbd'
|
import { Kbd } from '@repo/ui/components/ui/kbd'
|
||||||
import { cn } from '@repo/ui/lib/utils'
|
import { cn } from '@repo/ui/lib/utils'
|
||||||
|
import { createSearch } from '@repo/util/meili'
|
||||||
|
import { request as req } from '@repo/util/request'
|
||||||
|
|
||||||
type Cert = {
|
type Cert = {
|
||||||
exp_interval: number
|
exp_interval: number
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import * as XLSX from 'xlsx'
|
|||||||
|
|
||||||
import { DataTable, DataTableViewOptions } from '@/components/data-table'
|
import { DataTable, DataTableViewOptions } from '@/components/data-table'
|
||||||
import { RangeCalendarFilter } from '@/components/range-calendar-filter'
|
import { RangeCalendarFilter } from '@/components/range-calendar-filter'
|
||||||
import { createSearch } from '@/lib/meili'
|
|
||||||
|
|
||||||
import { FacetedFilter } from '@repo/ui/components/faceted-filter'
|
import { FacetedFilter } from '@repo/ui/components/faceted-filter'
|
||||||
import { SearchForm } from '@repo/ui/components/search-form'
|
import { SearchForm } from '@repo/ui/components/search-form'
|
||||||
@@ -32,6 +31,7 @@ import {
|
|||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from '@repo/ui/components/ui/dropdown-menu'
|
} from '@repo/ui/components/ui/dropdown-menu'
|
||||||
import { Kbd } from '@repo/ui/components/ui/kbd'
|
import { Kbd } from '@repo/ui/components/ui/kbd'
|
||||||
|
import { createSearch } from '@repo/util/meili'
|
||||||
import { columns, type Enrollment } from './columns'
|
import { columns, type Enrollment } from './columns'
|
||||||
import { headers, sortings, statuses } from './data'
|
import { headers, sortings, statuses } from './data'
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import { Suspense } from 'react'
|
|||||||
import { Await } from 'react-router'
|
import { Await } from 'react-router'
|
||||||
|
|
||||||
import { DataTable } from '@/components/data-table'
|
import { DataTable } from '@/components/data-table'
|
||||||
import { createSearch } from '@/lib/meili'
|
|
||||||
import { Skeleton } from '@repo/ui/components/skeleton'
|
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||||
|
import { createSearch } from '@repo/util/meili'
|
||||||
import { columns, type Order } from './columns'
|
import { columns, type Order } from './columns'
|
||||||
|
|
||||||
export function meta({}: Route.MetaArgs) {
|
export function meta({}: Route.MetaArgs) {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { Route } from './+types'
|
import type { Route } from './+types'
|
||||||
|
|
||||||
import { Suspense } from 'react'
|
import { Suspense } from 'react'
|
||||||
|
|
||||||
import { request as req } from '@/lib/request'
|
|
||||||
import { Skeleton } from '@repo/ui/components/skeleton'
|
|
||||||
import { Await } from 'react-router'
|
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) {
|
export function meta({}: Route.MetaArgs) {
|
||||||
return [{ title: 'Matrículas agendadas' }]
|
return [{ title: 'Matrículas agendadas' }]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import type { Route } from './+types'
|
|||||||
import { Suspense } from 'react'
|
import { Suspense } from 'react'
|
||||||
import { Await, useOutletContext } from 'react-router'
|
import { Await, useOutletContext } from 'react-router'
|
||||||
|
|
||||||
import { request as req } from '@/lib/request'
|
|
||||||
|
|
||||||
import { Skeleton } from '@repo/ui/components/skeleton'
|
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -18,6 +16,7 @@ import {
|
|||||||
NativeSelect,
|
NativeSelect,
|
||||||
NativeSelectOption
|
NativeSelectOption
|
||||||
} from '@repo/ui/components/ui/native-select'
|
} from '@repo/ui/components/ui/native-select'
|
||||||
|
import { request as req } from '@repo/util/request'
|
||||||
|
|
||||||
import { Button } from '@repo/ui/components/ui/button'
|
import { Button } from '@repo/ui/components/ui/button'
|
||||||
import type { User } from '../_.$orgid.users.$id/route'
|
import type { User } from '../_.$orgid.users.$id/route'
|
||||||
@@ -64,9 +63,13 @@ export default function Route({ loaderData: { data } }) {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-lg">Email principal</CardTitle>
|
<CardTitle className="text-lg">Email principal</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
<Kbd className="font-mono border">{user.email}</Kbd> será
|
<Kbd className="font-mono border">
|
||||||
usado para mensagens e pode ser usado para redefinições de
|
<span className="truncate max-lg:max-w-62">
|
||||||
senha.
|
{user.email}
|
||||||
|
</span>
|
||||||
|
</Kbd>{' '}
|
||||||
|
será usado para mensagens e pode ser usado para redefinições
|
||||||
|
de senha.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import {
|
|||||||
type ShouldRevalidateFunctionArgs
|
type ShouldRevalidateFunctionArgs
|
||||||
} from 'react-router'
|
} from 'react-router'
|
||||||
|
|
||||||
import { request as req } from '@/lib/request'
|
|
||||||
|
|
||||||
import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar'
|
import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar'
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
@@ -20,6 +18,7 @@ import {
|
|||||||
} from '@repo/ui/components/ui/breadcrumb'
|
} from '@repo/ui/components/ui/breadcrumb'
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@repo/ui/components/ui/tabs'
|
import { Tabs, TabsList, TabsTrigger } from '@repo/ui/components/ui/tabs'
|
||||||
import { initials } from '@repo/ui/lib/utils'
|
import { initials } from '@repo/ui/lib/utils'
|
||||||
|
import { request as req } from '@repo/util/request'
|
||||||
|
|
||||||
export function meta() {
|
export function meta() {
|
||||||
return [
|
return [
|
||||||
@@ -92,7 +91,9 @@ export default function Route({
|
|||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li className="font-bold text-lg">{user.name}</li>
|
<li className="font-bold text-lg">{user.name}</li>
|
||||||
<li className="text-muted-foreground text-sm">{user.email}</li>
|
<li className="text-muted-foreground text-sm truncate max-lg:max-w-62">
|
||||||
|
{user.email}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import { Suspense } from 'react'
|
|||||||
import { Await, Link, useSearchParams } from 'react-router'
|
import { Await, Link, useSearchParams } from 'react-router'
|
||||||
|
|
||||||
import { DataTable } from '@/components/data-table'
|
import { DataTable } from '@/components/data-table'
|
||||||
import { createSearch } from '@/lib/meili'
|
|
||||||
import { columns, type User } from './columns'
|
import { columns, type User } from './columns'
|
||||||
|
|
||||||
import { SearchForm } from '@repo/ui/components/search-form'
|
import { SearchForm } from '@repo/ui/components/search-form'
|
||||||
import { Skeleton } from '@repo/ui/components/skeleton'
|
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||||
import { Button } from '@repo/ui/components/ui/button'
|
import { Button } from '@repo/ui/components/ui/button'
|
||||||
import { Kbd } from '@repo/ui/components/ui/kbd'
|
import { Kbd } from '@repo/ui/components/ui/kbd'
|
||||||
|
import { createSearch } from '@repo/util/meili'
|
||||||
|
|
||||||
export function meta({}: Route.MetaArgs) {
|
export function meta({}: Route.MetaArgs) {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import { Input } from '@repo/ui/components/ui/input'
|
|||||||
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'
|
||||||
import { HttpMethod, request as req } from '@/lib/request'
|
import { HttpMethod, request as req } from '@repo/util/request'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
const isName = (name: string) => name && name.includes(' ')
|
const isName = (name: string) => name && name.includes(' ')
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import * as cookie from 'cookie'
|
|||||||
import { Outlet, type ShouldRevalidateFunctionArgs } from 'react-router'
|
import { Outlet, type ShouldRevalidateFunctionArgs } from 'react-router'
|
||||||
|
|
||||||
import { AppSidebar } from '@/components/app-sidebar'
|
import { AppSidebar } from '@/components/app-sidebar'
|
||||||
import { request as req } from '@/lib/request'
|
|
||||||
|
|
||||||
import { WorkspaceProvider } from '@/components/workspace-switcher'
|
import { WorkspaceProvider } from '@/components/workspace-switcher'
|
||||||
import { userContext } from '@repo/auth/context'
|
import { userContext } from '@repo/auth/context'
|
||||||
@@ -17,6 +16,7 @@ import {
|
|||||||
SidebarTrigger
|
SidebarTrigger
|
||||||
} from '@repo/ui/components/ui/sidebar'
|
} from '@repo/ui/components/ui/sidebar'
|
||||||
import { Toaster } from '@repo/ui/components/ui/sonner'
|
import { Toaster } from '@repo/ui/components/ui/sonner'
|
||||||
|
import { request as req } from '@repo/util/request'
|
||||||
|
|
||||||
export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
|
export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import type { Route } from './+types'
|
|||||||
|
|
||||||
import { redirect } from 'react-router'
|
import { redirect } from 'react-router'
|
||||||
|
|
||||||
import { request as req } from '@/lib/request'
|
|
||||||
import { userContext } from '@repo/auth/context'
|
import { userContext } from '@repo/auth/context'
|
||||||
import { authMiddleware } from '@repo/auth/middleware/auth'
|
import { authMiddleware } from '@repo/auth/middleware/auth'
|
||||||
|
import { request as req } from '@repo/util/request'
|
||||||
|
|
||||||
export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
|
export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"@react-router/fs-routes": "^7.9.5",
|
"@react-router/fs-routes": "^7.9.5",
|
||||||
"@repo/auth": "*",
|
"@repo/auth": "*",
|
||||||
"@repo/ui": "*",
|
"@repo/ui": "*",
|
||||||
|
"@repo/util": "^0.0.0",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"cookie": "^1.0.2",
|
"cookie": "^1.0.2",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { type Scorm12API } from 'scorm-again/scorm12'
|
import type { Scorm12API, Scorm2004API } from 'scorm-again'
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
API: Scorm12API
|
API: Scorm12API
|
||||||
|
API_1484_11: Scorm2004API
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ type ContainerProps = {
|
|||||||
|
|
||||||
export function Container({ children, className }: ContainerProps) {
|
export function Container({ children, className }: ContainerProps) {
|
||||||
return (
|
return (
|
||||||
<main className="px-4">
|
<main className="p-4">
|
||||||
<div className={cn('container mx-auto', className)}>{children}</div>
|
<div className={cn('container mx-auto', className)}>{children}</div>
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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<TData, TValue> {
|
|
||||||
value?: string[]
|
|
||||||
title?: string
|
|
||||||
options: {
|
|
||||||
label: string
|
|
||||||
value: string
|
|
||||||
icon?: React.ComponentType<{ className?: string }>
|
|
||||||
}[]
|
|
||||||
onChange?: (values: string[]) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FacetedFilter<TData, TValue>({
|
|
||||||
value = [],
|
|
||||||
title,
|
|
||||||
options,
|
|
||||||
onChange
|
|
||||||
}: FacetedFilterProps<TData, TValue>) {
|
|
||||||
const [selectedValues, setSelectedValues] = useState(new Set(value))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="h-9 border-dashed cursor-pointer"
|
|
||||||
>
|
|
||||||
<PlusCircle />
|
|
||||||
{title}
|
|
||||||
{selectedValues?.size > 0 && (
|
|
||||||
<>
|
|
||||||
<Separator orientation="vertical" className="mx-2 h-4" />
|
|
||||||
<Badge
|
|
||||||
variant="secondary"
|
|
||||||
className="rounded-sm px-1 font-normal lg:hidden"
|
|
||||||
>
|
|
||||||
{selectedValues.size}
|
|
||||||
</Badge>
|
|
||||||
<div className="hidden gap-1 lg:flex">
|
|
||||||
{selectedValues.size > 2 ? (
|
|
||||||
<Badge
|
|
||||||
variant="secondary"
|
|
||||||
className="rounded-sm px-1 font-normal"
|
|
||||||
>
|
|
||||||
{selectedValues.size} selecionados
|
|
||||||
</Badge>
|
|
||||||
) : (
|
|
||||||
options
|
|
||||||
.filter((option) => selectedValues.has(option.value))
|
|
||||||
.map((option) => (
|
|
||||||
<Badge
|
|
||||||
variant="secondary"
|
|
||||||
key={option.value}
|
|
||||||
className="rounded-sm px-1 font-normal"
|
|
||||||
>
|
|
||||||
{option.label}
|
|
||||||
</Badge>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-[200px] p-0" align="start">
|
|
||||||
<Command>
|
|
||||||
<CommandInput placeholder={title} />
|
|
||||||
<CommandList>
|
|
||||||
<CommandEmpty>Nenhum resultado encontrado.</CommandEmpty>
|
|
||||||
<CommandGroup>
|
|
||||||
{options.map((option) => {
|
|
||||||
const isSelected = selectedValues.has(option.value)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CommandItem
|
|
||||||
className="cursor-pointer"
|
|
||||||
key={option.value}
|
|
||||||
onSelect={() => {
|
|
||||||
if (isSelected) {
|
|
||||||
selectedValues.delete(option.value)
|
|
||||||
} else {
|
|
||||||
selectedValues.add(option.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedValues(selectedValues)
|
|
||||||
onChange?.(Array.from(selectedValues))
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'flex size-4 items-center justify-center rounded-[4px] border',
|
|
||||||
isSelected
|
|
||||||
? 'bg-primary border-primary text-primary-foreground'
|
|
||||||
: 'border-input [&_svg]:invisible'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Check className="text-primary-foreground size-3.5" />
|
|
||||||
</div>
|
|
||||||
{option.icon && (
|
|
||||||
<option.icon className="text-muted-foreground size-4" />
|
|
||||||
)}
|
|
||||||
<span>{option.label}</span>
|
|
||||||
</CommandItem>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</CommandGroup>
|
|
||||||
{selectedValues.size > 0 && (
|
|
||||||
<>
|
|
||||||
<CommandSeparator />
|
|
||||||
<CommandGroup>
|
|
||||||
<CommandItem
|
|
||||||
onSelect={() => {
|
|
||||||
setSelectedValues(new Set())
|
|
||||||
onChange?.([])
|
|
||||||
}}
|
|
||||||
className="justify-center text-center"
|
|
||||||
>
|
|
||||||
Limpar filtros
|
|
||||||
</CommandItem>
|
|
||||||
</CommandGroup>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,42 +1,56 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useLocalStorage } from '@/hooks/useLocalStorage'
|
import { omit } from 'ramda'
|
||||||
import SHA256 from 'crypto-js/sha256'
|
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { Scorm12API } from 'scorm-again/scorm12'
|
import { Scorm12API, Scorm2004API } from 'scorm-again'
|
||||||
|
|
||||||
const settings = {
|
export type ScormVersion = '1.2' | '2004'
|
||||||
autocommit: true
|
|
||||||
// mastery_override: true
|
export type ScormPlayerProps = {
|
||||||
// logLevel: 2,
|
settings: Record<string, any>
|
||||||
|
scormVersion: ScormVersion
|
||||||
|
scormState: Record<string, any>
|
||||||
|
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({
|
export function ScormPlayer({
|
||||||
|
settings,
|
||||||
|
scormVersion,
|
||||||
scormState,
|
scormState,
|
||||||
scormContentPath,
|
scormContentPath,
|
||||||
className
|
className,
|
||||||
}: {
|
onCommit
|
||||||
scormState: object
|
}: ScormPlayerProps) {
|
||||||
scormContentPath: string
|
|
||||||
className: string
|
|
||||||
}) {
|
|
||||||
const [iframeLoaded, setIframeLoaded] = useState(false)
|
const [iframeLoaded, setIframeLoaded] = useState(false)
|
||||||
const scormApiRef = useRef<Scorm12API | null>(null)
|
const scormApiRef = useRef<Scorm12API | Scorm2004API | null>(null)
|
||||||
const hash = SHA256(scormContentPath).toString()
|
|
||||||
const [_, setScormState] = useLocalStorage(`scormState.${hash}`, {})
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const scormApi = new Scorm12API(settings)
|
const cls = scormVersion === '1.2' ? Scorm12API : Scorm2004API
|
||||||
scormApi.loadFromFlattenedJSON(scormState)
|
const scormApi = new cls(settings)
|
||||||
|
scormApi.loadFromJSON(omit(['interactions'], scormState))
|
||||||
scormApi.on('LMSCommit', function () {
|
// scormApi.loadFromFlattenedJSON(scormState)
|
||||||
console.log('Committed')
|
|
||||||
|
|
||||||
setScormState(scormApi.renderCommitCMI(true))
|
|
||||||
})
|
|
||||||
|
|
||||||
scormApiRef.current = scormApi
|
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)
|
setIframeLoaded(true)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -54,21 +68,30 @@ export function ScormPlayer({
|
|||||||
|
|
||||||
function unload() {
|
function unload() {
|
||||||
if (unloaded || scormApi.isTerminated()) {
|
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
|
unloaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('beforeunload', unload)
|
window.addEventListener('beforeunload', unload)
|
||||||
window.addEventListener('unload', unload)
|
window.addEventListener('pagehide', unload)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('beforeunload', unload)
|
window.removeEventListener('beforeunload', unload)
|
||||||
window.removeEventListener('unload', unload)
|
window.removeEventListener('pagehide', unload)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
71
apps/saladeaula.digital/app/components/scorm12-player.tsx
Normal file
71
apps/saladeaula.digital/app/components/scorm12-player.tsx
Normal file
@@ -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<Scorm12API | null>(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 <iframe src={`/proxy/${scormContentPath}`} className={className} />
|
||||||
|
}
|
||||||
|
}
|
||||||
70
apps/saladeaula.digital/app/components/scorm2004-player.tsx
Normal file
70
apps/saladeaula.digital/app/components/scorm2004-player.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { Scorm2004API } from 'scorm-again/scorm2004'
|
||||||
|
import { settings, type ScormPlayerProps } from './scorm-player'
|
||||||
|
|
||||||
|
export function Scorm2004Player({
|
||||||
|
scormState,
|
||||||
|
scormContentPath,
|
||||||
|
className,
|
||||||
|
onCommit,
|
||||||
|
setValue
|
||||||
|
}: ScormPlayerProps) {
|
||||||
|
const [iframeLoaded, setIframeLoaded] = useState(false)
|
||||||
|
const scormApiRef = useRef<Scorm2004API | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const scormApi = new Scorm2004API(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_1484_11 = 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.SetValue('cmi.exit', 'suspend')
|
||||||
|
scormApi.Commit()
|
||||||
|
scormApi.Terminate()
|
||||||
|
|
||||||
|
unloaded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', unload)
|
||||||
|
window.addEventListener('unload', unload)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('beforeunload', unload)
|
||||||
|
window.removeEventListener('unload', unload)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (iframeLoaded) {
|
||||||
|
return <iframe src={`/proxy/${scormContentPath}`} className={className} />
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { useKeyPress } from '@/hooks/use-keypress'
|
|
||||||
import {
|
|
||||||
InputGroup,
|
|
||||||
InputGroupAddon,
|
|
||||||
InputGroupInput
|
|
||||||
} from '@repo/ui/components/ui/input-group'
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import { debounce } from 'lodash'
|
|
||||||
import { SearchIcon } from 'lucide-react'
|
|
||||||
import { useRef } from 'react'
|
|
||||||
|
|
||||||
export function SearchForm({
|
|
||||||
className,
|
|
||||||
onChange,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
|
||||||
|
|
||||||
useKeyPress('/', () => {
|
|
||||||
inputRef.current?.focus()
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<InputGroup className="group">
|
|
||||||
<InputGroupInput
|
|
||||||
className={clsx('peer', className)}
|
|
||||||
placeholder=" "
|
|
||||||
ref={inputRef}
|
|
||||||
onChange={debounce(onChange, 200)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
<InputGroupAddon>
|
|
||||||
<SearchIcon />
|
|
||||||
</InputGroupAddon>
|
|
||||||
<InputGroupAddon className="font-normal hidden peer-focus-within:hidden peer-placeholder-shown:block">
|
|
||||||
Filtar curso por nome
|
|
||||||
</InputGroupAddon>
|
|
||||||
</InputGroup>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import type { User } from '@/middleware/auth'
|
|
||||||
import { createContext } from 'react-router'
|
|
||||||
|
|
||||||
export const userContext = createContext<User | null>(null)
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
export function useLocalStorage<T>(key: string, defaultValue: T) {
|
|
||||||
const [value, setValue] = useState<T>(() => {
|
|
||||||
try {
|
|
||||||
const stored = window.localStorage.getItem(key)
|
|
||||||
return stored !== null ? JSON.parse(stored) : defaultValue
|
|
||||||
} catch {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
try {
|
|
||||||
window.localStorage.setItem(key, JSON.stringify(value))
|
|
||||||
} catch {}
|
|
||||||
}, [key, value])
|
|
||||||
|
|
||||||
return [value, setValue] as const
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { Meilisearch, type SearchResponse } from 'meilisearch'
|
|
||||||
|
|
||||||
export async function createSearch({
|
|
||||||
query,
|
|
||||||
filter,
|
|
||||||
index,
|
|
||||||
sort,
|
|
||||||
limit = 100,
|
|
||||||
env
|
|
||||||
}: {
|
|
||||||
query?: string | null
|
|
||||||
filter?: string | null
|
|
||||||
index: string
|
|
||||||
sort: string[]
|
|
||||||
limit?: number
|
|
||||||
env: Env
|
|
||||||
}): Promise<SearchResponse> {
|
|
||||||
const host = env.MEILI_HOST
|
|
||||||
const apiKey = env.MEILI_API_KEY
|
|
||||||
const client = new Meilisearch({ host, apiKey })
|
|
||||||
const index_ = client.index(index)
|
|
||||||
|
|
||||||
return index_.search(query, {
|
|
||||||
sort,
|
|
||||||
limit,
|
|
||||||
filter: filter ?? undefined
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { userContext } from '@repo/auth/context'
|
|
||||||
import type { LoaderFunctionArgs } from 'react-router'
|
|
||||||
|
|
||||||
enum Method {
|
|
||||||
GET = 'GET',
|
|
||||||
POST = 'POST',
|
|
||||||
PUT = 'PUT',
|
|
||||||
PATCH = 'PATCH',
|
|
||||||
DELETE = 'DELETE'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function request({
|
|
||||||
url,
|
|
||||||
method = Method.GET,
|
|
||||||
request,
|
|
||||||
context
|
|
||||||
}: {
|
|
||||||
url: string
|
|
||||||
method?: Method
|
|
||||||
request: LoaderFunctionArgs['request']
|
|
||||||
context: LoaderFunctionArgs['context']
|
|
||||||
}): Promise<Response> {
|
|
||||||
const user = context.get(userContext)
|
|
||||||
// @ts-ignore
|
|
||||||
const headers = new Headers({ Authorization: `Bearer ${user.accessToken}` })
|
|
||||||
// @ts-ignore
|
|
||||||
const endpoint = new URL(url, context.cloudflare.env.API_URL)
|
|
||||||
|
|
||||||
return fetch(endpoint.toString(), {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
signal: request.signal
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,8 @@ export default [
|
|||||||
route('certs', 'routes/certs.tsx'),
|
route('certs', 'routes/certs.tsx'),
|
||||||
route('payments', 'routes/payments.tsx'),
|
route('payments', 'routes/payments.tsx'),
|
||||||
route('settings', 'routes/settings.tsx'),
|
route('settings', 'routes/settings.tsx'),
|
||||||
route('player/:course', 'routes/player.tsx'),
|
route('konviva', 'routes/konviva.ts'),
|
||||||
|
route('player/:id', 'routes/player.tsx'),
|
||||||
route('proxy/*', 'routes/proxy.tsx')
|
route('proxy/*', 'routes/proxy.tsx')
|
||||||
]),
|
]),
|
||||||
route('logout', 'routes/auth/logout.ts'),
|
route('logout', 'routes/auth/logout.ts'),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Route } from './+types'
|
import type { Route } from './+types'
|
||||||
|
|
||||||
import { userContext } from '@/context'
|
import type { User } from '@repo/auth/auth'
|
||||||
import type { User } from '@/lib/auth'
|
import { userContext } from '@repo/auth/context'
|
||||||
|
|
||||||
export const loader = proxy
|
export const loader = proxy
|
||||||
export const action = proxy
|
export const action = proxy
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { Route } from './+types'
|
import type { Route } from './+types'
|
||||||
|
|
||||||
// import SHA256 from 'crypto-js/sha256'
|
|
||||||
import {
|
import {
|
||||||
Empty,
|
Empty,
|
||||||
EmptyDescription,
|
EmptyDescription,
|
||||||
@@ -8,6 +7,7 @@ import {
|
|||||||
EmptyMedia,
|
EmptyMedia,
|
||||||
EmptyTitle
|
EmptyTitle
|
||||||
} from '@repo/ui/components/ui/empty'
|
} from '@repo/ui/components/ui/empty'
|
||||||
|
import Fuse from 'fuse.js'
|
||||||
import {
|
import {
|
||||||
BanIcon,
|
BanIcon,
|
||||||
CircleCheckIcon,
|
CircleCheckIcon,
|
||||||
@@ -18,18 +18,16 @@ import {
|
|||||||
TimerIcon,
|
TimerIcon,
|
||||||
type LucideIcon
|
type LucideIcon
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
// import lzwCompress from 'lzwcompress'
|
|
||||||
import Fuse from 'fuse.js'
|
|
||||||
import { MeiliSearchFilterBuilder } from 'meilisearch-helper'
|
import { MeiliSearchFilterBuilder } from 'meilisearch-helper'
|
||||||
import { Suspense, useMemo } from 'react'
|
import { Suspense, useMemo } from 'react'
|
||||||
import { Await, useSearchParams } from 'react-router'
|
import { Await, NavLink, useSearchParams } from 'react-router'
|
||||||
|
|
||||||
import placeholder from '@/assets/placeholder.webp'
|
import placeholder from '@/assets/placeholder.webp'
|
||||||
import { createSearch } from '@/lib/meili'
|
|
||||||
|
|
||||||
import { Container } from '@/components/container'
|
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'
|
||||||
import { SearchForm } from '@repo/ui/components/search-form'
|
import { SearchForm } from '@repo/ui/components/search-form'
|
||||||
import { Skeleton } from '@repo/ui/components/skeleton'
|
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||||
@@ -42,27 +40,11 @@ import {
|
|||||||
} from '@repo/ui/components/ui/card'
|
} from '@repo/ui/components/ui/card'
|
||||||
import { Kbd } from '@repo/ui/components/ui/kbd'
|
import { Kbd } from '@repo/ui/components/ui/kbd'
|
||||||
import { Progress } from '@repo/ui/components/ui/progress'
|
import { Progress } from '@repo/ui/components/ui/progress'
|
||||||
|
import { createSearch } from '@repo/util/meili'
|
||||||
export const data = [
|
|
||||||
{
|
|
||||||
id: 'fbad867a-0022-4605-814f-db8ebe2b17fb',
|
|
||||||
courseName: 'All Golf',
|
|
||||||
scormContentPath: 'all-golf-scorm12/shared/launchpage.html'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5ece4b81-2243-4289-9394-b8d853ed0933',
|
|
||||||
courseName: 'CIPA',
|
|
||||||
scormContentPath: 'cipa-pt-1-scorm12/scormdriver/indexAPI.html'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '11ed8481-c6c7-4523-a856-6f7e8bfef022',
|
|
||||||
courseName: 'NR-18 Sinaleiro e Amarrador de Cargas para Içamento',
|
|
||||||
scormContentPath: 'nr-18-sinaleiro-pt-1-scorm12/scormdriver/indexAPI.html'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
type Course = {
|
type Course = {
|
||||||
name: string
|
name: string
|
||||||
|
scormset?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Enrollment = {
|
export type Enrollment = {
|
||||||
@@ -86,13 +68,16 @@ export const loader = async ({ request, context }: Route.ActionArgs) => {
|
|||||||
builder = builder.where('status', 'in', status)
|
builder = builder.where('status', 'in', status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const enrollments = createSearch({
|
||||||
|
index: 'betaeducacao-prod-enrollments',
|
||||||
|
filter: builder.build(),
|
||||||
|
sort: ['created_at:desc'],
|
||||||
|
hitsPerPage: 100,
|
||||||
|
env: context.cloudflare.env
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: createSearch({
|
data: enrollments
|
||||||
index: 'betaeducacao-prod-enrollments',
|
|
||||||
filter: builder.build(),
|
|
||||||
sort: ['created_at:desc'],
|
|
||||||
env: context.cloudflare.env
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,50 +192,18 @@ function List({ term, hits = [] }: { term: string; hits: Enrollment[] }) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function Enrollment({
|
function Enrollment({ id, course, progress }: Enrollment) {
|
||||||
id,
|
|
||||||
course,
|
|
||||||
status,
|
|
||||||
progress
|
|
||||||
// scormContentPath,
|
|
||||||
// courseName
|
|
||||||
}: Enrollment) {
|
|
||||||
// const status_ = statusTranslate[status] ?? status
|
|
||||||
// const { icon: Icon, color } = statusIcon?.[status] ?? defaultIcon
|
|
||||||
// const [mounted, setMounted] = useState(false)
|
|
||||||
// const [progress, setProgress] = useState<number>(0)
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// setMounted(true)
|
|
||||||
// const hash = SHA256(scormContentPath).toString()
|
|
||||||
// const stored = localStorage.getItem(`scormState.${hash}`)
|
|
||||||
// if (stored) {
|
|
||||||
// try {
|
|
||||||
// const scormState = JSON.parse(stored)
|
|
||||||
// const suspendData = JSON.parse(scormState?.cmi?.suspend_data || '{}')
|
|
||||||
// const d = lzwCompress.unpack(suspendData?.d)
|
|
||||||
// const data = JSON.parse(d || '{}')
|
|
||||||
// setProgress(data?.progress?.p ?? null)
|
|
||||||
// } catch (err) {
|
|
||||||
// console.warn('Erro ao carregar progresso:', err)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }, [scormContentPath])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a href={`/player/${id}`} className="hover:scale-105 transition">
|
<NavLink
|
||||||
|
to={course?.scormset ? `/player/${id}` : '/konviva'}
|
||||||
|
className="hover:scale-105 transition"
|
||||||
|
>
|
||||||
<Card className="overflow-hidden relative h-96">
|
<Card className="overflow-hidden relative h-96">
|
||||||
<CardHeader className="z-1 relative">
|
<CardHeader className="z-1 relative">
|
||||||
<CardTitle className="text-xl/6">{course.name}</CardTitle>
|
<CardTitle className="text-xl/6">{course.name}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="z-1">
|
<CardContent className="z-1"></CardContent>
|
||||||
{/* Você pode adicionar algo como tempo total aqui, se quiser */}
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="absolute z-1 bottom-6 w-full flex gap-1.5">
|
<CardFooter className="absolute z-1 bottom-6 w-full flex gap-1.5">
|
||||||
{/* <Badge variant="secondary" className={color}>
|
|
||||||
<Icon className="stroke-2" />
|
|
||||||
{status_}
|
|
||||||
</Badge>*/}
|
|
||||||
<Progress value={progress} />
|
<Progress value={progress} />
|
||||||
<span className="text-xs">{progress}%</span>
|
<span className="text-xs">{progress}%</span>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
@@ -260,7 +213,7 @@ function Enrollment({
|
|||||||
className="absolute bottom-0 opacity-75"
|
className="absolute bottom-0 opacity-75"
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</a>
|
</NavLink>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,7 +233,7 @@ const statuses: Record<
|
|||||||
COMPLETED: {
|
COMPLETED: {
|
||||||
icon: CircleCheckIcon,
|
icon: CircleCheckIcon,
|
||||||
color: 'text-green-400 [&_svg]:text-background [&_svg]:fill-green-500',
|
color: 'text-green-400 [&_svg]:text-background [&_svg]:fill-green-500',
|
||||||
label: 'Aprovado'
|
label: 'Concluído'
|
||||||
},
|
},
|
||||||
FAILED: {
|
FAILED: {
|
||||||
icon: CircleXIcon,
|
icon: CircleXIcon,
|
||||||
|
|||||||
6
apps/saladeaula.digital/app/routes/konviva.ts
Normal file
6
apps/saladeaula.digital/app/routes/konviva.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { redirect } from 'react-router'
|
||||||
|
import type { Route } from './+types'
|
||||||
|
|
||||||
|
export async function loader({ params, context }: Route.LoaderArgs) {
|
||||||
|
return redirect('https://lms.saladeaula.digital')
|
||||||
|
}
|
||||||
@@ -57,7 +57,7 @@ export default function Component({ loaderData }: Route.ComponentProps) {
|
|||||||
>
|
>
|
||||||
<div className="container mx-auto flex items-center">
|
<div className="container mx-auto flex items-center">
|
||||||
{/* Desktop Menu */}
|
{/* Desktop Menu */}
|
||||||
<div className="hidden lg:flex gap-8">
|
<div className="hidden lg:flex items-center gap-8">
|
||||||
<Link to="/">
|
<Link to="/">
|
||||||
<ThemedImage />
|
<ThemedImage />
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import type { Route } from './+types'
|
import type { Route } from './+types'
|
||||||
|
|
||||||
import { Link } from 'react-router'
|
import { MeiliSearchFilterBuilder } from 'meilisearch-helper'
|
||||||
|
import { Await, Link } from 'react-router'
|
||||||
|
|
||||||
|
import { type User } from '@repo/auth/auth'
|
||||||
|
import { userContext } from '@repo/auth/context'
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
@@ -10,39 +13,97 @@ import {
|
|||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
BreadcrumbSeparator
|
BreadcrumbSeparator
|
||||||
} from '@repo/ui/components/ui/breadcrumb'
|
} from '@repo/ui/components/ui/breadcrumb'
|
||||||
|
import { createSearch } from '@repo/util/meili'
|
||||||
|
|
||||||
import { Container } from '@/components/container'
|
import { Container } from '@/components/container'
|
||||||
|
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||||
|
import { Card, CardContent } from '@repo/ui/components/ui/card'
|
||||||
|
import { Suspense } from 'react'
|
||||||
|
|
||||||
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 const loader = async ({ request, context }: Route.ActionArgs) => {
|
||||||
|
const user = context.get(userContext) as User
|
||||||
|
const { searchParams } = new URL(request.url)
|
||||||
|
const status = searchParams.getAll('status') || []
|
||||||
|
let builder = new MeiliSearchFilterBuilder().where('user_id', '=', user.sub)
|
||||||
|
|
||||||
|
if (status.length) {
|
||||||
|
builder = builder.where('status', 'in', status)
|
||||||
|
}
|
||||||
|
|
||||||
|
const payments = createSearch({
|
||||||
|
index: 'betaeducacao-prod-orders',
|
||||||
|
filter: builder.build(),
|
||||||
|
sort: ['create_date:desc'],
|
||||||
|
env: context.cloudflare.env
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: payments
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Component({ loaderData: { data } }) {
|
||||||
return (
|
return (
|
||||||
<Container className="space-y-2.5">
|
<Container className="space-y-2.5">
|
||||||
<Breadcrumb>
|
<Suspense fallback={<Skeleton />}>
|
||||||
<BreadcrumbList>
|
<Await resolve={data}>
|
||||||
<BreadcrumbItem>
|
{({ hits }) => (
|
||||||
<BreadcrumbLink asChild>
|
<>
|
||||||
<Link to="..">Meus cursos</Link>
|
<Breadcrumb>
|
||||||
</BreadcrumbLink>
|
<BreadcrumbList>
|
||||||
</BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<BreadcrumbSeparator />
|
<BreadcrumbLink asChild>
|
||||||
<BreadcrumbItem>
|
<Link to="..">Meus cursos</Link>
|
||||||
<BreadcrumbPage>Histórico de compras</BreadcrumbPage>
|
</BreadcrumbLink>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
</BreadcrumbList>
|
<BreadcrumbSeparator />
|
||||||
</Breadcrumb>
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbPage>Histórico de compras</BreadcrumbPage>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</BreadcrumbList>
|
||||||
|
</Breadcrumb>
|
||||||
|
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5 mb-8">
|
||||||
<h1 className="text-2xl font-bold tracking-tight">
|
<h1 className="text-2xl font-bold tracking-tight">
|
||||||
Histórico de compras
|
Histórico de compras
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Acompanhe todos as compras realizadas, visualize pagamentos e mantenha
|
Acompanhe todos as compras realizadas, visualize pagamentos e
|
||||||
o controle financeiro.
|
mantenha o controle financeiro.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<table className="table-fixed">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Forma de pag.</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Total</th>
|
||||||
|
<th>Comprador em</th>
|
||||||
|
<th>Vencimento em</th>
|
||||||
|
</tr>
|
||||||
|
{hits.map(({ payment_method, status }) => {
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td>{payment_method}</td>
|
||||||
|
<td>{status}</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Await>
|
||||||
|
</Suspense>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,87 @@
|
|||||||
import type { Route } from './+types'
|
import type { Route } from './+types'
|
||||||
|
|
||||||
import { ScormPlayer } from '@/components/scorm-player'
|
import lzwCompress from 'lzwcompress'
|
||||||
import { useLocalStorage } from '@/hooks/useLocalStorage'
|
import { useBlocker, useFetcher } from 'react-router'
|
||||||
import SHA256 from 'crypto-js/sha256'
|
|
||||||
|
import { HttpMethod, request as req } from '@repo/util/request'
|
||||||
|
|
||||||
|
import { ScormPlayer, type ScormVersion } from '@/components/scorm-player'
|
||||||
|
// import { useLocalStorage } from '@/hooks/useLocalStorage'
|
||||||
|
// import SHA256 from 'crypto-js/sha256'
|
||||||
|
|
||||||
export function meta({ params }: Route.MetaArgs) {
|
export function meta({ params }: Route.MetaArgs) {
|
||||||
return [{ title: '' }]
|
return [{ title: '' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Route({ params }: Route.ComponentProps) {
|
export const loader = async ({
|
||||||
|
params,
|
||||||
|
request,
|
||||||
|
context
|
||||||
|
}: Route.ActionArgs) => {
|
||||||
|
const { id } = params
|
||||||
|
const r = await req({
|
||||||
|
url: `/enrollments/${id}/scorm`,
|
||||||
|
request,
|
||||||
|
context
|
||||||
|
})
|
||||||
|
return { data: await r.json() }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function action({ params, request, context }: Route.ActionArgs) {
|
||||||
|
const { id } = params
|
||||||
|
const body = JSON.stringify(await request.json())
|
||||||
|
|
||||||
|
console.log(body)
|
||||||
|
|
||||||
|
await req({
|
||||||
|
url: `/enrollments/${id}`,
|
||||||
|
method: HttpMethod.POST,
|
||||||
|
body,
|
||||||
|
headers: new Headers({ 'Content-Type': 'application/json' }),
|
||||||
|
request,
|
||||||
|
context
|
||||||
|
})
|
||||||
|
console.log(body)
|
||||||
|
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Route({ loaderData: { data } }: Route.ComponentProps) {
|
||||||
|
const fetcher = useFetcher()
|
||||||
const course = {
|
const course = {
|
||||||
id: 'fbad867a-0022-4605-814f-db8ebe2b17fb',
|
id: 'fbad867a-0022-4605-814f-db8ebe2b17fb',
|
||||||
courseName: 'All Golf',
|
// courseName: 'All Golf',
|
||||||
scormContentPath:
|
scormContentPath:
|
||||||
'nr-33-espacos-confinados-conteudo-de-demonstracao-scorm12/scormdriver/indexAPI.html'
|
// 'nr-33-espacos-confinados-conteudo-de-demonstracao-scorm12/scormdriver/indexAPI.html'
|
||||||
|
// 'cipa-pt-1-scorm12/scormdriver/indexAPI.html'
|
||||||
|
'test-scorm2004_4/scormdriver/indexAPI.html'
|
||||||
}
|
}
|
||||||
// const course = data.find((course) => course.id === params.course)
|
|
||||||
const hash = SHA256(course.scormContentPath).toString()
|
const scormState = data?.['last_commit']?.cmi || {}
|
||||||
const [scormState] = useLocalStorage(`scormState.${hash}`, {})
|
// const suspendData = JSON.parse(scormState?.cmi?.suspend_data || '{}')
|
||||||
|
// const d = lzwCompress.unpack(suspendData?.d)
|
||||||
|
// const d2 = JSON.parse(d || '{}')
|
||||||
|
// console.log(d2?.progress?.p ?? null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScormPlayer
|
<ScormPlayer
|
||||||
|
settings={{
|
||||||
|
autocommit: true,
|
||||||
|
throwExceptions: false,
|
||||||
|
logLevel: 2,
|
||||||
|
compatibilityMode: 1
|
||||||
|
}}
|
||||||
|
scormVersion="2004"
|
||||||
scormState={scormState}
|
scormState={scormState}
|
||||||
scormContentPath={course.scormContentPath}
|
scormContentPath={course.scormContentPath}
|
||||||
className="w-full h-full border-0"
|
className="w-full h-full border-0"
|
||||||
|
onCommit={async (data) => {
|
||||||
|
console.log(data)
|
||||||
|
await fetcher.submit(JSON.stringify(data), {
|
||||||
|
method: 'post',
|
||||||
|
encType: 'application/json'
|
||||||
|
})
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { Route } from './+types'
|
import type { Route } from './+types'
|
||||||
|
|
||||||
|
import { useForm } from 'react-hook-form'
|
||||||
import { Link } from 'react-router'
|
import { Link } from 'react-router'
|
||||||
|
|
||||||
import { request as req } from '@/lib/request'
|
|
||||||
|
|
||||||
import { Container } from '@/components/container'
|
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 {
|
||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
} 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 { Spinner } from '@repo/ui/components/ui/spinner'
|
import { Spinner } from '@repo/ui/components/ui/spinner'
|
||||||
import { useForm } from 'react-hook-form'
|
import { request as req } from '@repo/util/request'
|
||||||
|
|
||||||
export function meta({}: Route.MetaArgs) {
|
export function meta({}: Route.MetaArgs) {
|
||||||
return [{ title: 'Minha conta' }]
|
return [{ title: 'Minha conta' }]
|
||||||
|
|||||||
@@ -14,13 +14,15 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@repo/auth": "*",
|
"@repo/auth": "*",
|
||||||
"@repo/ui": "*",
|
"@repo/ui": "*",
|
||||||
|
"@repo/util": "*",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"meilisearch-helper": "github:sergiors/meilisearch-helper",
|
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"isbot": "^5.1.31",
|
"isbot": "^5.1.31",
|
||||||
"lzwcompress": "^1.1.0",
|
"lzwcompress": "^1.1.0",
|
||||||
"meilisearch": "^0.54.0",
|
"meilisearch": "^0.54.0",
|
||||||
|
"meilisearch-helper": "github:sergiors/meilisearch-helper",
|
||||||
|
"ramda": "^0.32.0",
|
||||||
"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",
|
||||||
@@ -29,11 +31,12 @@
|
|||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-router/dev": "^7.9.5",
|
|
||||||
"@cloudflare/vite-plugin": "^1.13.17",
|
"@cloudflare/vite-plugin": "^1.13.17",
|
||||||
|
"@react-router/dev": "^7.9.5",
|
||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/node": "^24",
|
"@types/node": "^24",
|
||||||
|
"@types/ramda": "^0.31.1",
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.2",
|
||||||
"@types/react-dom": "^19.2.2",
|
"@types/react-dom": "^19.2.2",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
import { Meilisearch, type SearchResponse } from 'meilisearch'
|
import { Meilisearch, type SearchResponse } from 'meilisearch'
|
||||||
|
|
||||||
|
const MAX_HITS_PER_PAGE = 100
|
||||||
|
|
||||||
export async function createSearch({
|
export async function createSearch({
|
||||||
query,
|
query,
|
||||||
filter = undefined,
|
filter = undefined,
|
||||||
index,
|
index,
|
||||||
|
page,
|
||||||
|
hitsPerPage,
|
||||||
sort,
|
sort,
|
||||||
env
|
env
|
||||||
}: {
|
}: {
|
||||||
query?: string
|
query?: string
|
||||||
filter?: string
|
filter?: string
|
||||||
index: string
|
index: string
|
||||||
|
page?: number
|
||||||
|
hitsPerPage: number
|
||||||
sort: string[]
|
sort: string[]
|
||||||
env: Env
|
env: Env
|
||||||
}): Promise<SearchResponse> {
|
}): Promise<SearchResponse> {
|
||||||
@@ -21,6 +27,8 @@ export async function createSearch({
|
|||||||
return index_.search(query, {
|
return index_.search(query, {
|
||||||
sort,
|
sort,
|
||||||
filter,
|
filter,
|
||||||
limit: 100
|
page,
|
||||||
|
hitsPerPage:
|
||||||
|
hitsPerPage > MAX_HITS_PER_PAGE ? MAX_HITS_PER_PAGE : hitsPerPage
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,9 +31,15 @@ export function request({
|
|||||||
const requestId = context.get(requestIdContext) as string
|
const requestId = context.get(requestIdContext) as string
|
||||||
const user = context.get(userContext) as User
|
const user = context.get(userContext) as User
|
||||||
const url_ = new URL(url, context.cloudflare.env.API_URL)
|
const url_ = new URL(url, context.cloudflare.env.API_URL)
|
||||||
const headers = new Headers(
|
const headers = new Headers({
|
||||||
Object.assign({ Authorization: `Bearer ${user.accessToken}` }, _headers)
|
Authorization: `Bearer ${user.accessToken}`
|
||||||
)
|
})
|
||||||
|
|
||||||
|
if (_headers instanceof Headers) {
|
||||||
|
_headers.forEach((value, key) => headers.set(key, value))
|
||||||
|
} else {
|
||||||
|
Object.entries(_headers).forEach(([key, value]) => headers.set(key, value))
|
||||||
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[${new Date().toISOString()}] [${requestId}] ${method} ${url_.toString()}`
|
`[${new Date().toISOString()}] [${requestId}] ${method} ${url_.toString()}`
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -138,7 +138,10 @@ function Course({ id, name, access_period, cert, draft }: Course) {
|
|||||||
|
|
||||||
<CardHeader className="z-1 relative">
|
<CardHeader className="z-1 relative">
|
||||||
<CardTitle className="text-xl/6">
|
<CardTitle className="text-xl/6">
|
||||||
{name} {draft ? <>(rascunho)</> : null}
|
{name}{' '}
|
||||||
|
{draft ? (
|
||||||
|
<span className="text-muted-foreground">(rascunho)</span>
|
||||||
|
) : null}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
from http.cookies import SimpleCookie
|
|
||||||
|
|
||||||
from authlib.oauth2.rfc6749 import errors
|
from authlib.oauth2.rfc6749 import errors
|
||||||
from authlib.oauth2.rfc6749.util import scope_to_list
|
from authlib.oauth2.rfc6749.util import scope_to_list
|
||||||
from aws_lambda_powertools import Logger
|
from aws_lambda_powertools import Logger
|
||||||
@@ -15,6 +13,7 @@ from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey
|
|||||||
from boto3clients import dynamodb_client
|
from boto3clients import dynamodb_client
|
||||||
from config import OAUTH2_TABLE
|
from config import OAUTH2_TABLE
|
||||||
from oauth2 import server
|
from oauth2 import server
|
||||||
|
from util import parse_cookies
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
logger = Logger(__name__)
|
logger = Logger(__name__)
|
||||||
@@ -24,7 +23,7 @@ dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
|
|||||||
@router.get('/authorize')
|
@router.get('/authorize')
|
||||||
def authorize():
|
def authorize():
|
||||||
current_event = router.current_event
|
current_event = router.current_event
|
||||||
cookies = _parse_cookies(current_event.get('cookies', []))
|
cookies = parse_cookies(current_event.get('cookies', []))
|
||||||
session = cookies.get('__session')
|
session = cookies.get('__session')
|
||||||
|
|
||||||
if not session:
|
if not session:
|
||||||
@@ -80,18 +79,4 @@ def _user_scopes(sub: str) -> set:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _parse_cookies(cookies: list[str] | None) -> dict[str, str]:
|
|
||||||
parsed_cookies = {}
|
|
||||||
|
|
||||||
if not cookies:
|
|
||||||
return parsed_cookies
|
|
||||||
|
|
||||||
for s in cookies:
|
|
||||||
c = SimpleCookie()
|
|
||||||
c.load(s)
|
|
||||||
parsed_cookies.update({k: morsel.value for k, morsel in c.items()})
|
|
||||||
|
|
||||||
return parsed_cookies
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidSession(BadRequestError): ...
|
class InvalidSession(BadRequestError): ...
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
from http.cookies import SimpleCookie
|
||||||
|
|
||||||
ROOT = os.path.abspath(os.path.dirname(__file__))
|
ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
@@ -10,3 +11,17 @@ def get_file_path(name: str) -> str:
|
|||||||
def read_file_path(name: str) -> str:
|
def read_file_path(name: str) -> str:
|
||||||
with open(get_file_path(name)) as f:
|
with open(get_file_path(name)) as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_cookies(cookies: list[str] | None) -> dict[str, str]:
|
||||||
|
parsed_cookies = {}
|
||||||
|
|
||||||
|
if not cookies:
|
||||||
|
return parsed_cookies
|
||||||
|
|
||||||
|
for s in cookies:
|
||||||
|
c = SimpleCookie()
|
||||||
|
c.load(s)
|
||||||
|
parsed_cookies.update({k: morsel.value for k, morsel in c.items()})
|
||||||
|
|
||||||
|
return parsed_cookies
|
||||||
|
|||||||
4
konviva-events/uv.lock
generated
4
konviva-events/uv.lock
generated
@@ -1,5 +1,5 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 2
|
revision = 3
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -497,7 +497,7 @@ dev = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "layercake"
|
name = "layercake"
|
||||||
version = "0.11.0"
|
version = "0.11.1"
|
||||||
source = { directory = "../layercake" }
|
source = { directory = "../layercake" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "arnparse" },
|
{ name = "arnparse" },
|
||||||
|
|||||||
70
package-lock.json
generated
70
package-lock.json
generated
@@ -28,6 +28,7 @@
|
|||||||
"@react-router/fs-routes": "^7.9.5",
|
"@react-router/fs-routes": "^7.9.5",
|
||||||
"@repo/auth": "*",
|
"@repo/auth": "*",
|
||||||
"@repo/ui": "*",
|
"@repo/ui": "*",
|
||||||
|
"@repo/util": "^0.0.0",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"cookie": "^1.0.2",
|
"cookie": "^1.0.2",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
@@ -156,6 +157,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@repo/auth": "*",
|
"@repo/auth": "*",
|
||||||
"@repo/ui": "*",
|
"@repo/ui": "*",
|
||||||
|
"@repo/util": "*",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
@@ -163,6 +165,7 @@
|
|||||||
"lzwcompress": "^1.1.0",
|
"lzwcompress": "^1.1.0",
|
||||||
"meilisearch": "^0.54.0",
|
"meilisearch": "^0.54.0",
|
||||||
"meilisearch-helper": "github:sergiors/meilisearch-helper",
|
"meilisearch-helper": "github:sergiors/meilisearch-helper",
|
||||||
|
"ramda": "^0.32.0",
|
||||||
"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",
|
||||||
@@ -176,6 +179,7 @@
|
|||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/node": "^24",
|
"@types/node": "^24",
|
||||||
|
"@types/ramda": "^0.31.1",
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.2",
|
||||||
"@types/react-dom": "^19.2.2",
|
"@types/react-dom": "^19.2.2",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
@@ -4087,6 +4091,10 @@
|
|||||||
"resolved": "packages/ui",
|
"resolved": "packages/ui",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@repo/util": {
|
||||||
|
"resolved": "packages/util",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.52.5",
|
"version": "4.52.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
|
||||||
@@ -4771,6 +4779,16 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/ramda": {
|
||||||
|
"version": "0.31.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.31.1.tgz",
|
||||||
|
"integrity": "sha512-Vt6sFXnuRpzaEj+yeutA0q3bcAsK7wdPuASIzR9LXqL4gJPyFw8im9qchlbp4ltuf3kDEIRmPJTD/Fkg60dn7g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"types-ramda": "^0.31.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.2.2",
|
"version": "19.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||||
@@ -6373,6 +6391,16 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ramda": {
|
||||||
|
"version": "0.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.32.0.tgz",
|
||||||
|
"integrity": "sha512-GQWAHhxhxWBWA8oIBr1XahFVjQ9Fic6MK9ikijfd4TZHfE2+urfk+irVlR5VOn48uwMgM+loRRBJd6Yjsbc0zQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/ramda"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "19.2.0",
|
"version": "19.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||||
@@ -7062,6 +7090,13 @@
|
|||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ts-toolbelt": {
|
||||||
|
"version": "9.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz",
|
||||||
|
"integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/tsconfck": {
|
"node_modules/tsconfck": {
|
||||||
"version": "3.1.6",
|
"version": "3.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz",
|
||||||
@@ -7200,6 +7235,16 @@
|
|||||||
"url": "https://github.com/sponsors/Wombosvideo"
|
"url": "https://github.com/sponsors/Wombosvideo"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/types-ramda": {
|
||||||
|
"version": "0.31.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/types-ramda/-/types-ramda-0.31.0.tgz",
|
||||||
|
"integrity": "sha512-vaoC35CRC3xvL8Z6HkshDbi6KWM1ezK0LHN0YyxXWUn9HKzBNg/T3xSGlJZjCYspnOD3jE7bcizsp0bUXZDxnQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ts-toolbelt": "^9.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.2",
|
"version": "5.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||||
@@ -8282,6 +8327,31 @@
|
|||||||
"vite": "^7.2.0",
|
"vite": "^7.2.0",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"packages/util": {
|
||||||
|
"name": "@repo/util",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@repo/auth": "*",
|
||||||
|
"@types/node": "^24.9.2",
|
||||||
|
"@types/react": "^19.2.2",
|
||||||
|
"@types/react-dom": "^19.2.2",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/util/node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export function ModeToggle() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ThemedImageProps = {
|
type ThemedImageProps = {
|
||||||
children?: string
|
children?: string | React.ReactNode
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
15
packages/util/package.json
Normal file
15
packages/util/package.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "@repo/util",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"exports": {
|
||||||
|
"./*": "./src/*.ts"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@repo/auth": "*",
|
||||||
|
"@types/node": "^24.9.2",
|
||||||
|
"@types/react": "^19.2.2",
|
||||||
|
"@types/react-dom": "^19.2.2",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ export async function createSearch({
|
|||||||
filter = undefined,
|
filter = undefined,
|
||||||
index,
|
index,
|
||||||
page,
|
page,
|
||||||
hitsPerPage,
|
hitsPerPage = 25,
|
||||||
sort,
|
sort,
|
||||||
env
|
env
|
||||||
}: {
|
}: {
|
||||||
@@ -15,9 +15,9 @@ export async function createSearch({
|
|||||||
filter?: string
|
filter?: string
|
||||||
index: string
|
index: string
|
||||||
page?: number
|
page?: number
|
||||||
hitsPerPage: number
|
hitsPerPage?: number
|
||||||
sort: string[]
|
sort: string[]
|
||||||
env: Env
|
env: any
|
||||||
}): Promise<SearchResponse> {
|
}): Promise<SearchResponse> {
|
||||||
const host = env.MEILI_HOST
|
const host = env.MEILI_HOST
|
||||||
const apiKey = env.MEILI_API_KEY
|
const apiKey = env.MEILI_API_KEY
|
||||||
11
packages/util/tsconfig.json
Normal file
11
packages/util/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user