update superpage

This commit is contained in:
2025-04-15 19:07:36 -03:00
parent 27769ba363
commit c702ca870b
24 changed files with 538 additions and 605 deletions

View File

@@ -1,57 +0,0 @@
from layercake.dateutils import now
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, TransactItems
from models import Course, Org
def create_course(
course: Course,
org: Org,
/,
persistence_layer: DynamoDBPersistenceLayer,
):
now_ = now()
transact = TransactItems(persistence_layer.table_name)
transact.put(
item={
'sk': '0',
'tenant__org_id': {org.id},
'create_date': now_,
**course.model_dump(),
}
)
transact.put(
item={
'id': course.id,
'sk': 'tenant',
'org_id': org.id,
'name': org.name,
'create_date': now_,
}
)
return persistence_layer.transact_write_items(transact)
def update_course(
id: str,
course: Course,
/,
persistence_layer: DynamoDBPersistenceLayer,
):
now_ = now()
transact = TransactItems(persistence_layer.table_name)
transact.update(
key=KeyPair(id, '0'),
update_expr='SET #name = :name, access_period = :access_period, cert = :cert, update_date = :update_date',
expr_attr_names={
'#name': 'name',
},
expr_attr_values={
':name': course.name,
':cert': course.cert.model_dump() if course.cert else None,
':access_period': course.access_period,
':update_date': now_,
},
cond_expr='attribute_exists(sk)',
)
return persistence_layer.transact_write_items(transact)

View File

@@ -1,102 +0,0 @@
from typing import TypedDict
from uuid import uuid4
from layercake.dateutils import now
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, TransactItems
from settings import ORDER_TABLE
class Author(TypedDict):
id: str
name: str
class Course(TypedDict):
id: str
name: str
time_in_days: int
def set_status_as_canceled(
id: str,
*,
lock_hash: str,
author: Author,
course: Course | None = None,
vacancy_key: KeyPair | None = None,
persistence_layer: DynamoDBPersistenceLayer,
):
"""Cancel the enrollment if there's a `cancel_policy`
and put its vacancy back if `vacancy_key` is provided."""
now_ = now()
transact = TransactItems(persistence_layer.table_name)
transact.update(
key=KeyPair(id, '0'),
update_expr='SET #status = :canceled, update_date = :update',
expr_attr_names={
'#status': 'status',
},
expr_attr_values={
':canceled': 'CANCELED',
':update': now_,
},
)
transact.put(
item={
'id': id,
'sk': 'canceled_date',
'author': author,
'create_date': now_,
},
)
transact.delete(
key=KeyPair(id, 'cancel_policy'),
cond_expr='attribute_exists(sk)',
)
# Remove schedules lifecycle events, referencies and locks
transact.delete(key=KeyPair(id, 'schedules#archive_it'))
transact.delete(key=KeyPair(id, 'schedules#no_activity'))
transact.delete(key=KeyPair(id, 'schedules#access_period_ends'))
transact.delete(key=KeyPair(id, 'schedules#does_not_access'))
transact.delete(key=KeyPair(id, 'parent_vacancy'))
transact.delete(key=KeyPair(id, 'lock'))
transact.delete(key=KeyPair('lock', lock_hash))
if vacancy_key and course:
vacancy_pk, vacancy_sk = vacancy_key.values()
org_id = vacancy_pk.removeprefix('vacancies#')
order_id, enrollment_id = vacancy_sk.split('#')
transact.condition(
key=KeyPair(order_id, '0'),
cond_expr='attribute_exists(id)',
table_name=ORDER_TABLE,
)
# Put the vacancy back and assign a new ID
transact.put(
item={
'id': f'vacancies#{org_id}',
'sk': f'{order_id}#{uuid4()}',
'course': course,
'create_date': now_,
},
cond_expr='attribute_not_exists(sk)',
)
# Set the status of `generated_items` to `ROLLBACK` to know
# which vacancy is available for reuse
transact.update(
key=KeyPair(order_id, f'generated_items#{enrollment_id}'),
update_expr='SET #status = :status, update_date = :update',
expr_attr_names={
'#status': 'status',
},
expr_attr_values={
':status': 'ROLLBACK',
':update': now_,
},
cond_expr='attribute_exists(sk)',
table_name=ORDER_TABLE,
)
return persistence_layer.transact_write_items(transact)

View File

@@ -1,40 +0,0 @@
from layercake.dateutils import now
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, TransactItems
def update_policies(
id: str,
/,
payment_policy: dict = {},
billing_policy: dict = {},
*,
persistence_layer: DynamoDBPersistenceLayer,
):
now_ = now()
transact = TransactItems(persistence_layer.table_name)
if payment_policy:
transact.put(
item={
'id': id,
'sk': 'payment_policy',
'create_date': now_,
}
| payment_policy
)
else:
transact.delete(key=KeyPair(id, 'payment_policy'))
if billing_policy:
transact.put(
item={
'id': id,
'sk': 'billing_policy',
'create_date': now_,
}
| billing_policy
)
else:
transact.delete(key=KeyPair(id, 'billing_policy'))
return persistence_layer.transact_write_items(transact)

View File

@@ -7,9 +7,9 @@ from meilisearch import Client as Meilisearch
from api_gateway import JSONResponse from api_gateway import JSONResponse
from boto3clients import dynamodb_client from boto3clients import dynamodb_client
from course import create_course, update_course
from middlewares import AuditLogMiddleware, Tenant, TenantMiddleware from middlewares import AuditLogMiddleware, Tenant, TenantMiddleware
from models import Course, Org from models import Course, Org
from rules.course import create_course, update_course
from settings import ( from settings import (
COURSE_TABLE, COURSE_TABLE,
MEILISEARCH_API_KEY, MEILISEARCH_API_KEY,

View File

@@ -13,9 +13,9 @@ from pydantic import UUID4, BaseModel
import elastic import elastic
from boto3clients import dynamodb_client from boto3clients import dynamodb_client
from enrollment import set_status_as_canceled
from middlewares.audit_log_middleware import AuditLogMiddleware from middlewares.audit_log_middleware import AuditLogMiddleware
from middlewares.authentication_middleware import User from middlewares.authentication_middleware import User
from rules.enrollment import set_status_as_canceled
from settings import ELASTIC_CONN, ENROLLMENT_TABLE, USER_TABLE from settings import ELASTIC_CONN, ENROLLMENT_TABLE, USER_TABLE
router = Router() router = Router()

View File

@@ -15,7 +15,7 @@ from pydantic.main import BaseModel
from typing_extensions import Literal from typing_extensions import Literal
from boto3clients import dynamodb_client from boto3clients import dynamodb_client
from org import update_policies from rules.org import update_policies
from settings import USER_TABLE from settings import USER_TABLE
router = Router() router = Router()

View File

@@ -16,8 +16,8 @@ from pydantic import BaseModel, EmailStr
from api_gateway import JSONResponse from api_gateway import JSONResponse
from boto3clients import dynamodb_client from boto3clients import dynamodb_client
from middlewares import AuditLogMiddleware from middlewares import AuditLogMiddleware
from rules.user import add_email, del_email, set_email_as_primary
from settings import USER_TABLE from settings import USER_TABLE
from user import add_email, del_email, set_email_as_primary
class BadRequestError(MissingError, PowertoolsBadRequestError): ... class BadRequestError(MissingError, PowertoolsBadRequestError): ...

View File

@@ -17,8 +17,8 @@ from pydantic import BaseModel
from api_gateway import JSONResponse from api_gateway import JSONResponse
from boto3clients import dynamodb_client from boto3clients import dynamodb_client
from middlewares.audit_log_middleware import AuditLogMiddleware from middlewares.audit_log_middleware import AuditLogMiddleware
from rules.user import del_org_member
from settings import USER_TABLE from settings import USER_TABLE
from user import del_org_member
class BadRequestError(MissingError, PowertoolsBadRequestError): ... class BadRequestError(MissingError, PowertoolsBadRequestError): ...

View File

@@ -1,155 +0,0 @@
from aws_lambda_powertools.event_handler.exceptions import (
BadRequestError,
)
from botocore.exceptions import ClientError
from layercake.dateutils import now
from layercake.dynamodb import (
ComposeKey,
DynamoDBPersistenceLayer,
KeyPair,
TransactItems,
)
def add_email(
id: str,
email: str,
/,
*,
persistence_layer: DynamoDBPersistenceLayer,
):
now_ = now()
transact = TransactItems(persistence_layer.table_name)
transact.update(
key=KeyPair(id, '0'),
update_expr='ADD emails :email',
expr_attr_values={
':email': {email},
},
)
transact.put(
item={
'id': id,
'sk': f'emails#{email}',
'email_primary': False,
'email_verified': False,
'create_date': now_,
},
cond_expr='attribute_not_exists(sk)',
)
transact.put(
item={
'id': 'email',
'sk': email,
'user_id': id,
'create_date': now_,
},
cond_expr='attribute_not_exists(sk)',
)
try:
return persistence_layer.transact_write_items(transact)
except ClientError:
raise BadRequestError('Email already exists.')
def del_email(
id: str,
email: str,
/,
*,
persistence_layer: DynamoDBPersistenceLayer,
) -> bool:
"""Delete any email except the primary email."""
transact = TransactItems(persistence_layer.table_name)
transact.delete(
key=KeyPair('email', email),
)
transact.delete(
key=KeyPair(id, ComposeKey(email, prefix='emails')),
cond_expr='email_primary <> :primary',
expr_attr_values={':primary': True},
)
transact.update(
key=KeyPair(id, '0'),
update_expr='DELETE emails :email',
expr_attr_values={
':email': {email},
},
)
try:
return persistence_layer.transact_write_items(transact)
except ClientError:
raise BadRequestError('Cannot remove the primary email.')
def set_email_as_primary(
id: str,
new_email: str,
old_email: str,
/,
*,
email_verified: bool = False,
persistence_layer: DynamoDBPersistenceLayer,
):
now_ = now()
expr = 'SET email_primary = :email_primary, update_date = :update_date'
transact = TransactItems(persistence_layer.table_name)
# Set the old email as non-primary
transact.update(
key=KeyPair(id, ComposeKey(old_email, 'emails')),
update_expr=expr,
expr_attr_values={
':email_primary': False,
':update_date': now_,
},
)
# Set the new email as primary
transact.update(
key=KeyPair(id, ComposeKey(new_email, 'emails')),
update_expr=expr,
expr_attr_values={
':email_primary': True,
':update_date': now_,
},
)
transact.update(
key=KeyPair(id, '0'),
update_expr=(
'SET email = :email, email_verified = :email_verified, '
'update_date = :update_date'
),
expr_attr_values={
':email': new_email,
':email_verified': email_verified,
':update_date': now_,
},
)
return persistence_layer.transact_write_items(transact)
def del_org_member(
id: str,
*,
org_id: str,
persistence_layer: DynamoDBPersistenceLayer,
) -> bool:
transact = TransactItems(persistence_layer.table_name)
# Remove the user's relationship with the organization and their privileges
transact.delete(key=KeyPair(id, f'acls#{org_id}'))
transact.delete(key=KeyPair(id, f'orgs#{org_id}'))
transact.update(
key=KeyPair(id, '0'),
update_expr='DELETE #tenant :org_id',
expr_attr_names={'#tenant': 'tenant__org_id'},
expr_attr_values={':org_id': {org_id}},
)
# Remove the user from the organization's admins and members list
transact.delete(key=KeyPair(org_id, f'admins#{id}'))
transact.delete(key=KeyPair(f'orgmembers#{org_id}', id))
return persistence_layer.transact_write_items(transact)

6
superpage/.prettierrc Normal file
View File

@@ -0,0 +1,6 @@
{
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": true
}

View File

@@ -1,14 +1,18 @@
// @ts-check // @ts-check
import { defineConfig } from 'astro/config'; import { defineConfig } from 'astro/config'
import react from '@astrojs/react'; import react from '@astrojs/react'
import tailwindcss from '@tailwindcss/vite'; import tailwindcss from '@tailwindcss/vite'
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
integrations: [react()], integrations: [react()],
vite: { vite: {
plugins: [tailwindcss()] plugins: [tailwindcss()],
} },
}); server: {
host: '0.0.0.0',
allowedHosts: ['7aaa-187-57-7-239.ngrok-free.app'],
},
})

View File

@@ -9,7 +9,7 @@
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@astrojs/react": "^4.2.1", "@astrojs/react": "^4.2.1",
"@headlessui/react": "^2.2.0", "@headlessui/react": "^2.2.1",
"@heroicons/react": "^2.2.0", "@heroicons/react": "^2.2.0",
"@tailwindcss/vite": "^4.0.13", "@tailwindcss/vite": "^4.0.13",
"@tanstack/react-query": "^5.68.0", "@tanstack/react-query": "^5.68.0",
@@ -873,15 +873,15 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@headlessui/react": { "node_modules/@headlessui/react": {
"version": "2.2.0", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.0.tgz", "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.1.tgz",
"integrity": "sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==", "integrity": "sha512-daiUqVLae8CKVjEVT19P/izW0aGK0GNhMSAeMlrDebKmoVZHcRRwbxzgtnEadUVDXyBsWo9/UH4KHeniO+0tMg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@floating-ui/react": "^0.26.16", "@floating-ui/react": "^0.26.16",
"@react-aria/focus": "^3.17.1", "@react-aria/focus": "^3.17.1",
"@react-aria/interactions": "^3.21.3", "@react-aria/interactions": "^3.21.3",
"@tanstack/react-virtual": "^3.8.1" "@tanstack/react-virtual": "^3.11.1"
}, },
"engines": { "engines": {
"node": ">=10" "node": ">=10"
@@ -1316,14 +1316,14 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@react-aria/focus": { "node_modules/@react-aria/focus": {
"version": "3.20.1", "version": "3.20.2",
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.1.tgz", "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.2.tgz",
"integrity": "sha512-lgYs+sQ1TtBrAXnAdRBQrBo0/7o5H6IrfDxec1j+VRpcXL0xyk0xPq+m3lZp8typzIghqDgpnKkJ5Jf4OrzPIw==", "integrity": "sha512-Q3rouk/rzoF/3TuH6FzoAIKrl+kzZi9LHmr8S5EqLAOyP9TXIKG34x2j42dZsAhrw7TbF9gA8tBKwnCNH4ZV+Q==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@react-aria/interactions": "^3.24.1", "@react-aria/interactions": "^3.25.0",
"@react-aria/utils": "^3.28.1", "@react-aria/utils": "^3.28.2",
"@react-types/shared": "^3.28.0", "@react-types/shared": "^3.29.0",
"@swc/helpers": "^0.5.0", "@swc/helpers": "^0.5.0",
"clsx": "^2.0.0" "clsx": "^2.0.0"
}, },
@@ -1333,15 +1333,15 @@
} }
}, },
"node_modules/@react-aria/interactions": { "node_modules/@react-aria/interactions": {
"version": "3.24.1", "version": "3.25.0",
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.24.1.tgz", "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.0.tgz",
"integrity": "sha512-OWEcIC6UQfWq4Td5Ptuh4PZQ4LHLJr/JL2jGYvuNL6EgL3bWvzPrRYIF/R64YbfVxIC7FeZpPSkS07sZ93/NoA==", "integrity": "sha512-GgIsDLlO8rDU/nFn6DfsbP9rfnzhm8QFjZkB9K9+r+MTSCn7bMntiWQgMM+5O6BiA8d7C7x4zuN4bZtc0RBdXQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@react-aria/ssr": "^3.9.7", "@react-aria/ssr": "^3.9.8",
"@react-aria/utils": "^3.28.1", "@react-aria/utils": "^3.28.2",
"@react-stately/flags": "^3.1.0", "@react-stately/flags": "^3.1.1",
"@react-types/shared": "^3.28.0", "@react-types/shared": "^3.29.0",
"@swc/helpers": "^0.5.0" "@swc/helpers": "^0.5.0"
}, },
"peerDependencies": { "peerDependencies": {
@@ -1350,9 +1350,9 @@
} }
}, },
"node_modules/@react-aria/ssr": { "node_modules/@react-aria/ssr": {
"version": "3.9.7", "version": "3.9.8",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.7.tgz", "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.8.tgz",
"integrity": "sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==", "integrity": "sha512-lQDE/c9uTfBSDOjaZUJS8xP2jCKVk4zjQeIlCH90xaLhHDgbpCdns3xvFpJJujfj3nI4Ll9K7A+ONUBDCASOuw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@swc/helpers": "^0.5.0" "@swc/helpers": "^0.5.0"
@@ -1365,15 +1365,15 @@
} }
}, },
"node_modules/@react-aria/utils": { "node_modules/@react-aria/utils": {
"version": "3.28.1", "version": "3.28.2",
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.28.1.tgz", "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.28.2.tgz",
"integrity": "sha512-mnHFF4YOVu9BRFQ1SZSKfPhg3z+lBRYoW5mLcYTQihbKhz48+I1sqRkP7ahMITr8ANH3nb34YaMME4XWmK2Mgg==", "integrity": "sha512-J8CcLbvnQgiBn54eeEvQQbIOfBF3A1QizxMw9P4cl9MkeR03ug7RnjTIdJY/n2p7t59kLeAB3tqiczhcj+Oi5w==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@react-aria/ssr": "^3.9.7", "@react-aria/ssr": "^3.9.8",
"@react-stately/flags": "^3.1.0", "@react-stately/flags": "^3.1.1",
"@react-stately/utils": "^3.10.5", "@react-stately/utils": "^3.10.6",
"@react-types/shared": "^3.28.0", "@react-types/shared": "^3.29.0",
"@swc/helpers": "^0.5.0", "@swc/helpers": "^0.5.0",
"clsx": "^2.0.0" "clsx": "^2.0.0"
}, },
@@ -1383,18 +1383,18 @@
} }
}, },
"node_modules/@react-stately/flags": { "node_modules/@react-stately/flags": {
"version": "3.1.0", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.0.tgz", "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.1.tgz",
"integrity": "sha512-KSHOCxTFpBtxhIRcKwsD1YDTaNxFtCYuAUb0KEihc16QwqZViq4hasgPBs2gYm7fHRbw7WYzWKf6ZSo/+YsFlg==", "integrity": "sha512-XPR5gi5LfrPdhxZzdIlJDz/B5cBf63l4q6/AzNqVWFKgd0QqY5LvWJftXkklaIUpKSJkIKQb8dphuZXDtkWNqg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@swc/helpers": "^0.5.0" "@swc/helpers": "^0.5.0"
} }
}, },
"node_modules/@react-stately/utils": { "node_modules/@react-stately/utils": {
"version": "3.10.5", "version": "3.10.6",
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.5.tgz", "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.6.tgz",
"integrity": "sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ==", "integrity": "sha512-O76ip4InfTTzAJrg8OaZxKU4vvjMDOpfA/PGNOytiXwBbkct2ZeZwaimJ8Bt9W1bj5VsZ81/o/tW4BacbdDOMA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@swc/helpers": "^0.5.0" "@swc/helpers": "^0.5.0"
@@ -1404,9 +1404,9 @@
} }
}, },
"node_modules/@react-types/shared": { "node_modules/@react-types/shared": {
"version": "3.28.0", "version": "3.29.0",
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.28.0.tgz", "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.29.0.tgz",
"integrity": "sha512-9oMEYIDc3sk0G5rysnYvdNrkSg7B04yTKl50HHSZVbokeHpnU0yRmsDaWb9B/5RprcKj8XszEk5guBO8Sa/Q+Q==", "integrity": "sha512-IDQYu/AHgZimObzCFdNl1LpZvQW/xcfLt3v20sorl5qRucDVj4S9os98sVTZ4IRIBjmS+MkjqpR5E70xan7ooA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peerDependencies": { "peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
@@ -1757,9 +1757,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@swc/helpers": { "node_modules/@swc/helpers": {
"version": "0.5.15", "version": "0.5.17",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": "^2.8.0" "tslib": "^2.8.0"
@@ -2016,12 +2016,12 @@
} }
}, },
"node_modules/@tanstack/react-virtual": { "node_modules/@tanstack/react-virtual": {
"version": "3.13.2", "version": "3.13.6",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.2.tgz", "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.6.tgz",
"integrity": "sha512-LceSUgABBKF6HSsHK2ZqHzQ37IKV/jlaWbHm+NyTa3/WNb/JZVcThDuTainf+PixltOOcFCYXwxbLpOX9sCx+g==", "integrity": "sha512-WT7nWs8ximoQ0CDx/ngoFP7HbQF9Q2wQe4nh2NB+u2486eX3nZRE40P9g6ccCVq7ZfTSH5gFOuCoVH5DLNS/aA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tanstack/virtual-core": "3.13.2" "@tanstack/virtual-core": "3.13.6"
}, },
"funding": { "funding": {
"type": "github", "type": "github",
@@ -2033,9 +2033,9 @@
} }
}, },
"node_modules/@tanstack/virtual-core": { "node_modules/@tanstack/virtual-core": {
"version": "3.13.2", "version": "3.13.6",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.2.tgz", "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.6.tgz",
"integrity": "sha512-Qzz4EgzMbO5gKrmqUondCjiHcuu4B1ftHb0pjCut661lXZdGoHeze9f/M8iwsK1t5LGR6aNuNGU7mxkowaW6RQ==", "integrity": "sha512-cnQUeWnhNP8tJ4WsGcYiX24Gjkc9ALstLbHcBj1t3E7EimN6n6kHH+DPV4PpDnuw00NApQp+ViojMj1GRdwYQg==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",

View File

@@ -10,7 +10,7 @@
}, },
"dependencies": { "dependencies": {
"@astrojs/react": "^4.2.1", "@astrojs/react": "^4.2.1",
"@headlessui/react": "^2.2.0", "@headlessui/react": "^2.2.1",
"@heroicons/react": "^2.2.0", "@heroicons/react": "^2.2.0",
"@tailwindcss/vite": "^4.0.13", "@tailwindcss/vite": "^4.0.13",
"@tanstack/react-query": "^5.68.0", "@tanstack/react-query": "^5.68.0",

View File

@@ -1,30 +1,30 @@
import clsx from "clsx"; import clsx from 'clsx'
interface CardProps { interface CardProps {
children: React.ReactNode; children: React.ReactNode
color?: "gradient" | "darker" | "yellow" | "zinc"; color?: 'gradient' | 'darker' | 'yellow' | 'zinc'
className?: string | undefined; className?: string | undefined
} }
export function Card({ children, color = "gradient", className }: CardProps) { export function Card({ children, color = 'gradient', className }: CardProps) {
const colorVariants = { const colorVariants = {
gradient: "bg-linear-to-tr from-green-secondary to-yellow-primary", gradient: 'bg-linear-to-tr from-[#8CD366] via-[#C7D174] to-[#FFCF82]',
darker: "bg-green-primary text-white", darker: 'bg-green-primary text-white',
yellow: "text-green-primary bg-yellow-tertiary", yellow: 'text-green-primary bg-yellow-tertiary',
zinc: "text-white bg-zinc-900", zinc: 'text-white bg-zinc-900',
}; }
return ( return (
<div <div
className={clsx( className={clsx(
"lg:rounded-2xl", 'lg:rounded-2xl',
"lg:drop-shadow-sm", 'lg:drop-shadow-sm',
"p-3 lg:p-12", 'p-3 lg:p-12',
colorVariants[color], colorVariants[color],
className, className,
)} )}
> >
{children} {children}
</div> </div>
); )
} }

View File

@@ -1,10 +1,14 @@
import clsx from "clsx"; import clsx from 'clsx'
interface ContainerProps { interface ContainerProps {
children: React.ReactNode; children: React.ReactNode
className?: string | undefined; className?: string | undefined
} }
export function Container({ children, className }: ContainerProps) { export function Container({ children, className }: ContainerProps) {
return <div className={clsx("max-w-7xl mx-auto", className)}>{children}</div>; return (
<div className={clsx('max-w-7xl mx-auto max-lg:px-3', className)}>
{children}
</div>
)
} }

View File

@@ -1,33 +1,33 @@
import { useForm } from "react-hook-form"; import { useForm } from 'react-hook-form'
import { useMutation } from "node_modules/@tanstack/react-query/build/legacy"; import { useMutation } from 'node_modules/@tanstack/react-query/build/legacy'
import { queryClient } from "../queryClient"; import { queryClient } from '../queryClient'
import axios from "axios"; import axios from 'axios'
import { createElement } from "react"; import { createElement } from 'react'
import clsx from "clsx"; import clsx from 'clsx'
interface IFormInput { interface IFormInput {
name: string; name: string
email: string; email: string
message: string; message: string
} }
export function Form() { export function Form() {
const { register, handleSubmit, reset, formState } = useForm<IFormInput>(); const { register, handleSubmit, reset, formState } = useForm<IFormInput>()
const { mutateAsync } = useMutation( const { mutateAsync } = useMutation(
{ {
mutationFn: async (data: IFormInput) => { mutationFn: async (data: IFormInput) => {
return await axios.post("https://n8n.sergio.run/webhook/eduseg", data); return await axios.post('https://n8n.sergio.run/webhook/eduseg', data)
}, },
onSuccess: () => { onSuccess: () => {
reset(); reset()
}, },
}, },
queryClient, queryClient,
); )
const onSubmit = async (data: IFormInput) => { const onSubmit = async (data: IFormInput) => {
await mutateAsync(data); await mutateAsync(data)
}; }
return ( return (
<form <form
@@ -41,17 +41,17 @@ export function Form() {
<div className="grid lg:grid-cols-2 gap-3 lg:gap-6"> <div className="grid lg:grid-cols-2 gap-3 lg:gap-6">
<label> <label>
Nome Nome
<Input {...register("name")} /> <Input {...register('name')} />
</label> </label>
<label> <label>
Email Email
<Input {...register("email")} /> <Input {...register('email')} />
</label> </label>
</div> </div>
<label> <label>
Mensagem Mensagem
<Input as="textarea" className="h-26" {...register("message")} /> <Input as="textarea" className="h-26" {...register('message')} />
</label> </label>
<button <button
@@ -61,20 +61,20 @@ export function Form() {
Quero um orçamento Quero um orçamento
</button> </button>
</form> </form>
); )
} }
interface IInput extends React.HTMLAttributes<HTMLElement> { interface IInput extends React.HTMLAttributes<HTMLElement> {
as?: string; as?: string
className?: string | undefined; className?: string | undefined
} }
export function Input({ as = "input", className, ...props }: IInput) { export function Input({ as = 'input', className, ...props }: IInput) {
return createElement(as, { return createElement(as, {
className: clsx( className: clsx(
"border border-transparent focus:border-green-secondary focus:ring ring-green-secondary text-white bg-black p-3 rounded-lg w-full outline-none", 'border border-transparent focus:border-green-secondary focus:ring ring-green-secondary text-white bg-black p-3 rounded-lg w-full outline-none',
className, className,
), ),
...props, ...props,
}); })
} }

View File

@@ -0,0 +1,102 @@
import {
Disclosure,
DisclosureButton,
DisclosurePanel,
} from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/24/solid'
export function Example() {
return (
<>
<ListItem defaultOpen={false}>
<ListButton>1. Introdução</ListButton>
<ListPanel>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed sit
amet neque id libero semper vulputate a ut ex. Pellentesque semper
ultrices mi in efficitur.
</p>
<p>
Nulla sit amet quam eu neque convallis volutpat. Pellentesque eu
commodo sem. Suspendisse ac lobortis massa, ac mattis mauris.
Integer malesuada bibendum ante, sed consequat augue convallis et.
</p>
</ListPanel>
</ListItem>
<ListItem>
<ListButton>2. Aspectos gerais dos primeiros socorros</ListButton>
<ListPanel>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed sit
amet neque id libero semper vulputate a ut ex. Pellentesque semper
ultrices mi in efficitur.
</p>
<p>
Nulla sit amet quam eu neque convallis volutpat. Pellentesque eu
commodo sem. Suspendisse ac lobortis massa, ac mattis mauris.
Integer malesuada bibendum ante, sed consequat augue convallis et.
</p>
</ListPanel>
</ListItem>
<ListItem>
<ListButton>3. Sinais vitais e avaliação primária</ListButton>
<ListPanel>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed sit
amet neque id libero semper vulputate a ut ex. Pellentesque semper
ultrices mi in efficitur.
</p>
<p>
Nulla sit amet quam eu neque convallis volutpat. Pellentesque eu
commodo sem. Suspendisse ac lobortis massa, ac mattis mauris.
Integer malesuada bibendum ante, sed consequat augue convallis et.
</p>
</ListPanel>
</ListItem>
<ListItem>
<ListButton>4. Parada cardiorrespiratória</ListButton>
<ListPanel>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed sit
amet neque id libero semper vulputate a ut ex. Pellentesque semper
ultrices mi in efficitur.
</p>
<p>
Nulla sit amet quam eu neque convallis volutpat. Pellentesque eu
commodo sem. Suspendisse ac lobortis massa, ac mattis mauris.
Integer malesuada bibendum ante, sed consequat augue convallis et.
</p>
</ListPanel>
</ListItem>
</>
)
}
export function ListItem({ children, ...props }) {
return (
<Disclosure
as="div"
className="bg-white/10 rounded-lg w-full data-open:bg-white/15"
{...props}
>
{children}
</Disclosure>
)
}
export function ListButton({ children }) {
return (
<DisclosureButton className="group flex items-center justify-between w-full px-5 py-3 cursor-pointer">
<span className="text-left">{children}</span>
<ChevronDownIcon className="size-5 fill-white/60 group-data-[hover]:fill-white/50 group-data-[open]:rotate-180" />
</DisclosureButton>
)
}
export function ListPanel({ children }) {
return (
<DisclosurePanel className="text-sm/6 text-white/70 space-y-2 px-5 pb-3">
{children}
</DisclosurePanel>
)
}

View File

@@ -60,7 +60,7 @@ export function Regular(props) {
</g> </g>
</g> </g>
</svg> </svg>
); )
} }
export function Smallest(props) { export function Smallest(props) {
@@ -95,5 +95,5 @@ export function Smallest(props) {
</g> </g>
</g> </g>
</svg> </svg>
); )
} }

View File

@@ -4,32 +4,32 @@ import {
ComboboxInput, ComboboxInput,
ComboboxOption, ComboboxOption,
ComboboxOptions, ComboboxOptions,
} from "@headlessui/react"; } from '@headlessui/react'
import { CheckIcon, ChevronDownIcon } from "@heroicons/react/20/solid"; import { CheckIcon, ChevronDownIcon } from '@heroicons/react/20/solid'
import clsx from "clsx"; import clsx from 'clsx'
import { useState } from "react"; import { useState } from 'react'
const people = [ const people = [
{ id: 1, name: "8 horas" }, { id: 1, name: '8 horas' },
{ id: 2, name: "40 horas" }, { id: 2, name: '40 horas' },
]; ]
export function Select() { export function Select() {
const [query, setQuery] = useState(""); const [query, setQuery] = useState('')
const [selected, setSelected] = useState(people[1]); const [selected, setSelected] = useState(people[1])
const filteredPeople = const filteredPeople =
query === "" query === ''
? people ? people
: people.filter((person) => { : people.filter((person) => {
return person.name.toLowerCase().includes(query.toLowerCase()); return person.name.toLowerCase().includes(query.toLowerCase())
}); })
return ( return (
<Combobox <Combobox
value={selected} value={selected}
onChange={(value) => setSelected(value)} onChange={(value) => setSelected(value)}
onClose={() => setQuery("")} onClose={() => setQuery('')}
> >
<div className="relative"> <div className="relative">
<ComboboxInput <ComboboxInput
@@ -50,5 +50,5 @@ export function Select() {
))} ))}
</ComboboxOptions> </ComboboxOptions>
</Combobox> </Combobox>
); )
} }

View File

@@ -1,82 +1,96 @@
--- ---
import "../styles/app.css"; import '../styles/app.css'
import { Regular as Logo } from "@components/Logo"; import { Regular as Logo } from '@components/Logo'
import { Container } from "@components/Container"; import { Container } from '@components/Container'
import { Select } from "@components/Select";
import { import {
ArrowLeftStartOnRectangleIcon, ArrowLeftStartOnRectangleIcon,
AcademicCapIcon, AcademicCapIcon,
} from "@heroicons/react/24/solid"; ChevronDownIcon,
} from '@heroicons/react/24/solid'
--- ---
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} /> <meta name="generator" content={Astro.generator} />
<title>EDUSEG&reg; &mdash; Educação que garante sua segurança</title> <title>EDUSEG&reg; &mdash; Educação que garante sua segurança</title>
</head> </head>
<body> <body>
<Container className="flex items-center p-3"> <Container className="flex items-center py-3">
<Logo className="h-8" /> <Logo className="h-8" />
<div class="ml-auto"> <div class="ml-auto">
<a href="#" class="flex gap-1 items-center"> <a href="#" class="flex gap-1 items-center">
<ArrowLeftStartOnRectangleIcon className="w-5 rotate-180" /> <ArrowLeftStartOnRectangleIcon className="w-5 rotate-180" />
<>Entrar</> <>Entrar</>
</a> </a>
</div>
</Container>
<section class="bg-lime-400 sticky top-0 z-10 py-3">
<Container className="flex items-center">
<div
class="flex gap-1.5 lg:gap-3 items-center rounded hover:ring-2 ring-black"
>
<div class="flex gap-1.5">
<div class="text-black truncate max-lg:max-w-36 font-semibold">
NR-18 PEMT Plataforma Móvel de Trabalho Aéreo
</div> </div>
</Container> <ChevronDownIcon className="w-5 fill-black" />
</div>
</div>
<section class="bg-lime-400 sticky top-0 z-10"> <div class="ml-auto">
<Container className="flex items-center p-3"> <button
<div class="flex gap-1.5 lg:gap-3 items-center"> class="bg-black font-semibold py-2.5 px-3 rounded-md cursor-pointer"
<div class="bg-black p-1.5 lg:p-3 rounded-lg lg:rounded-xl"> >
<AcademicCapIcon className="w-5 fill-lime-400" /> Contratar agora
</div> </button>
</div>
</Container>
</section>
<div class="flex gap-1.5"> <div class="mb-12">
<div class="text-black truncate max-lg:max-w-36"> <slot />
NR-18 PEMT Plataforma Móvel de Trabalho Aéreo </div>
</div>
<!-- <Select client:load /> -->
</div>
</div>
<div class="ml-auto"> <footer class="bg-stone-900 py-3 lg:py-10 hidden">
<button <Container className="space-y-5 lg:grid grid-cols-6">
class="bg-black p-2.5 rounded-md cursor-pointer text-sm" <div class="space-y-2.5">
> <Logo className="h-10" />
Comprar R$149,00 <p class="text-sm/4 tracking-tighter text-balance">
</button> Educação que garante<br />sua segurança.
</div> </p>
</Container> </div>
</section>
<slot /> <div class="space-y-1 lg:col-span-5">
<h6 class="text-2xl/4">Conheça outros cursos</h6>
<footer class="bg-stone-900 p-3 lg:py-10"> <ul class="mt-2.5">
<Container className="space-y-5"> <li>Lei Lucas</li>
<div class="space-y-1"> <li>NR-18 PEMT Plataforma Móvel de Trabalho Aéreo</li>
<Logo className="h-10" /> <li>NR-35 Trabalho em Altura</li>
<p class="text-sm text-green-tertiary leading-4"> <li>NR-10 Básico</li>
Educação que garante<br />sua segurança. <li>
</p> NR-12 Máquinas e Equipamentos <span
</div> class="bg-lime-300 py-0.5 px-1 text-black rounded text-sm/2"
>Reciclagem</span
<div class="space-y-1"> >
<h6 class="text-xl">Cursos</h6> </li>
<ul> <li>
<li>Lei Lucas</li> NR-18 PEMT Plataforma Elevatória Móvel de Trabalho <span
<li>NR-18 PEMT Plataforma Móvel de Trabalho Aéreo</li> class="bg-lime-300 py-0.5 px-1 text-black rounded text-sm/2"
<li>NR-35 Trabalho em Altura</li> >Reciclagem</span
<li>NR-10 Básico</li> >
</ul> </li>
</div> <li>LOTO Lockout Tagout</li>
</Container> <li>NR-18 Sinaleiro e Amarrador de Cargas para Içamento</li>
</footer> </ul>
</body> </div>
</Container>
</footer>
</body>
</html> </html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

View File

@@ -1,77 +1,234 @@
--- ---
import { Bookmark } from "@components/Bookmark"; import { Image } from 'astro:assets'
import { Card } from "@components/Card"; import { Card } from '@components/Card'
import { Container } from "@components/Container"; import { Container } from '@components/Container'
import { Form } from "@components/Form"; import { Form } from '@components/Form'
import Layout from "@layouts/Layout.astro"; import Layout from '@layouts/Layout.astro'
import { Example } from '@components/List'
import {
AcademicCapIcon,
GlobeAmericasIcon,
PhoneIcon,
BanknotesIcon,
StarIcon,
ClockIcon,
} from '@heroicons/react/24/outline'
import { CheckBadgeIcon } from '@heroicons/react/24/solid'
import nr18plataforma from './nr18-plataforma.png'
import mulherdenegocios from './mulher-de-negocios.png'
import homemdenegocios from './homem-de-negocios.png'
--- ---
<Layout> <Layout>
<Container className="lg:space-y-5 lg:p-3"> <Container className="py-3 lg:py-12 lg:flex items-center gap-5">
<!-- <div class="max-lg:px-5"> <Image
<Bookmark className="h-96" /> src={nr18plataforma}
</div> --> alt="NR-18"
<Card> class="size-1/3 object-bottom hidden lg:block"
<p> />
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam <section>
quis mattis tortor, sit amet mollis lorem. In imperdiet, ante <div class="space-y-5">
sit amet maximus dictum, est elit ultrices lacus, in placerat <span class="font-medium">Curso de formação</span>
ante risus vel massa. Maecenas porta purus non feugiat <h1 class="font-semibold text-5xl lg:text-7xl">
venenatis. Sed tempus quam id commodo interdum. Aliquam id NR-18 PEMT Plataforma Móvel de Trabalho Aéreo
ullamcorper diam. Morbi a porttitor tellus. Fusce viverra </h1>
euismod laoreet. Cras id sapien quis orci rutrum lacinia. Donec <p class="text-base/6">
vitae libero at felis auctor blandit commodo sed libero. Aliquam NR 18 PEMT capacita operadores de plataformas elevatórias para
tellus risus, sagittis a libero eget, hendrerit feugiat mauris. trabalhos em altura com segurança. Com foco na manutenção, inspeção e
Ut vehicula id est non iaculis. Suspendisse potenti. Maecenas in uso correto dos EPIs, previne sempre acidentes, garante certificação
tellus risus. Proin quis libero et ero s ullamcorper faucibus MTE e valoriza sua carreira.
non vitae lacus. Vestibulum at ultricies sem, vel euismod dolor. </p>
</p><p> <ul class="lg:flex gap-3">
In tempor vel felis ut imperdiet. Pellentesque ac vulputate <li class="flex gap-1">
lorem, id pellentesque velit. Sed rutrum, nisi vel convallis <ClockIcon className="w-5" />
rhoncus, ex mi vulputate leo, id hendrerit ipsum sapien in <span>Carga horária de 40 horas</span>
dolor. Nullam auctor eu nunc sed euismod. Donec molestie velit </li>
nec est bibendum pulvinar. Nullam mattis mollis neque, nec
cursus mi iaculis et. Morbi tempus purus sit amet orci pulvinar
accumsan. Fusce mattis, nisl ac fringilla euismod, orci odio
condimentum sapien, a convallis lacus diam et libero. Nunc non
urna a orci eleifend porttitor in eget nisi. Ut scelerisque
egestas hendrerit. Aenean in tortor cursus, lobortis dolor
iaculis, dignissim velit. Nulla facilisi.
</p>
</Card>
<Card color="darker"> <li class="flex gap-1">
<p> <CheckBadgeIcon className="w-5 fill-blue-400" />
In tempor vel felis ut imperdiet. Pellentesque ac vulputate <span>Certificado com assinatura digital</span>
lorem, id pellentesque velit. Sed rutrum, nisi vel convallis </li>
rhoncus, ex mi vulputate leo, id hendrerit ipsum sapien in </ul>
dolor. Nullam auctor eu nunc sed euismod. Donec molestie velit
nec est bibendum pulvinar. Nullam mattis mollis neque, nec
cursus mi iaculis et. Morbi tempus purus sit amet orci pulvinar
accumsan. Fusce mattis, nisl ac fringilla euismod, orci odio
condimentum sapien, a convallis lacus diam et libero. Nunc non
urna a orci eleifend porttitor in eget nisi. Ut scelerisque
egestas hendrerit. Aenean in tortor cursus, lobortis dolor
iaculis, dignissim velit. Nulla facilisi.
</p>
</Card>
<section class="grid lg:grid-cols-2 gap-6 p-3"> <div
<div class="flex items-center justify-center"> class="flex max-lg:flex-col justify-center gap-2.5 lg:gap-8 lg:mt-16"
<div class="space-y-3"> >
<h4 class="font-medium text-4xl">Solicite um orçamento</h4> <a
<p> href="#"
Quer saber como podemos capacitar sua equipe?<br class="text-black font-semibold bg-lime-400 rounded p-3.5 hover:bg-white max-lg:text-center"
class="max-lg:hidden" >
/> Contratar agora
Fale com nossa equipe e receba uma proposta personalizada. </a>
</p> <a href="http://bit.ly/3RlROu6" class="flex flex-col">
</div> <div class="flex items-center gap-1">
<span class="font-bold">4.7</span>
<ul class="flex">
<li>
<StarIcon className="w-4 text-yellow-500 fill-yellow-500" />
</li>
<li>
<StarIcon className="w-4 text-yellow-500 fill-yellow-500" />
</li>
<li>
<StarIcon className="w-4 text-yellow-500 fill-yellow-500" />
</li>
<li>
<StarIcon className="w-4 text-yellow-500 fill-yellow-500" />
</li>
<li><StarIcon className="w-4" /></li>
</ul>
</div> </div>
<Card color="zinc" className="rounded-xl p-4"> <span>66 avaliações no Google</span>
<Form client:load /> </a>
</Card> </div>
</div>
</section>
</Container>
<Container className="h-full">
<div class="border border-lime-400 rounded-2xl grid grid-cols-3">
<div class="bg-lime-300 rounded-2xl p-6 relative h-[36rem]">
<Image
alt="Homem de negócios"
src={homemdenegocios}
class="absolute bottom-0 -left-32"
/>
<Image
alt="Mulher de negócios"
src={mulherdenegocios}
class="absolute bottom-0 -right-26"
/>
</div>
<div class="col-span-2 flex items-center">
<div class="w-7/12 mx-auto space-y-5 p-6">
<h2 class="text-4xl font-semibold">
Por que capacitar sua equipe com a EDUSEG&reg;
</h2>
<p>
Nós cuidamos da burocracia, oferecemos uma plataforma completa para
simplicar a gestão e capacitação em massa de seus colaboradores. Com
a EDUSEG&reg, sua empresa se beneficia de uma tecnologia eficiente e
confiável.
</p>
<ul class="grid grid-cols-2 gap-2.5">
<li class="bg-white/10 p-5 rounded-lg">
Conformidade legal garantida
</li>
<li class="bg-white/10 p-5 rounded-lg">
Economia de tempo e recursos
</li>
<li class="bg-white/10 p-5 rounded-lg">
Relatórios e monitoramento
</li>
<li class="bg-white/10 p-5 rounded-lg">
Suporte especializado para gestores
</li>
</ul>
</div>
</div>
</div>
</Container>
<Container
className="py-3 lg:py-12 grid gap-2.5 lg:grid-cols-3 lg:gap-5 lg:w-3/6"
>
<div class="space-y-2.5">
<h1 class="text-4xl lg:text-5xl text-pretty">Módulos deste curso</h1>
<p class="text-base/6">
O curso é dividido em módulos para facilitar seu aprendizado e garantir
que você domine todos os aspectos teóricos e práticos.
</p>
</div>
<div class="lg:col-span-2 flex flex-col gap-1.5">
<Example client:load />
</div>
</Container>
<Container className="py-6 hidden">
<ul class="flex gap-2.5">
<li class="flex items-center gap-2.5">
<span class="bg-white/10 p-2 rounded">
<BanknotesIcon className="w-6" />
</span>
<span class="text-base/5">Economize até 70% com o curso online</span>
</li>
<li class="flex items-center gap-2.5">
<span class="bg-white/10 p-2 rounded">
<AcademicCapIcon className="w-6" />
</span>
<span class="text-base/6"
>Certificado digital reconhecido em até 24 horas</span
>
</li>
<li class="flex items-center gap-2.5">
<span class="bg-white/10 p-2 rounded">
<GlobeAmericasIcon className="w-6" />
</span>
<span>Aprenda no seu tempo e de qualquer lugar</span>
</li>
<li class="flex items-center gap-2.5">
<span class="bg-white/10 p-2 rounded">
<PhoneIcon className="w-6" />
</span>
<span>Suporte de especialistas sempre que precisar</span>
</li>
</ul>
</Container>
<Container className="hidden">
<h2>Quem é o instrutor?</h2>
</Container>
<!--
<Container className="lg:space-y-5 lg:p-3">
<Card className="space-y-2.5">
<h1 class="text-2xl font-medium">
Garanta a capacitação para sua empresa
</h1>
<ul>
<li>Pagamento flexível</li>
<li>Acesso imediato</li>
<li>Carga horária</li>
</ul>
<ul>
<li>Economize até 70% com o curso online</li>
<li>Certificado Digital reconhecido em até 24 Horas</li>
<li>Aprenda no seu tempo e de qualquer lugar</li>
<li>Suporte de especialistas sempre que precisar</li>
</ul>
</Card>
<section class="lg:grid grid-cols-4">
<div>
<h1 class="text-2xl font-medium">Módulos deste treinamento</h1>
<div>
O curso é dividido em módulos para facilitar seu aprendizado
e garantir que você domine todos os aspectos teóricos e
práticos.
</div>
</div>
<div><Example client:load /></div>
</section> </section>
</Container> </Container>
<section class="grid lg:grid-cols-2 gap-6 p-3">
<div class="flex items-center justify-center">
<div class="space-y-3">
<h4 class="font-medium text-4xl">Solicite um orçamento</h4>
<p>
Quer saber como podemos capacitar sua equipe?
<br class="max-lg:hidden" />
Fale com nossa equipe e receba uma proposta personalizada.
</p>
</div>
</div>
<Card color="zinc" className="rounded-xl p-4">
<Form client:load />
</Card>
</section>
-->
</Layout> </Layout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 KiB