diff --git a/api.saladeaula.digital/app/exceptions.py b/api.saladeaula.digital/app/exceptions.py index 2a0fd75..26b5463 100644 --- a/api.saladeaula.digital/app/exceptions.py +++ b/api.saladeaula.digital/app/exceptions.py @@ -1,6 +1,7 @@ from http import HTTPStatus from aws_lambda_powertools.event_handler.exceptions import ( + NotFoundError, ServiceError, ) @@ -8,3 +9,24 @@ from aws_lambda_powertools.event_handler.exceptions import ( class ConflictError(ServiceError): def __init__(self, msg: str | dict): super().__init__(HTTPStatus.CONFLICT, msg) + + +class UserNotFoundError(NotFoundError): ... + + +class EmailNotFoundError(NotFoundError): ... + + +class EmailVerificationNotFoundError(NotFoundError): ... + + +class UserConflictError(ConflictError): ... + + +class EmailConflictError(ConflictError): ... + + +class CPFConflictError(ConflictError): ... + + +class CancelPolicyConflictError(ConflictError): ... diff --git a/api.saladeaula.digital/app/routes/enrollments/cancel.py b/api.saladeaula.digital/app/routes/enrollments/cancel.py index 900d562..da08cd2 100644 --- a/api.saladeaula.digital/app/routes/enrollments/cancel.py +++ b/api.saladeaula.digital/app/routes/enrollments/cancel.py @@ -2,15 +2,13 @@ from typing import Annotated from aws_lambda_powertools import Logger from aws_lambda_powertools.event_handler.api_gateway import Router -from aws_lambda_powertools.event_handler.exceptions import ( - BadRequestError, -) from aws_lambda_powertools.event_handler.openapi.params import Body from layercake.dateutils import now from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair from boto3clients import dynamodb_client from config import ENROLLMENT_TABLE +from exceptions import CancelPolicyConflictError from middlewares.authentication_middleware import User as Authenticated logger = Logger(__name__) @@ -18,9 +16,6 @@ router = Router() dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) -class CancelPolicyConflictError(BadRequestError): ... - - @router.patch('//cancel') def cancel( enrollment_id: str, diff --git a/api.saladeaula.digital/app/routes/orgs/users/add.py b/api.saladeaula.digital/app/routes/orgs/users/add.py index 80b7d81..dcb8b38 100644 --- a/api.saladeaula.digital/app/routes/orgs/users/add.py +++ b/api.saladeaula.digital/app/routes/orgs/users/add.py @@ -13,7 +13,12 @@ from pydantic import BaseModel, EmailStr, Field from api_gateway import JSONResponse from boto3clients import dynamodb_client from config import INTERNAL_EMAIL_DOMAIN, USER_TABLE -from exceptions import ConflictError +from exceptions import ( + CPFConflictError, + EmailConflictError, + UserConflictError, + UserNotFoundError, +) from middlewares.authentication_middleware import User as Authenticated router = Router() @@ -32,18 +37,6 @@ class User(BaseModel): email: EmailStr -class CPFConflictError(ConflictError): ... - - -class EmailConflictError(ConflictError): ... - - -class UserConflictError(ConflictError): ... - - -class UserNotFoundError(NotFoundError): ... - - class OrgNotFoundError(NotFoundError): ... diff --git a/api.saladeaula.digital/app/routes/users/emails.py b/api.saladeaula.digital/app/routes/users/emails.py index 99b9ef1..b4e0efa 100644 --- a/api.saladeaula.digital/app/routes/users/emails.py +++ b/api.saladeaula.digital/app/routes/users/emails.py @@ -2,9 +2,6 @@ from http import HTTPStatus from uuid import uuid4 from aws_lambda_powertools.event_handler.api_gateway import Router -from aws_lambda_powertools.event_handler.exceptions import ( - NotFoundError, -) from aws_lambda_powertools.event_handler.openapi.params import Body, Path, Query from layercake.dateutils import now, ttl from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey, TransactKey @@ -14,24 +11,17 @@ from typing_extensions import Annotated from api_gateway import JSONResponse from boto3clients import dynamodb_client from config import USER_TABLE -from exceptions import ConflictError +from exceptions import ( + EmailConflictError, + EmailNotFoundError, + EmailVerificationNotFoundError, + UserNotFoundError, +) router = Router() dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) -class UserNotFoundError(NotFoundError): ... - - -class EmailNotFoundError(NotFoundError): ... - - -class EmailVerificationNotFoundError(NotFoundError): ... - - -class EmailConflictError(ConflictError): ... - - @router.get('//emails') def get_emails(user_id: str, start_key: Annotated[str | None, Query] = None): return dyn.collection.query( diff --git a/api.saladeaula.digital/app/routes/users/password.py b/api.saladeaula.digital/app/routes/users/password.py index 02225f3..c302985 100644 --- a/api.saladeaula.digital/app/routes/users/password.py +++ b/api.saladeaula.digital/app/routes/users/password.py @@ -2,7 +2,6 @@ from http import HTTPStatus from typing import Annotated from aws_lambda_powertools.event_handler.api_gateway import Router -from aws_lambda_powertools.event_handler.exceptions import NotFoundError from aws_lambda_powertools.event_handler.openapi.params import Body from layercake.dateutils import now from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair @@ -11,14 +10,12 @@ from passlib.hash import pbkdf2_sha256 from api_gateway import JSONResponse from boto3clients import dynamodb_client from config import USER_TABLE +from exceptions import UserNotFoundError router = Router() dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) -class UserNotFoundError(NotFoundError): ... - - @router.post('//password') def password( user_id: str, diff --git a/api.saladeaula.digital/uv.lock b/api.saladeaula.digital/uv.lock index 43ed342..e5dc835 100644 --- a/api.saladeaula.digital/uv.lock +++ b/api.saladeaula.digital/uv.lock @@ -11,6 +11,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] +[[package]] +name = "anyio" +version = "4.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, +] + [[package]] name = "api-saladeaula-digital" version = "0.1.0" @@ -323,6 +335,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/1a/aff8bb287a4b1400f69e09a53bd65de96aa5cee5691925b38731c67fc695/click_default_group-1.2.4-py2.py3-none-any.whl", hash = "sha256:9b60486923720e7fc61731bdb32b617039aba820e22e1c88766b1125592eaa5f", size = 4123, upload-time = "2023-08-04T07:54:56.875Z" }, ] +[[package]] +name = "cloudflare" +version = "4.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/48/e481c0a9b9010a5c41b5ca78ff9fbe00dc8a9a4d39da5af610a4ec49c7f7/cloudflare-4.3.1.tar.gz", hash = "sha256:b1e1c6beeb8d98f63bfe0a1cba874fc4e22e000bcc490544f956c689b3b5b258", size = 1933187, upload-time = "2025-06-16T21:43:18.716Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/8f/c6c543565efd3144da4304efa5917aac06b6416a8663a6defe0e9b2b7569/cloudflare-4.3.1-py3-none-any.whl", hash = "sha256:6927135a5ee5633d6e2e1952ca0484745e933727aeeb189996d2ad9d292071c6", size = 4406465, upload-time = "2025-06-16T21:43:17.3Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -458,6 +487,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/47/ef/4cb333825d10317a36a1154341ba37e6e9c087bac99c1990ef07ffdb376f/dictdiffer-0.9.0-py2.py3-none-any.whl", hash = "sha256:442bfc693cfcadaf46674575d2eba1c53b42f5e404218ca2c2ff549f2df56595", size = 16754, upload-time = "2021-07-22T13:24:26.783Z" }, ] +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + [[package]] name = "dnspython" version = "2.8.0" @@ -527,6 +565,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9c/a2/75fd80784ec33da8d39cf885e8811a4fbc045a90db5e336b8e345e66dbb2/glom-24.11.0-py3-none-any.whl", hash = "sha256:991db7fcb4bfa9687010aa519b7b541bbe21111e70e58fdd2d7e34bbaa2c1fbd", size = 102690, upload-time = "2024-11-02T23:17:46.468Z" }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + [[package]] name = "idna" version = "3.10" @@ -592,12 +667,13 @@ wheels = [ [[package]] name = "layercake" -version = "0.11.2" +version = "0.11.3" source = { directory = "../layercake" } dependencies = [ { name = "arnparse" }, { name = "authlib" }, { name = "aws-lambda-powertools", extra = ["all"] }, + { name = "cloudflare" }, { name = "dictdiffer" }, { name = "ftfy" }, { name = "glom" }, @@ -623,6 +699,7 @@ requires-dist = [ { name = "arnparse", specifier = ">=0.0.2" }, { name = "authlib", specifier = ">=1.6.5" }, { name = "aws-lambda-powertools", extras = ["all"], specifier = ">=3.23.0" }, + { name = "cloudflare", specifier = ">=4.3.1" }, { name = "dictdiffer", specifier = ">=0.9.0" }, { name = "ftfy", specifier = ">=6.3.1" }, { name = "glom", specifier = ">=24.11.0" }, @@ -1166,6 +1243,15 @@ s3 = [ { name = "boto3" }, ] +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + [[package]] name = "sqlite-fts4" version = "1.0.3" diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.courses._index/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.courses._index/route.tsx index b9d1108..0d8c2b0 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.courses._index/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.courses._index/route.tsx @@ -27,6 +27,7 @@ import { createSearch } from '@repo/util/meili' import { request as req } from '@repo/util/request' import placeholder from '@/assets/placeholder.webp' +import { Currency } from '@repo/ui/components/currency' type Cert = { exp_interval: number @@ -220,12 +221,12 @@ function Course({ 'font-bold': !custom_pricing })} > - {currency.format(metadata__unit_price)} + {metadata__unit_price} {custom_pricing && ( - {currency.format(custom_pricing)} + {custom_pricing} )} diff --git a/apps/admin.saladeaula.digital/app/routes/_.setup.cnpj[.]son/route.ts b/apps/admin.saladeaula.digital/app/routes/_.setup.cnpj[.]son/route.ts new file mode 100644 index 0000000..5b7f5f7 --- /dev/null +++ b/apps/admin.saladeaula.digital/app/routes/_.setup.cnpj[.]son/route.ts @@ -0,0 +1,65 @@ +import type { Route } from './+types/route' + +import { data } from 'react-router' + +export async function loader({ params, context, request }: Route.LoaderArgs) { + const url = new URL(request.url) + const cnpj = url.searchParams.get('cnpj') + + const r = await fetch(`https://brasilapi.com.br/api/cnpj/v1/${cnpj}`, { + method: 'GET', + headers: { + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', + Accept: 'application/json' + } + }) + + if (!r.ok) { + throw new Response(await r.text(), { status: r.status }) + } + + return data({}) +} + +// export const prerender = false +// import type { APIRoute } from 'astro' +// import lodash from 'lodash' + +// export const GET: APIRoute = async ({ params }) => { +// // await new Promise((r) => setTimeout(r, 2000)) + +// const cnpj = params.cnpj +// const res = await fetch(`https://brasilapi.com.br/api/cnpj/v1/${cnpj}`, { +// method: 'GET', +// headers: { +// 'User-Agent': +// 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', +// Accept: 'application/json' +// } +// }) + +// if (!res.ok) { +// return new Response(null, { +// status: 404 +// }) +// } + +// const json = await res.json() +// const addr = lodash.startCase( +// lodash.toLower(`${json.descricao_tipo_de_logradouro} ${json.logradouro}`) +// ) + +// return new Response( +// JSON.stringify({ +// name: json.razao_social, +// address: { +// postcode: json.cep, +// address1: `${addr}, ${json.numero}`, +// neighborhood: lodash.capitalize(json.bairro), +// city: lodash.capitalize(json.municipio), +// state: json.uf +// } +// }) +// ) +// } diff --git a/apps/saladeaula.digital/app/routes.ts b/apps/saladeaula.digital/app/routes.ts index 06a55f0..07c7dfb 100644 --- a/apps/saladeaula.digital/app/routes.ts +++ b/apps/saladeaula.digital/app/routes.ts @@ -8,6 +8,7 @@ import { export default [ layout('routes/layout.tsx', [ index('routes/index.tsx'), + route('catalog', 'routes/catalog.tsx'), route('certs', 'routes/certs.tsx'), route('history', 'routes/history.tsx'), route('settings', 'routes/settings/layout.tsx', [ diff --git a/apps/saladeaula.digital/app/routes/catalog.tsx b/apps/saladeaula.digital/app/routes/catalog.tsx new file mode 100644 index 0000000..59f3e34 --- /dev/null +++ b/apps/saladeaula.digital/app/routes/catalog.tsx @@ -0,0 +1,136 @@ +import type { Route } from './+types/catalog' + +import { Fragment, Suspense } from 'react' +import { Await } from 'react-router' +import { ListFilterIcon, PlusIcon, SearchIcon } from 'lucide-react' + +import { cloudflareContext } from '@repo/auth/context' +import { createSearch } from '@repo/util/meili' +import { Skeleton } from '@repo/ui/components/skeleton' +import { + InputGroup, + InputGroupAddon, + InputGroupButton, + InputGroupInput +} from '@repo/ui/components/ui/input-group' +import { + Item, + ItemActions, + ItemContent, + ItemDescription, + ItemGroup, + ItemMedia, + ItemSeparator, + ItemTitle +} from '@repo/ui/components/ui/item' +import { Currency } from '@repo/ui/components/currency' +import { Avatar, AvatarImage } from '@repo/ui/components/ui/avatar' + +import { Container } from '@/components/container' +import placeholder from '@/assets/placeholder.webp' +import { Button } from '@repo/ui/components/ui/button' + +export function meta({}: Route.MetaArgs) { + return [{ title: 'Expore nossos cursos' }] +} + +export async function loader({ context, request, params }: Route.LoaderArgs) { + const cloudflare = context.get(cloudflareContext) + const courses = createSearch({ + index: 'saladeaula_courses', + sort: ['created_at:desc'], + filter: 'unlisted NOT EXISTS', + hitsPerPage: 100, + env: cloudflare.env + }) + + return { + courses + } +} + +export default function Component({ + loaderData: { courses } +}: Route.ComponentProps) { + return ( + + }> + {/* + + + + Meus cursos + + + + + Exporar mais cursos + + + */} + +
+

+ Expore nossos cursos +

+

+ Conheça todos os cursos que oferecemos, desenvolvidos para + fortalecer suas competências profissionais e apoiar seu crescimento + contínuo na carreira. +

+
+ + + + + + + + + + + + + {({ hits }) => ( + <> + + {hits + .filter( + ({ metadata__unit_price = 0 }) => metadata__unit_price > 0 + ) + .map(({ name, metadata__unit_price }, index) => ( + + + + + + + + + {name} + + {metadata__unit_price} + + + + + + + + {index !== hits.length - 1 && } + + ))} + + + )} + +
+
+ ) +} diff --git a/apps/saladeaula.digital/app/routes/index.tsx b/apps/saladeaula.digital/app/routes/index.tsx index 7b2fce5..7c32eb7 100644 --- a/apps/saladeaula.digital/app/routes/index.tsx +++ b/apps/saladeaula.digital/app/routes/index.tsx @@ -149,6 +149,10 @@ export default function Component({ )} + +
+ exporar mais +
) diff --git a/apps/saladeaula.digital/app/routes/layout.tsx b/apps/saladeaula.digital/app/routes/layout.tsx index 07160fa..96297e4 100644 --- a/apps/saladeaula.digital/app/routes/layout.tsx +++ b/apps/saladeaula.digital/app/routes/layout.tsx @@ -13,6 +13,7 @@ import { authMiddleware } from '@repo/auth/middleware/auth' import { ModeToggle, ThemedImage } from '@repo/ui/components/dark-mode' import { NavUser } from '@repo/ui/components/nav-user' import { Button } from '@repo/ui/components/ui/button' +import { HoverBorderGradient } from '@repo/ui/components/ui/hover-border-gradient' import { NavigationMenu, NavigationMenuLink, @@ -140,6 +141,11 @@ export default function Component({
+ + + Explorar mais cursos + +
diff --git a/id.saladeaula.digital/app/routes/authentication.py b/id.saladeaula.digital/app/routes/authentication.py index bd62c85..7e20202 100644 --- a/id.saladeaula.digital/app/routes/authentication.py +++ b/id.saladeaula.digital/app/routes/authentication.py @@ -39,7 +39,7 @@ def authentication( else: if not pbkdf2_sha256.verify(password, password_hash): dyn.update_item( - key=KeyPair(user_id, 'FAILED_ATTEMPTS'), + key=KeyPair(user_id, 'LOGIN#FAILED_ATTEMPTS'), update_expr='SET #count = if_not_exists(#count, :zero) + :one, \ updated_at = :now', expr_attr_names={ @@ -153,12 +153,12 @@ def new_session(user_id: str) -> str: with dyn.transact_writer() as transact: transact.delete( - key=KeyPair(user_id, 'FAILED_ATTEMPTS'), + key=KeyPair(user_id, 'LOGIN#FAILED_ATTEMPTS'), ) transact.update( key=KeyPair(user_id, '0'), # Post-migration (users): uncomment the following line - # update_expr='SET last_login = :now', + # update_expr='SET last_login_at = :now', update_expr='SET lastLogin = :now', expr_attr_values={ ':now': now_, diff --git a/package-lock.json b/package-lock.json index 42f8356..f98464c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5037,6 +5037,33 @@ "node": ">=18" } }, + "node_modules/framer-motion": { + "version": "12.23.26", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz", + "integrity": "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.23", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fs-extra": { "version": "11.3.2", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", @@ -5616,6 +5643,47 @@ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "license": "MIT" }, + "node_modules/motion": { + "version": "12.23.26", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.23.26.tgz", + "integrity": "sha512-Ll8XhVxY8LXMVYTCfme27WH2GjBrCIzY4+ndr5QKxsK+YwCtOi2B/oBi5jcIbik5doXuWT/4KKDOVAZJkeY5VQ==", + "license": "MIT", + "dependencies": { + "framer-motion": "^12.23.26", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -7382,6 +7450,7 @@ "date-fns": "^4.1.0", "lodash": "^4.17.21", "lucide-react": "^0.556.0", + "motion": "^12.23.26", "next-themes": "^0.4.6", "postcss": "^8.5.6", "react-day-picker": "^9.11.3", diff --git a/packages/ui/components.json b/packages/ui/components.json index 778ea14..2d24c0b 100644 --- a/packages/ui/components.json +++ b/packages/ui/components.json @@ -10,6 +10,7 @@ "cssVariables": true, "prefix": "" }, + "iconLibrary": "lucide", "aliases": { "components": "@/components", "utils": "@/lib/utils", @@ -17,5 +18,7 @@ "lib": "@/lib", "hooks": "@/hooks" }, - "iconLibrary": "lucide" + "registries": { + "@aceternity": "https://ui.aceternity.com/registry/{name}.json" + } } diff --git a/packages/ui/package.json b/packages/ui/package.json index e37b956..e31aa06 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -42,6 +42,7 @@ "date-fns": "^4.1.0", "lodash": "^4.17.21", "lucide-react": "^0.556.0", + "motion": "^12.23.26", "next-themes": "^0.4.6", "postcss": "^8.5.6", "react-day-picker": "^9.11.3", diff --git a/packages/ui/src/components/ui/hover-border-gradient.tsx b/packages/ui/src/components/ui/hover-border-gradient.tsx new file mode 100644 index 0000000..1afdd19 --- /dev/null +++ b/packages/ui/src/components/ui/hover-border-gradient.tsx @@ -0,0 +1,99 @@ +"use client"; +import React, { useState, useEffect, useRef } from "react"; + +import { motion } from "motion/react"; +import { cn } from "@/lib/utils"; + +type Direction = "TOP" | "LEFT" | "BOTTOM" | "RIGHT"; + +export function HoverBorderGradient({ + children, + containerClassName, + className, + as: Tag = "button", + duration = 1, + clockwise = true, + ...props +}: React.PropsWithChildren< + { + as?: React.ElementType; + containerClassName?: string; + className?: string; + duration?: number; + clockwise?: boolean; + } & React.HTMLAttributes +>) { + const [hovered, setHovered] = useState(false); + const [direction, setDirection] = useState("TOP"); + + const rotateDirection = (currentDirection: Direction): Direction => { + const directions: Direction[] = ["TOP", "LEFT", "BOTTOM", "RIGHT"]; + const currentIndex = directions.indexOf(currentDirection); + const nextIndex = clockwise + ? (currentIndex - 1 + directions.length) % directions.length + : (currentIndex + 1) % directions.length; + return directions[nextIndex]; + }; + + const movingMap: Record = { + TOP: "radial-gradient(20.7% 50% at 50% 0%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)", + LEFT: "radial-gradient(16.6% 43.1% at 0% 50%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)", + BOTTOM: + "radial-gradient(20.7% 50% at 50% 100%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)", + RIGHT: + "radial-gradient(16.2% 41.199999999999996% at 100% 50%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)", + }; + + const highlight = + "radial-gradient(75% 181.15942028985506% at 50% 50%, #3275F8 0%, rgba(255, 255, 255, 0) 100%)"; + + useEffect(() => { + if (!hovered) { + const interval = setInterval(() => { + setDirection((prevState) => rotateDirection(prevState)); + }, duration * 1000); + return () => clearInterval(interval); + } + }, [hovered]); + return ( + ) => { + setHovered(true); + }} + onMouseLeave={() => setHovered(false)} + className={cn( + "relative flex rounded-full border content-center bg-black/20 hover:bg-black/10 transition duration-500 dark:bg-white/20 items-center flex-col flex-nowrap gap-10 h-min justify-center overflow-visible p-px decoration-clone w-fit", + containerClassName + )} + {...props} + > +
+ {children} +
+ +
+ + ); +}