diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..3452b5c --- /dev/null +++ b/.env.template @@ -0,0 +1 @@ +VITE_API_URL= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1c66fdb..209707a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,26 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* -.eslintcache - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* +.eslintcache +.env + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/package.json b/package.json index e0d58e6..baa8dd4 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,13 @@ "preview": "vite preview" }, "dependencies": { + "@fontsource-variable/inter": "^5.2.8", + "@fontsource-variable/montserrat": "^5.2.8", + "framer-motion": "^12.26.2", "react": "^19.2.3", "react-dom": "^19.2.3", - "react-icons": "^5.5.0" + "react-icons": "^5.5.0", + "react-responsive": "^10.0.1" }, "devDependencies": { "@eslint/eslintrc": "^3.3.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c259acc..bb19713 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,15 @@ importers: .: dependencies: + '@fontsource-variable/inter': + specifier: ^5.2.8 + version: 5.2.8 + '@fontsource-variable/montserrat': + specifier: ^5.2.8 + version: 5.2.8 + framer-motion: + specifier: ^12.26.2 + version: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: specifier: ^19.2.3 version: 19.2.3 @@ -17,6 +26,9 @@ importers: react-icons: specifier: ^5.5.0 version: 5.5.0(react@19.2.3) + react-responsive: + specifier: ^10.0.1 + version: 10.0.1(react@19.2.3) devDependencies: '@eslint/eslintrc': specifier: ^3.3.3 @@ -367,6 +379,12 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fontsource-variable/inter@5.2.8': + resolution: {integrity: sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==} + + '@fontsource-variable/montserrat@5.2.8': + resolution: {integrity: sha512-d8wykq7GCKhEOlLwEuQIOK3w3qsXNxwDlH0meru4AZZzQ4/+rZyvHWKL6BBtHdmbovSHEEQDkwkD8qYUKlcFtQ==} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -954,6 +972,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-mediaquery@0.1.2: + resolution: {integrity: sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -1249,6 +1270,20 @@ packages: fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + framer-motion@12.26.2: + resolution: {integrity: sha512-lflOQEdjquUi9sCg5Y1LrsZDlsjrHw7m0T9Yedvnk7Bnhqfkc89/Uha10J3CFhkL+TCZVCRw9eUGyM/lyYhXQA==} + 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 + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1344,6 +1379,9 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + hyphenate-style-name@1.1.0: + resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1532,9 +1570,16 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + matchmediaquery@0.4.2: + resolution: {integrity: sha512-wrZpoT50ehYOudhDjt/YvUJc6eUzcdFPdmbizfgvswCKNHD1/OBOHYJpHie+HXpu6bSkEGieFMYk6VuutaiRfA==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -1557,6 +1602,12 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + motion-dom@12.26.2: + resolution: {integrity: sha512-KLMT1BroY8oKNeliA3JMNJ+nbCIsTKg6hJpDb4jtRAJ7nCKnnpg/LTq/NGqG90Limitz3kdAnAVXecdFVGlWTw==} + + motion-utils@12.24.10: + resolution: {integrity: sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1784,6 +1835,9 @@ packages: engines: {node: '>=14'} hasBin: true + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1801,6 +1855,15 @@ packages: peerDependencies: react: '*' + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-responsive@10.0.1: + resolution: {integrity: sha512-OM5/cRvbtUWEX8le8RCT8scA8y2OPtb0Q/IViEyCEM5FBN8lRrkUOZnu87I88A6njxDldvxG+rLBxWiA7/UM9g==} + engines: {node: '>=14'} + peerDependencies: + react: '>=16.8.0' + react@19.2.3: resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} @@ -1880,6 +1943,9 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + shallow-equal@3.1.0: + resolution: {integrity: sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2365,6 +2431,10 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@fontsource-variable/inter@5.2.8': {} + + '@fontsource-variable/montserrat@5.2.8': {} + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -2906,6 +2976,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-mediaquery@0.1.2: {} + cssesc@3.0.0: {} csstype@3.2.3: {} @@ -3303,6 +3375,15 @@ snapshots: fraction.js@5.3.4: {} + framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + motion-dom: 12.26.2 + motion-utils: 12.24.10 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + fsevents@2.3.3: optional: true @@ -3398,6 +3479,8 @@ snapshots: dependencies: hermes-estree: 0.25.1 + hyphenate-style-name@1.1.0: {} + ignore@5.3.2: {} ignore@7.0.5: {} @@ -3576,10 +3659,18 @@ snapshots: lodash.merge@4.6.2: {} + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + lru-cache@5.1.1: dependencies: yallist: 3.1.1 + matchmediaquery@0.4.2: + dependencies: + css-mediaquery: 0.1.2 + math-intrinsics@1.1.0: {} merge2@1.4.1: {} @@ -3599,6 +3690,12 @@ snapshots: minimist@1.2.8: {} + motion-dom@12.26.2: + dependencies: + motion-utils: 12.24.10 + + motion-utils@12.24.10: {} + ms@2.1.3: {} mz@2.7.0: @@ -3749,6 +3846,12 @@ snapshots: prettier@3.7.4: {} + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + punycode@2.3.1: {} queue-microtask@1.2.3: {} @@ -3762,6 +3865,16 @@ snapshots: dependencies: react: 19.2.3 + react-is@16.13.1: {} + + react-responsive@10.0.1(react@19.2.3): + dependencies: + hyphenate-style-name: 1.1.0 + matchmediaquery: 0.4.2 + prop-types: 15.8.1 + react: 19.2.3 + shallow-equal: 3.1.0 + react@19.2.3: {} read-cache@1.0.0: @@ -3886,6 +3999,8 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 + shallow-equal@3.1.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -4034,8 +4149,7 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 - tslib@2.8.1: - optional: true + tslib@2.8.1: {} type-check@0.4.0: dependencies: diff --git a/src/App.tsx b/src/App.tsx index eaf9389..e98a267 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ -import Landing from '@/components/Landing' +import LoginPage from '@pages/LoginPage' -function App() { - return +export const App = () => { + return } export default App diff --git a/src/assets/bg.webp b/src/assets/bg.webp new file mode 100644 index 0000000..2265da6 Binary files /dev/null and b/src/assets/bg.webp differ diff --git a/src/components/BufferringLogo.tsx b/src/components/BufferringLogo.tsx new file mode 100644 index 0000000..9fe064b --- /dev/null +++ b/src/components/BufferringLogo.tsx @@ -0,0 +1,30 @@ +import { type SVGProps, type FC } from 'react' + +export type BufferringLogoProps = SVGProps & { + className?: string +} + +export const BufferringLogo: FC = ({ className }) => { + return ( + + + + + + ) +} + +export default BufferringLogo diff --git a/src/components/Button.tsx b/src/components/Button.tsx new file mode 100644 index 0000000..e97e254 --- /dev/null +++ b/src/components/Button.tsx @@ -0,0 +1,48 @@ +// Provides a reusable button component with predefined styling. + +import { type ReactNode, type ButtonHTMLAttributes, forwardRef, useMemo } from 'react' + +// Defines allowed color variants. +type ColorsType = 'primary' + +// Props for the Button component. +export interface ButtonProps extends ButtonHTMLAttributes { + children: ReactNode + color?: ColorsType +} + +// Maps color variants to Tailwind class strings. +const styles: Record = { + primary: { + classes: 'text-white bg-blue-600 hover:bg-blue-500 active:bg-blue-400', + }, +} + +// Button component that forwards its ref and applies computed classes. +export const Button = forwardRef((props, ref) => { + const { children, color = 'primary', className, ...rest } = props + + // Memoizes the combined class list to avoid recomputation. + const classes = useMemo( + () => + Object.values({ + base: 'select-none rounded-full px-4 py-2 font-body text-sm font-semibold transition-colors duration-200 ease-in-out shadow-sm', + btn: styles[color].classes, + className, + }) + .map((variants) => variants) + .join(' '), + [color, className] + ) + + // Renders a button element with the computed classes and passes through other props. + return ( + + ) +}) + +// Sets component display name for React DevTools. +Button.displayName = 'Button' +export default Button diff --git a/src/components/Landing.tsx b/src/components/Landing.tsx deleted file mode 100644 index 7aecdb6..0000000 --- a/src/components/Landing.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { FaGoogle, FaGithub, FaInstagram } from 'react-icons/fa' - -function Landing() { - return ( -
-
-
-
-
- UNEFA Codex -
- -

Welcome Back

- - -
- -
- - - -
-
- sigue a bufferring -
-
- -
- - - -
-
- -

Somo la cabra lo reye con lo dioses

-
- -
-
-
- ) -} - -export default Landing diff --git a/src/components/Logo.tsx b/src/components/Logo.tsx new file mode 100644 index 0000000..ff28a63 --- /dev/null +++ b/src/components/Logo.tsx @@ -0,0 +1,135 @@ +import { type SVGProps, type FC, useMemo } from 'react' + +export type LogoProps = SVGProps & { + className?: string +} + +export const Logo: FC = ({ className }) => { + const combinedClass = useMemo(() => `fill-current ${className ?? ''}`.trim(), [className]) + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +export default Logo diff --git a/src/components/Select.tsx b/src/components/Select.tsx new file mode 100644 index 0000000..31bc4e1 --- /dev/null +++ b/src/components/Select.tsx @@ -0,0 +1,321 @@ +// This component renders a custom accessible dropdown select with keyboard and pointer support + +import React, { useEffect, useRef, useState } from 'react' +import { motion, AnimatePresence } from 'framer-motion' +import { IoIosArrowForward } from 'react-icons/io' +import { FaCheck } from 'react-icons/fa6' + +type Option = { value: string; label: string; disabled?: boolean } + +type SelectProps = { + name?: string + options: Option[] + value?: string | null + onChange: (value: string) => void + placeholder?: string + className?: string + id?: string +} + +// Render a fully featured select component +export const Select: React.FC = ({ + name, + options, + value = null, + onChange, + placeholder = 'Select', + className = 'w-full', + id = '', +}) => { + const uid = id + const buttonId = `select-button-${uid}` + const listId = `select-list-${uid}` + + const [open, setOpen] = useState(false) + const [highlighted, setHighlighted] = useState(null) + + const buttonRef = useRef(null) + const divListRef = useRef(null) + + // Track active pointer to differentiate tap from drag + const activePointerRef = useRef<{ + id: number + startX: number + startY: number + moved: boolean + } | null>(null) + + // Update highlighted option when dropdown opens or value changes + useEffect(() => { + if (!open) { + setHighlighted(null) + return + } + const idx = options.findIndex((o) => o.value === value && !o.disabled) + if (idx >= 0) { + setHighlighted(idx) + } else { + const first = options.findIndex((o) => !o.disabled) + setHighlighted(first >= 0 ? first : null) + } + }, [open, value, options]) + + // Close dropdown when clicking outside of button or list + useEffect(() => { + function onDoc(e: PointerEvent) { + const target = e.target as Node + if (!buttonRef.current?.contains(target) && !divListRef.current?.contains(target)) { + setOpen(false) + } + } + document.addEventListener('pointerdown', onDoc) + return () => document.removeEventListener('pointerdown', onDoc) + }, []) + + // Scroll highlighted option into view if needed + const scrollIntoViewIfNeeded = (idx: number) => { + const item = divListRef.current?.querySelector(`[data-option-index="${idx}"]`) + if (item) item.scrollIntoView({ block: 'nearest' }) + } + + // Move highlight forward or backward, skipping disabled options + const moveHighlight = (dir: 1 | -1) => { + if (options.length === 0) return + let idx = highlighted ?? -1 + for (let i = 0; i < options.length; i++) { + idx = (idx + dir + options.length) % options.length + if (!options[idx].disabled) { + setHighlighted(idx) + scrollIntoViewIfNeeded(idx) + break + } + } + } + + // Select option by index and close dropdown + const selectByIndex = (idx: number | null) => { + if (idx == null) return + const opt = options[idx] + if (!opt || opt.disabled) return + onChange(opt.value) + setOpen(false) + buttonRef.current?.focus() + } + + // Shortcut to select currently highlighted option + const selectHighlighted = () => selectByIndex(highlighted) + + // Handle keyboard navigation and selection + const handleKeyDown = (e: React.KeyboardEvent) => { + const key = e.key + if (key === 'ArrowDown') { + e.preventDefault() + if (!open) setOpen(true) + else moveHighlight(1) + } else if (key === 'ArrowUp') { + e.preventDefault() + if (!open) setOpen(true) + else moveHighlight(-1) + } else if (key === 'Home') { + e.preventDefault() + const idx = options.findIndex((o) => !o.disabled) + if (idx >= 0) { + setHighlighted(idx) + scrollIntoViewIfNeeded(idx) + } + } else if (key === 'End') { + e.preventDefault() + for (let i = options.length - 1; i >= 0; i--) { + if (!options[i].disabled) { + setHighlighted(i) + scrollIntoViewIfNeeded(i) + break + } + } + } else if (key === 'Enter' || key === ' ') { + e.preventDefault() + if (!open) setOpen(true) + else selectHighlighted() + } else if (key === 'Escape') { + e.preventDefault() + setOpen(false) + buttonRef.current?.focus() + } else if (key.length === 1) { + const char = key.toLowerCase() + const idx = options.findIndex((o) => !o.disabled && o.label.toLowerCase().startsWith(char)) + if (idx >= 0) { + setHighlighted(idx) + scrollIntoViewIfNeeded(idx) + } + } + } + + // Minimum movement in pixels to treat pointer action as a drag + const MOVE_THRESHOLD = 8 + + return ( +
+ {/* Button that toggles the dropdown */} + + + + {open && ( + +
    + {options.map((opt, idx) => { + const selected = value === opt.value + const isHighlighted = highlighted === idx + + // Pointer down handler to start tracking movement + const handlePointerDown = (e: React.PointerEvent) => { + if (opt.disabled) return + activePointerRef.current = { + id: e.pointerId, + startX: e.clientX, + startY: e.clientY, + moved: false, + } + try { + e.currentTarget.setPointerCapture(e.pointerId) + } catch { + { + /* ignore */ + } + } + } + + // Pointer move handler to detect drag distance + const handlePointerMove = (e: React.PointerEvent) => { + const s = activePointerRef.current + if (!s || s.id !== e.pointerId) return + const dx = Math.abs(e.clientX - s.startX) + const dy = Math.abs(e.clientY - s.startY) + if (dx > MOVE_THRESHOLD || dy > MOVE_THRESHOLD) { + s.moved = true + } + } + + // Pointer up handler to finalize tap selection + const handlePointerUp = (e: React.PointerEvent) => { + const s = activePointerRef.current + if (!s || s.id !== e.pointerId) { + try { + e.currentTarget.releasePointerCapture(e.pointerId) + } catch { + { + /* */ + } + } + return + } + try { + e.currentTarget.releasePointerCapture(e.pointerId) + } catch { + { + /* */ + } + } + const moved = s.moved + activePointerRef.current = null + if (!moved && !opt.disabled) { + onChange(opt.value) + setOpen(false) + buttonRef.current?.focus() + } + } + + return ( +
  • !opt.disabled && setHighlighted(idx)} + onPointerDown={handlePointerDown} + onPointerMove={handlePointerMove} + onPointerUp={handlePointerUp} + > + {opt.label} + + {selected && ( + + + + )} +
  • + ) + })} +
+
+ )} +
+ + {name && ( + + )} +
+ ) +} + +export default Select diff --git a/src/config/env.ts b/src/config/env.ts new file mode 100644 index 0000000..7c20f69 --- /dev/null +++ b/src/config/env.ts @@ -0,0 +1,16 @@ +interface EnvConfig { + readonly API_URL: string +} + +function loadEnv(): EnvConfig { + const rawEnv = import.meta.env + + const config: EnvConfig = { + API_URL: rawEnv.VITE_API_URL ?? window.location.origin, + } + + return config +} + +export const envConfig = loadEnv() +export default envConfig diff --git a/src/main.tsx b/src/main.tsx index 3e9143b..960f7f8 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,22 @@ +import montserratUrl from '@fontsource-variable/montserrat/files/montserrat-latin-wght-normal.woff2?url' +import interUrl from '@fontsource-variable/inter/files/inter-latin-wght-normal.woff2?url' + +for (const url of [montserratUrl, interUrl]) { + const link = document.createElement('link') + link.rel = 'preload' + link.href = url + link.as = 'font' + link.type = 'font/woff2' + link.crossOrigin = 'anonymous' + document.head.appendChild(link) +} + +import '@styles/global.css' +import '@styles/fonts.css' + import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' -import '@/styles/global.css' + import App from '@/App.tsx' createRoot(document.getElementById('root')!).render( diff --git a/src/pages/LoginPage/components/AsideContent.tsx b/src/pages/LoginPage/components/AsideContent.tsx new file mode 100644 index 0000000..2292f23 --- /dev/null +++ b/src/pages/LoginPage/components/AsideContent.tsx @@ -0,0 +1,111 @@ +// Aside panel for selecting a workspace and providing social media links. + +import React from 'react' + +import { FaGithub, FaInstagram, FaTiktok } from 'react-icons/fa' +import { FaArrowRightToBracket } from 'react-icons/fa6' + +import Logo from '@components/Logo' +import BufferringLogo from '@components/BufferringLogo' + +import Select from '@components/Select' +import Button from '@components/Button' + +interface Props { + workspacesNum: number + workspace: string | null + onSelectWorkspace: (value: string) => void +} + +// AsideContent: functional component rendering UI for workspace selection and navigation. +const AsideContent: React.FC = ({ workspace, onSelectWorkspace, workspacesNum }) => ( +
+ {/* Application logo */} + + +

+ UNEFA Codex +

+ +

+ ¡Bienvenido de Vuelta! +

+ + {/* Workspace selection section */} +
+ +