Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/atoms/Icons/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './AcademicRecord'
export * from './AlertInfo'
export * from './ArrowRight'
export * from './Calendar'
export * from './Certificate'
export * from './CircularCheck'
export * from './CircularInformation'
Expand Down
161 changes: 161 additions & 0 deletions src/documentation/pages/Molecules/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { Box } from '@chakra-ui/react'
import { Avatar } from '@molecules'
import { ListComponent, MyHeading, MyText, MyTitle, Code } from '@/documentation/components'

export const ViewAvatar = (): JSX.Element => {
return (
<>
<MyHeading>Avatar</MyHeading>
<MyText>
Muestra la foto de perfil de un usuario dentro de un círculo. Si la imagen no está
disponible o no se proporciona, muestra las primeras dos iniciales del nombre completo sobre
un fondo de color determinista según el <code>userId</code>.
</MyText>
<Code text={`import { Avatar } from '@eclass/ui-kit'`} />

{/* ── Uso básico ─────────────────────────────────────────────── */}
<MyTitle>Uso básico</MyTitle>
<MyText>
Proporciona <code>fullName</code> y <code>userId</code>. Cuando no hay imagen, se renderizan
las iniciales con un color de fondo único para ese usuario.
</MyText>
<ListComponent>
<Avatar fullName="Josefin Ferrada" userId={564654} size={40} />
<Avatar fullName="Carlos López" userId={12} size={40} />
<Avatar fullName="Ana María Torres" userId={7} size={40} />
</ListComponent>
<Code
text={`<Avatar fullName="Josefin Ferrada" userId={564654} size={40} />
<Avatar fullName="Carlos López" userId={12} size={40} />
<Avatar fullName="Ana María Torres" userId={7} size={40} />`}
/>

{/* ── Con imagen ─────────────────────────────────────────────── */}
<MyTitle>Con foto de perfil</MyTitle>
<MyText>
Cuando se proporciona <code>picture</code>, el componente intenta cargar la imagen. Si la
URL es inválida, cae automáticamente en las iniciales.
</MyText>
<ListComponent>
<Avatar
fullName="Josefin Ferrada"
userId={564654}
picture="https://i.pravatar.cc/150?img=12"
size={40}
/>
<Avatar
fullName="Josefin Ferrada"
userId={3}
picture="https://url-que-no-existe.xyz/foto.jpg"
size={40}
/>
</ListComponent>
<Code
text={`<Avatar
fullName="Josefin Ferrada"
userId={564654}
picture="https://i.pravatar.cc/150?img=12"
size={40}
/>
{/* Si la URL falla, muestra las iniciales */}
<Avatar
fullName="Josefin Ferrada"
userId={3}
picture="https://url-que-no-existe.xyz/foto.jpg"
size={40}
/>`}
/>

{/* ── Tamaños ────────────────────────────────────────────────── */}
<MyTitle>Tamaños</MyTitle>
<MyText>
El prop <code>size</code> controla el diámetro en píxeles. El valor por defecto es{' '}
<code>50</code>. <code>fontSize</code> ajusta el tamaño de las iniciales (por defecto{' '}
<code>14</code>).
</MyText>
<ListComponent gap="1.5rem">
<Box display="flex" flexDir="column" alignItems="center" gap="0.25rem">
<Avatar fullName="Josefin Ferrada" userId={564654} size={24} fontSize={10} />
<code style={{ fontSize: '11px' }}>24px</code>
</Box>
<Box display="flex" flexDir="column" alignItems="center" gap="0.25rem">
<Avatar fullName="Josefin Ferrada" userId={564654} size={32} fontSize={12} />
<code style={{ fontSize: '11px' }}>32px</code>
</Box>
<Box display="flex" flexDir="column" alignItems="center" gap="0.25rem">
<Avatar fullName="Josefin Ferrada" userId={564654} size={40} fontSize={14} />
<code style={{ fontSize: '11px' }}>40px</code>
</Box>
<Box display="flex" flexDir="column" alignItems="center" gap="0.25rem">
<Avatar fullName="Josefin Ferrada" userId={564654} />
<code style={{ fontSize: '11px' }}>50px (default)</code>
</Box>
<Box display="flex" flexDir="column" alignItems="center" gap="0.25rem">
<Avatar fullName="Josefin Ferrada" userId={564654} size={64} fontSize={22} />
<code style={{ fontSize: '11px' }}>64px</code>
</Box>
</ListComponent>
<Code
text={`<Avatar fullName="Josefin Ferrada" userId={564654} size={24} fontSize={10} />
<Avatar fullName="Josefin Ferrada" userId={564654} size={32} fontSize={12} />
<Avatar fullName="Josefin Ferrada" userId={564654} size={40} fontSize={14} />
<Avatar fullName="Josefin Ferrada" userId={564654} /> {/* default */}
<Avatar fullName="Josefin Ferrada" userId={564654} size={64} fontSize={22} />`}
/>

{/* ── Color determinista ─────────────────────────────────────── */}
<MyTitle>Color por userId</MyTitle>
<MyText>
El color de fondo se calcula a partir del último dígito del <code>userId</code>, por lo que
siempre es consistente para el mismo usuario en todas las vistas.
</MyText>
<ListComponent gap="1rem">
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((digit) => (
<Box key={digit} display="flex" flexDir="column" alignItems="center" gap="0.25rem">
<Avatar fullName={`User ${digit}`} userId={digit} size={40} />
<code style={{ fontSize: '11px' }}>id: {digit}</code>
</Box>
))}
</ListComponent>
<Code
text={`{/* Cada userId termina en un dígito → 10 colores posibles */}
<Avatar fullName="User 0" userId={0} size={40} />
<Avatar fullName="User 1" userId={1} size={40} />
{/* ... hasta userId terminado en 9 */}`}
/>

{/* ── Borde decorativo ───────────────────────────────────────── */}
<MyTitle>Borde decorativo</MyTitle>
<MyText>
El prop <code>borderDecoration</code> añade un borde en el color primario del sistema de
diseño (<code>#0189FF</code>). Útil para destacar al usuario activo en un listado.
</MyText>
<ListComponent>
<Avatar fullName="Josefin Ferrada" userId={564654} size={40} />
<Avatar fullName="Josefin Ferrada" userId={564654} size={40} borderDecoration />
</ListComponent>
<Code
text={`<Avatar fullName="Josefin Ferrada" userId={564654} size={40} />
<Avatar fullName="Josefin Ferrada" userId={564654} size={40} borderDecoration />`}
/>

{/* ── Props ──────────────────────────────────────────────────── */}
<MyTitle>Props</MyTitle>
<Code
language="typescript"
text={`interface AvatarProps {
fullName: string // Nombre completo (para iniciales y accesibilidad)
userId: number // Determina el color de fondo de forma determinista
picture?: string // URL de la foto de perfil (opcional)
size?: number // Diámetro en px (default: 50)
fontSize?: number // Tamaño fuente de iniciales en px (default: 14)
alt?: string // Texto alternativo accesible (default: 'Avatar')
id?: string // ID del elemento
borderDecoration?: boolean // Borde en color primario (default: false)
}`}
/>
</>
)
}

export default ViewAvatar
142 changes: 142 additions & 0 deletions src/documentation/pages/Organisms/UserDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { Code, MyHeading, MyText, MyTitle } from '@/documentation/components'
import { UserDropdownMenu, ProfileMenuItem } from '@/organisms'
import { Box } from '@chakra-ui/react'
import { vars } from '@theme'

const user = {
name: 'Josefin ferrada',
id: 564654,
picture: 'https://i.pravatar.cc/150?img=12',
}

const onLogout = (): void => {
console.log('cerrar sesión')
}

const profileMenuItems: ProfileMenuItem[] = [
{ label: 'Mis Cursos activos', type: 'courses', href: '/courses' },
{ label: 'Mi Calendario', type: 'calendar', href: '/calendar' },
{ label: 'Mi Historial académico', type: 'academic-history', href: '/courses-past' },
{ label: 'Mi perfil', type: 'profile', href: '/profile' },
{ label: 'Cerrar sesión', type: 'logout', onClick: onLogout },
]

export const UserDropdownPage = (): JSX.Element => {
return (
<>
<MyHeading>UserDropdownMenu</MyHeading>

<MyText>
El componente <strong>UserDropdownMenu</strong> es un menú desplegable de perfil de usuario
accesible. Muestra un avatar como botón disparador que, al hacer clic, despliega un dropdown
con:
<ul style={{ listStylePosition: 'inside', marginTop: '8px' }}>
<li>El nombre del usuario en el encabezado del menú</li>
<li>Una lista de links de navegación</li>
<li>Un botón de cerrar sesión (opcional)</li>
</ul>
<br />
El componente es responsivo: en pantallas móviles ({'<'}480px) el menú ocupa toda la
pantalla con un overlay de fondo. Soporta navegación por teclado con las teclas de flecha y
es compatible con lectores de pantalla.
<br />
<br />
El estado del menú puede manejarse de forma <strong>interna</strong> (sin pasar props) o de
forma <strong>controlada</strong> externamente (pasando <code>isOpen</code>,{' '}
<code>onOpen</code> y <code>onClose</code>), lo que permite coordinarlo con otros menús.
</MyText>

<MyTitle>Demo</MyTitle>

<Box
bg="rgb(34, 34, 34)"
border={`1px solid ${vars('colors-neutral-platinum')}`}
display="flex"
justifyContent="end"
p="40px 10px"
position="relative"
zIndex={10} // Aseguramos que el menú esté por encima del resto
>
<UserDropdownMenu userInfo={user} menuItems={profileMenuItems} />
</Box>

<Box id="ViewContainer" mt="20px">
<MyTitle>Uso</MyTitle>
<MyText>Importa el componente desde el paquete de ui-kit:</MyText>
<Code language="jsx" text={`import { UserDropdownMenu } from '@eclass/ui-kit'`} />

<MyTitle>Uso básico</MyTitle>
<MyText>
Ejemplo mínimo con estado interno (el componente maneja su propio open/close):
</MyText>
<Code
language="jsx"
text={`import { UserDropdownMenu } from '@eclass/ui-kit'

const menuItems = [
{ label: 'Cursos', type: 'courses', href: '/courses' },
{ label: 'Mi perfil', type: 'profile', href: '/profile' },
{ label: 'Cerrar sesión', type: 'logout', onClick: () => console.log('logout') },
]

<UserDropdownMenu
userInfo={{
name: 'Josefin',
id: 12345,
picture: 'https://ejemplo.com/avatar.jpg',
}}
menuItems={menuItems}
/>`}
/>

<MyTitle>Uso controlado</MyTitle>
<MyText>
Pasa <code>isOpen</code>, <code>onOpen</code> y <code>onClose</code> para controlar el
estado externamente, útil cuando hay otros menús que deben cerrarse al abrir éste:
</MyText>
<Code
language="jsx"
text={`const [isOpen, setIsOpen] = useState(false)

<UserDropdownMenu
userInfo={userInfo}
menuItems={menuItems}
isOpen={isOpen}
onOpen={() => setIsOpen(true)}
onClose={() => setIsOpen(false)}
/>`}
/>

<MyTitle>Props</MyTitle>
<MyText>El componente acepta los siguientes props:</MyText>
<Code
language="tsx"
text={`type ProfileMenuItemType = 'courses' | 'calendar' | 'academic-history' | 'profile' | 'logout'

interface ProfileMenuItem {
label: string // Texto del link o botón
type?: ProfileMenuItemType // Tipo de ícono predefinido
href?: string // URL de destino (si es un link)
onClick?: () => void // Función a ejecutar (si es un botón)
}

interface AccesibleProfileMenuProps {
userInfo: {
name: string // Nombre del usuario (se muestra en el header del menú)
id: number // ID del usuario
picture: string // URL del avatar del usuario
}
menuItems: ProfileMenuItem[] // Lista de items del menú (links o acciones)

// Control externo del estado (opcional)
isOpen?: boolean
onOpen?: () => void
onClose?: () => void
}`}
/>
</Box>
</>
)
}

export default UserDropdownPage
12 changes: 12 additions & 0 deletions src/documentation/utils/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const Ripples = React.lazy(async () => await import('../pages/Atoms/Ripples'))
const TinyAlert = React.lazy(async () => await import('../pages/Atoms/TinyAlert'))

/** MOLECULES */
const Avatar = React.lazy(async () => await import('../pages/Molecules/Avatar'))
const Buttons = React.lazy(async () => await import('../pages/Molecules/Buttons'))
const Tooltip = React.lazy(async () => await import('../pages/Molecules/Tooltip'))
const UserWay = React.lazy(async () => await import('../pages/Molecules/UserWay'))
Expand All @@ -34,6 +35,7 @@ const Modals = React.lazy(async () => await import('../pages/Organisms/Modals'))
const Events = React.lazy(async () => await import('../pages/Organisms/Events'))
const EventsList = React.lazy(async () => await import('../pages/Organisms/EventsList'))
const Resources = React.lazy(async () => await import('../pages/Organisms/Resources'))
const UserDropdown = React.lazy(async () => await import('../pages/Organisms/UserDropdown'))

/**
* Rutas que tiene el proyecto con el respectivo link en la navegación.
Expand Down Expand Up @@ -125,6 +127,11 @@ export const routes: IRoute[] = [
label: 'Molecules',
},
/** ****************************** */
{
path: '/molecules/avatar',
label: 'Avatar',
component: <Avatar />,
},
{
path: '/molecules/buttons',
label: 'Buttons',
Expand Down Expand Up @@ -155,6 +162,11 @@ export const routes: IRoute[] = [
label: 'Calendar Dropdown',
component: <CalendarDropdown />,
},
{
path: '/organisms/userdropdown',
label: 'User Dropdown',
component: <UserDropdown />,
},
{
path: '/organisms/courselist',
label: 'Course List',
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export { ModalAlert } from './organisms/ModalAlert'
export { Eventos } from './organisms/Events'
export { Resources } from './organisms/Resources'
export { CalendarDropdown, EventsList } from './organisms/Calendar'
export { UserDropdownMenu } from './organisms/User'
export type { AccesibleProfileMenuProps, ProfileMenuItem } from './organisms/User'

// Tema
export { theme, vars } from './theme'
Loading
Loading