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):
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -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('/<enrollment_id>')
|
||||
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('/<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,
|
||||
)
|
||||
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
|
||||
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']
|
||||
|
||||
@@ -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"}
|
||||
{"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 (
|
||||
<SidebarMenuItem key={title} onClick={onToggle}>
|
||||
<NavLink to={`/${orgid}${url}`}>
|
||||
{({ isActive }) => (
|
||||
{({ isActive, isPending }) => (
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
className="data-[active=true]:text-lime-500"
|
||||
@@ -72,7 +72,7 @@ function SidebarMenuItemLink({ title, url, icon: Icon }: NavItem) {
|
||||
tooltip={title}
|
||||
>
|
||||
<span>
|
||||
{Icon && <Icon />}
|
||||
{Icon ? <Icon /> : null}
|
||||
<span>{title}</span>
|
||||
</span>
|
||||
</SidebarMenuButton>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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' }]
|
||||
}
|
||||
|
||||
@@ -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 } }) {
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Email principal</CardTitle>
|
||||
<CardDescription>
|
||||
<Kbd className="font-mono border">{user.email}</Kbd> será
|
||||
usado para mensagens e pode ser usado para redefinições de
|
||||
senha.
|
||||
<Kbd className="font-mono border">
|
||||
<span className="truncate max-lg:max-w-62">
|
||||
{user.email}
|
||||
</span>
|
||||
</Kbd>{' '}
|
||||
será usado para mensagens e pode ser usado para redefinições
|
||||
de senha.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
||||
@@ -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({
|
||||
|
||||
<ul>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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(' ')
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ type ContainerProps = {
|
||||
|
||||
export function Container({ children, className }: ContainerProps) {
|
||||
return (
|
||||
<main className="px-4">
|
||||
<main className="p-4">
|
||||
<div className={cn('container mx-auto', className)}>{children}</div>
|
||||
</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'
|
||||
|
||||
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<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({
|
||||
settings,
|
||||
scormVersion,
|
||||
scormState,
|
||||
scormContentPath,
|
||||
className
|
||||
}: {
|
||||
scormState: object
|
||||
scormContentPath: string
|
||||
className: string
|
||||
}) {
|
||||
className,
|
||||
onCommit
|
||||
}: ScormPlayerProps) {
|
||||
const [iframeLoaded, setIframeLoaded] = useState(false)
|
||||
const scormApiRef = useRef<Scorm12API | null>(null)
|
||||
const hash = SHA256(scormContentPath).toString()
|
||||
const [_, setScormState] = useLocalStorage(`scormState.${hash}`, {})
|
||||
const scormApiRef = useRef<Scorm12API | Scorm2004API | null>(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)
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
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('payments', 'routes/payments.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('logout', 'routes/auth/logout.ts'),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Route } from './+types'
|
||||
|
||||
import { userContext } from '@/context'
|
||||
import type { User } from '@/lib/auth'
|
||||
import type { User } from '@repo/auth/auth'
|
||||
import { userContext } from '@repo/auth/context'
|
||||
|
||||
export const loader = proxy
|
||||
export const action = proxy
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Route } from './+types'
|
||||
|
||||
// import SHA256 from 'crypto-js/sha256'
|
||||
import {
|
||||
Empty,
|
||||
EmptyDescription,
|
||||
@@ -8,6 +7,7 @@ import {
|
||||
EmptyMedia,
|
||||
EmptyTitle
|
||||
} from '@repo/ui/components/ui/empty'
|
||||
import Fuse from 'fuse.js'
|
||||
import {
|
||||
BanIcon,
|
||||
CircleCheckIcon,
|
||||
@@ -18,18 +18,16 @@ import {
|
||||
TimerIcon,
|
||||
type LucideIcon
|
||||
} from 'lucide-react'
|
||||
// import lzwCompress from 'lzwcompress'
|
||||
import Fuse from 'fuse.js'
|
||||
import { MeiliSearchFilterBuilder } from 'meilisearch-helper'
|
||||
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 { createSearch } from '@/lib/meili'
|
||||
|
||||
import { Container } from '@/components/container'
|
||||
|
||||
import type { User } from '@repo/auth/auth'
|
||||
import { userContext } from '@repo/auth/context'
|
||||
|
||||
import { FacetedFilter } from '@repo/ui/components/faceted-filter'
|
||||
import { SearchForm } from '@repo/ui/components/search-form'
|
||||
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||
@@ -42,27 +40,11 @@ import {
|
||||
} from '@repo/ui/components/ui/card'
|
||||
import { Kbd } from '@repo/ui/components/ui/kbd'
|
||||
import { Progress } from '@repo/ui/components/ui/progress'
|
||||
|
||||
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'
|
||||
}
|
||||
]
|
||||
import { createSearch } from '@repo/util/meili'
|
||||
|
||||
type Course = {
|
||||
name: string
|
||||
scormset?: string
|
||||
}
|
||||
|
||||
export type Enrollment = {
|
||||
@@ -86,13 +68,16 @@ export const loader = async ({ request, context }: Route.ActionArgs) => {
|
||||
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 {
|
||||
data: createSearch({
|
||||
index: 'betaeducacao-prod-enrollments',
|
||||
filter: builder.build(),
|
||||
sort: ['created_at:desc'],
|
||||
env: context.cloudflare.env
|
||||
})
|
||||
data: enrollments
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,50 +192,18 @@ function List({ term, hits = [] }: { term: string; hits: Enrollment[] }) {
|
||||
})
|
||||
}
|
||||
|
||||
function 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])
|
||||
|
||||
function Enrollment({ id, course, progress }: Enrollment) {
|
||||
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">
|
||||
<CardHeader className="z-1 relative">
|
||||
<CardTitle className="text-xl/6">{course.name}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="z-1">
|
||||
{/* Você pode adicionar algo como tempo total aqui, se quiser */}
|
||||
</CardContent>
|
||||
<CardContent className="z-1"></CardContent>
|
||||
<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} />
|
||||
<span className="text-xs">{progress}%</span>
|
||||
</CardFooter>
|
||||
@@ -260,7 +213,7 @@ function Enrollment({
|
||||
className="absolute bottom-0 opacity-75"
|
||||
/>
|
||||
</Card>
|
||||
</a>
|
||||
</NavLink>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -280,7 +233,7 @@ const statuses: Record<
|
||||
COMPLETED: {
|
||||
icon: CircleCheckIcon,
|
||||
color: 'text-green-400 [&_svg]:text-background [&_svg]:fill-green-500',
|
||||
label: 'Aprovado'
|
||||
label: 'Concluído'
|
||||
},
|
||||
FAILED: {
|
||||
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">
|
||||
{/* Desktop Menu */}
|
||||
<div className="hidden lg:flex gap-8">
|
||||
<div className="hidden lg:flex items-center gap-8">
|
||||
<Link to="/">
|
||||
<ThemedImage />
|
||||
</Link>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
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 {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
@@ -10,39 +13,97 @@ import {
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator
|
||||
} from '@repo/ui/components/ui/breadcrumb'
|
||||
import { createSearch } from '@repo/util/meili'
|
||||
|
||||
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) {
|
||||
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 (
|
||||
<Container className="space-y-2.5">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link to="..">Meus cursos</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Histórico de compras</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
<Await resolve={data}>
|
||||
{({ hits }) => (
|
||||
<>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link to="..">Meus cursos</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Histórico de compras</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
|
||||
<div className="space-y-0.5">
|
||||
<h1 className="text-2xl font-bold tracking-tight">
|
||||
Histórico de compras
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Acompanhe todos as compras realizadas, visualize pagamentos e mantenha
|
||||
o controle financeiro.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-0.5 mb-8">
|
||||
<h1 className="text-2xl font-bold tracking-tight">
|
||||
Histórico de compras
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Acompanhe todos as compras realizadas, visualize pagamentos e
|
||||
mantenha o controle financeiro.
|
||||
</p>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,29 +1,87 @@
|
||||
import type { Route } from './+types'
|
||||
|
||||
import { ScormPlayer } from '@/components/scorm-player'
|
||||
import { useLocalStorage } from '@/hooks/useLocalStorage'
|
||||
import SHA256 from 'crypto-js/sha256'
|
||||
import lzwCompress from 'lzwcompress'
|
||||
import { useBlocker, useFetcher } from 'react-router'
|
||||
|
||||
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) {
|
||||
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 = {
|
||||
id: 'fbad867a-0022-4605-814f-db8ebe2b17fb',
|
||||
courseName: 'All Golf',
|
||||
// courseName: 'All Golf',
|
||||
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] = useLocalStorage(`scormState.${hash}`, {})
|
||||
|
||||
const scormState = data?.['last_commit']?.cmi || {}
|
||||
// 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 (
|
||||
<ScormPlayer
|
||||
settings={{
|
||||
autocommit: true,
|
||||
throwExceptions: false,
|
||||
logLevel: 2,
|
||||
compatibilityMode: 1
|
||||
}}
|
||||
scormVersion="2004"
|
||||
scormState={scormState}
|
||||
scormContentPath={course.scormContentPath}
|
||||
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 { useForm } from 'react-hook-form'
|
||||
import { Link } from 'react-router'
|
||||
|
||||
import { request as req } from '@/lib/request'
|
||||
|
||||
import { Container } from '@/components/container'
|
||||
|
||||
import type { User } from '@repo/auth/auth'
|
||||
import { userContext } from '@repo/auth/context'
|
||||
import {
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
} from '@repo/ui/components/ui/form'
|
||||
import { Input } from '@repo/ui/components/ui/input'
|
||||
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) {
|
||||
return [{ title: 'Minha conta' }]
|
||||
|
||||
@@ -14,13 +14,15 @@
|
||||
"dependencies": {
|
||||
"@repo/auth": "*",
|
||||
"@repo/ui": "*",
|
||||
"@repo/util": "*",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"meilisearch-helper": "github:sergiors/meilisearch-helper",
|
||||
"crypto-js": "^4.2.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
"isbot": "^5.1.31",
|
||||
"lzwcompress": "^1.1.0",
|
||||
"meilisearch": "^0.54.0",
|
||||
"meilisearch-helper": "github:sergiors/meilisearch-helper",
|
||||
"ramda": "^0.32.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router": "^7.9.5",
|
||||
@@ -29,11 +31,12 @@
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-router/dev": "^7.9.5",
|
||||
"@cloudflare/vite-plugin": "^1.13.17",
|
||||
"@react-router/dev": "^7.9.5",
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/node": "^24",
|
||||
"@types/ramda": "^0.31.1",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"prettier": "^3.6.2",
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { Meilisearch, type SearchResponse } from 'meilisearch'
|
||||
|
||||
const MAX_HITS_PER_PAGE = 100
|
||||
|
||||
export async function createSearch({
|
||||
query,
|
||||
filter = undefined,
|
||||
index,
|
||||
page,
|
||||
hitsPerPage,
|
||||
sort,
|
||||
env
|
||||
}: {
|
||||
query?: string
|
||||
filter?: string
|
||||
index: string
|
||||
page?: number
|
||||
hitsPerPage: number
|
||||
sort: string[]
|
||||
env: Env
|
||||
}): Promise<SearchResponse> {
|
||||
@@ -21,6 +27,8 @@ export async function createSearch({
|
||||
return index_.search(query, {
|
||||
sort,
|
||||
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 user = context.get(userContext) as User
|
||||
const url_ = new URL(url, context.cloudflare.env.API_URL)
|
||||
const headers = new Headers(
|
||||
Object.assign({ Authorization: `Bearer ${user.accessToken}` }, _headers)
|
||||
)
|
||||
const headers = new 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(
|
||||
`[${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">
|
||||
<CardTitle className="text-xl/6">
|
||||
{name} {draft ? <>(rascunho)</> : null}
|
||||
{name}{' '}
|
||||
{draft ? (
|
||||
<span className="text-muted-foreground">(rascunho)</span>
|
||||
) : null}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from http.cookies import SimpleCookie
|
||||
|
||||
from authlib.oauth2.rfc6749 import errors
|
||||
from authlib.oauth2.rfc6749.util import scope_to_list
|
||||
from aws_lambda_powertools import Logger
|
||||
@@ -15,6 +13,7 @@ from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey
|
||||
from boto3clients import dynamodb_client
|
||||
from config import OAUTH2_TABLE
|
||||
from oauth2 import server
|
||||
from util import parse_cookies
|
||||
|
||||
router = Router()
|
||||
logger = Logger(__name__)
|
||||
@@ -24,7 +23,7 @@ dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
|
||||
@router.get('/authorize')
|
||||
def authorize():
|
||||
current_event = router.current_event
|
||||
cookies = _parse_cookies(current_event.get('cookies', []))
|
||||
cookies = parse_cookies(current_event.get('cookies', []))
|
||||
session = cookies.get('__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): ...
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
from http.cookies import SimpleCookie
|
||||
|
||||
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:
|
||||
with open(get_file_path(name)) as f:
|
||||
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
|
||||
revision = 2
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
@@ -497,7 +497,7 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "layercake"
|
||||
version = "0.11.0"
|
||||
version = "0.11.1"
|
||||
source = { directory = "../layercake" }
|
||||
dependencies = [
|
||||
{ name = "arnparse" },
|
||||
|
||||
70
package-lock.json
generated
70
package-lock.json
generated
@@ -28,6 +28,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",
|
||||
@@ -156,6 +157,7 @@
|
||||
"dependencies": {
|
||||
"@repo/auth": "*",
|
||||
"@repo/ui": "*",
|
||||
"@repo/util": "*",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
@@ -163,6 +165,7 @@
|
||||
"lzwcompress": "^1.1.0",
|
||||
"meilisearch": "^0.54.0",
|
||||
"meilisearch-helper": "github:sergiors/meilisearch-helper",
|
||||
"ramda": "^0.32.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router": "^7.9.5",
|
||||
@@ -176,6 +179,7 @@
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/node": "^24",
|
||||
"@types/ramda": "^0.31.1",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"prettier": "^3.6.2",
|
||||
@@ -4087,6 +4091,10 @@
|
||||
"resolved": "packages/ui",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@repo/util": {
|
||||
"resolved": "packages/util",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
|
||||
@@ -4771,6 +4779,16 @@
|
||||
"devOptional": true,
|
||||
"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": {
|
||||
"version": "19.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||
@@ -6373,6 +6391,16 @@
|
||||
"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": {
|
||||
"version": "19.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||
@@ -7062,6 +7090,13 @@
|
||||
"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": {
|
||||
"version": "3.1.6",
|
||||
"resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz",
|
||||
@@ -7200,6 +7235,16 @@
|
||||
"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": {
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||
@@ -8282,6 +8327,31 @@
|
||||
"vite": "^7.2.0",
|
||||
"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 = {
|
||||
children?: string
|
||||
children?: string | React.ReactNode
|
||||
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,
|
||||
index,
|
||||
page,
|
||||
hitsPerPage,
|
||||
hitsPerPage = 25,
|
||||
sort,
|
||||
env
|
||||
}: {
|
||||
@@ -15,9 +15,9 @@ export async function createSearch({
|
||||
filter?: string
|
||||
index: string
|
||||
page?: number
|
||||
hitsPerPage: number
|
||||
hitsPerPage?: number
|
||||
sort: string[]
|
||||
env: Env
|
||||
env: any
|
||||
}): Promise<SearchResponse> {
|
||||
const host = env.MEILI_HOST
|
||||
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