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 boto3clients import dynamodb_client
from course import create_course, update_course
from middlewares import AuditLogMiddleware, Tenant, TenantMiddleware
from models import Course, Org
from rules.course import create_course, update_course
from settings import (
COURSE_TABLE,
MEILISEARCH_API_KEY,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,14 @@
import clsx from "clsx";
import clsx from 'clsx'
interface ContainerProps {
children: React.ReactNode;
className?: string | undefined;
children: React.ReactNode
className?: string | undefined
}
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 { useMutation } from "node_modules/@tanstack/react-query/build/legacy";
import { queryClient } from "../queryClient";
import axios from "axios";
import { createElement } from "react";
import clsx from "clsx";
import { useForm } from 'react-hook-form'
import { useMutation } from 'node_modules/@tanstack/react-query/build/legacy'
import { queryClient } from '../queryClient'
import axios from 'axios'
import { createElement } from 'react'
import clsx from 'clsx'
interface IFormInput {
name: string;
email: string;
message: string;
name: string
email: string
message: string
}
export function Form() {
const { register, handleSubmit, reset, formState } = useForm<IFormInput>();
const { register, handleSubmit, reset, formState } = useForm<IFormInput>()
const { mutateAsync } = useMutation(
{
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: () => {
reset();
reset()
},
},
queryClient,
);
)
const onSubmit = async (data: IFormInput) => {
await mutateAsync(data);
};
await mutateAsync(data)
}
return (
<form
@@ -41,17 +41,17 @@ export function Form() {
<div className="grid lg:grid-cols-2 gap-3 lg:gap-6">
<label>
Nome
<Input {...register("name")} />
<Input {...register('name')} />
</label>
<label>
Email
<Input {...register("email")} />
<Input {...register('email')} />
</label>
</div>
<label>
Mensagem
<Input as="textarea" className="h-26" {...register("message")} />
<Input as="textarea" className="h-26" {...register('message')} />
</label>
<button
@@ -61,20 +61,20 @@ export function Form() {
Quero um orçamento
</button>
</form>
);
)
}
interface IInput extends React.HTMLAttributes<HTMLElement> {
as?: string;
className?: string | undefined;
as?: string
className?: string | undefined
}
export function Input({ as = "input", className, ...props }: IInput) {
export function Input({ as = 'input', className, ...props }: IInput) {
return createElement(as, {
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,
),
...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>
</svg>
);
)
}
export function Smallest(props) {
@@ -95,5 +95,5 @@ export function Smallest(props) {
</g>
</g>
</svg>
);
)
}

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

View File

@@ -1,77 +1,234 @@
---
import { Bookmark } from "@components/Bookmark";
import { Card } from "@components/Card";
import { Container } from "@components/Container";
import { Form } from "@components/Form";
import Layout from "@layouts/Layout.astro";
import { Image } from 'astro:assets'
import { Card } from '@components/Card'
import { Container } from '@components/Container'
import { Form } from '@components/Form'
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>
<Container className="lg:space-y-5 lg:p-3">
<!-- <div class="max-lg:px-5">
<Bookmark className="h-96" />
</div> -->
<Card>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam
quis mattis tortor, sit amet mollis lorem. In imperdiet, ante
sit amet maximus dictum, est elit ultrices lacus, in placerat
ante risus vel massa. Maecenas porta purus non feugiat
venenatis. Sed tempus quam id commodo interdum. Aliquam id
ullamcorper diam. Morbi a porttitor tellus. Fusce viverra
euismod laoreet. Cras id sapien quis orci rutrum lacinia. Donec
vitae libero at felis auctor blandit commodo sed libero. Aliquam
tellus risus, sagittis a libero eget, hendrerit feugiat mauris.
Ut vehicula id est non iaculis. Suspendisse potenti. Maecenas in
tellus risus. Proin quis libero et ero s ullamcorper faucibus
non vitae lacus. Vestibulum at ultricies sem, vel euismod dolor.
</p><p>
In tempor vel felis ut imperdiet. Pellentesque ac vulputate
lorem, id pellentesque velit. Sed rutrum, nisi vel convallis
rhoncus, ex mi vulputate leo, id hendrerit ipsum sapien in
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>
<Container className="py-3 lg:py-12 lg:flex items-center gap-5">
<Image
src={nr18plataforma}
alt="NR-18"
class="size-1/3 object-bottom hidden lg:block"
/>
<section>
<div class="space-y-5">
<span class="font-medium">Curso de formação</span>
<h1 class="font-semibold text-5xl lg:text-7xl">
NR-18 PEMT Plataforma Móvel de Trabalho Aéreo
</h1>
<p class="text-base/6">
NR 18 PEMT capacita operadores de plataformas elevatórias para
trabalhos em altura com segurança. Com foco na manutenção, inspeção e
uso correto dos EPIs, previne sempre acidentes, garante certificação
MTE e valoriza sua carreira.
</p>
<ul class="lg:flex gap-3">
<li class="flex gap-1">
<ClockIcon className="w-5" />
<span>Carga horária de 40 horas</span>
</li>
<Card color="darker">
<p>
In tempor vel felis ut imperdiet. Pellentesque ac vulputate
lorem, id pellentesque velit. Sed rutrum, nisi vel convallis
rhoncus, ex mi vulputate leo, id hendrerit ipsum sapien in
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>
<li class="flex gap-1">
<CheckBadgeIcon className="w-5 fill-blue-400" />
<span>Certificado com assinatura digital</span>
</li>
</ul>
<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
class="flex max-lg:flex-col justify-center gap-2.5 lg:gap-8 lg:mt-16"
>
<a
href="#"
class="text-black font-semibold bg-lime-400 rounded p-3.5 hover:bg-white max-lg:text-center"
>
Contratar agora
</a>
<a href="http://bit.ly/3RlROu6" class="flex flex-col">
<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>
<Card color="zinc" className="rounded-xl p-4">
<Form client:load />
</Card>
<span>66 avaliações no Google</span>
</a>
</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>
</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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 KiB