From 849a00d41ab91a8d70e3fc70e6e91a6c97f1d2f0 Mon Sep 17 00:00:00 2001 From: kellyalejandra861 Date: Thu, 9 Oct 2025 00:22:35 -0400 Subject: [PATCH] feature(common): Build UpcomingQuizCard component --- .../UpcomingQuizCard.constants.ts | 1 + .../UpcomingQuizCard.docs.mdx | 42 +++++++++++++ .../UpcomingQuizCard/UpcomingQuizCard.hook.ts | 29 +++++++++ .../UpcomingQuizCard.stories.tsx | 25 ++++++++ .../UpcomingQuizCard.styles.ts | 28 +++++++++ .../UpcomingQuizCard.test.tsx | 19 ++++++ .../UpcomingQuizCard/UpcomingQuizCard.tsx | 59 +++++++++++++++++++ .../UpcomingQuizCard.types.ts | 8 +++ .../UpcomingQuizCard.utils.ts | 10 ++++ .../UpcomingQuizCard/icon/CalendarIcon.tsx | 57 ++++++++++++++++++ 10 files changed, 278 insertions(+) create mode 100644 src/components/UpcomingQuizCard/UpcomingQuizCard.constants.ts create mode 100644 src/components/UpcomingQuizCard/UpcomingQuizCard.docs.mdx create mode 100644 src/components/UpcomingQuizCard/UpcomingQuizCard.hook.ts create mode 100644 src/components/UpcomingQuizCard/UpcomingQuizCard.stories.tsx create mode 100644 src/components/UpcomingQuizCard/UpcomingQuizCard.styles.ts create mode 100644 src/components/UpcomingQuizCard/UpcomingQuizCard.test.tsx create mode 100644 src/components/UpcomingQuizCard/UpcomingQuizCard.tsx create mode 100644 src/components/UpcomingQuizCard/UpcomingQuizCard.types.ts create mode 100644 src/components/UpcomingQuizCard/UpcomingQuizCard.utils.ts create mode 100644 src/components/UpcomingQuizCard/icon/CalendarIcon.tsx diff --git a/src/components/UpcomingQuizCard/UpcomingQuizCard.constants.ts b/src/components/UpcomingQuizCard/UpcomingQuizCard.constants.ts new file mode 100644 index 0000000..e841d84 --- /dev/null +++ b/src/components/UpcomingQuizCard/UpcomingQuizCard.constants.ts @@ -0,0 +1 @@ +export const DEFAULT_DATE_FORMAT = 'dd MMM, yyyy'; diff --git a/src/components/UpcomingQuizCard/UpcomingQuizCard.docs.mdx b/src/components/UpcomingQuizCard/UpcomingQuizCard.docs.mdx new file mode 100644 index 0000000..a7fff1e --- /dev/null +++ b/src/components/UpcomingQuizCard/UpcomingQuizCard.docs.mdx @@ -0,0 +1,42 @@ +# 🧩 UpcomingQuizCard + +The **UpcomingQuizCard** component displays concise information about an upcoming quiz or competition. +It highlights the event date, provides a short description, and includes a clear call-to-action for registration. + +--- + +## ✨ Features + +- 📅 Displays a formatted event date with an icon for quick recognition. +- 📝 Optionally includes a brief description of the quiz or competition. +- 🚀 Includes a **“Register Now”** button that triggers a callback when clicked. +- 🎨 Fully customizable through MUI’s theme and `sx` prop. + +--- + +## ⚙️ Props + +| Prop | Type | Required | Description | +|------|------|-----------|-------------| +| **`title`** | `string` | ✅ Yes | Title of the quiz or competition. | +| **`date`** | `string` \| `Date` | ✅ Yes | Date of the upcoming event. Formatted using the default pattern defined in `UpcomingQuizCard.constants.ts`. | +| **`description`** | `string` | ❌ No | Additional text describing the event (optional). | +| **`onRegister`** | `() => void` | ✅ Yes | Callback executed when the user clicks the **Register Now** button. | + +--- + +## 🧱 Usage Example + +```tsx +import UpcomingQuizCard from '@/components/UpcomingQuizCard'; + +function Example() { + return ( + alert('Registered!')} + /> + ); +} diff --git a/src/components/UpcomingQuizCard/UpcomingQuizCard.hook.ts b/src/components/UpcomingQuizCard/UpcomingQuizCard.hook.ts new file mode 100644 index 0000000..8f44ec7 --- /dev/null +++ b/src/components/UpcomingQuizCard/UpcomingQuizCard.hook.ts @@ -0,0 +1,29 @@ +import { useMemo } from 'react'; + +import { formatDate } from './UpcomingQuizCard.utils'; + +export const useUpcomingQuizCard = (date: string | Date) => { + const formattedDate = useMemo(() => { + const normalizedDate = normalizeDate(date); + const nextDay = new Date(normalizedDate); + nextDay.setDate(nextDay.getDate() + 1); + return formatDate(nextDay); + }, [date]); + + return { formattedDate }; +}; + +const normalizeDate = (date: string | Date): Date => { + if (typeof date === 'string') { + const dateParts = date.split('-'); + if (dateParts.length === 3) { + const [year, month, day] = dateParts; + const normalizedYear = year.padStart(4, '0'); + const normalizedMonth = month.padStart(2, '0'); + const normalizedDay = day.padStart(2, '0'); + + return new Date(`${normalizedYear}-${normalizedMonth}-${normalizedDay}T00:00:00.000Z`); + } + } + return new Date(date); +}; diff --git a/src/components/UpcomingQuizCard/UpcomingQuizCard.stories.tsx b/src/components/UpcomingQuizCard/UpcomingQuizCard.stories.tsx new file mode 100644 index 0000000..6ab09bc --- /dev/null +++ b/src/components/UpcomingQuizCard/UpcomingQuizCard.stories.tsx @@ -0,0 +1,25 @@ +import UpcomingQuizCard from './UpcomingQuizCard'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + title: 'Components/UpcomingQuizCard', + component: UpcomingQuizCard, + args: { + title: 'Upcoming Quiz Competition', + date: '2025-08-12', + description: 'Join our next exciting quiz and test your knowledge!', + onRegister: () => alert('Registered!'), + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const WithoutDescription: Story = { + args: { + description: undefined, + }, +}; diff --git a/src/components/UpcomingQuizCard/UpcomingQuizCard.styles.ts b/src/components/UpcomingQuizCard/UpcomingQuizCard.styles.ts new file mode 100644 index 0000000..9640481 --- /dev/null +++ b/src/components/UpcomingQuizCard/UpcomingQuizCard.styles.ts @@ -0,0 +1,28 @@ +import { Card, Button, Typography, Box } from '@mui/material'; +import { styled } from '@mui/material/styles'; + +export const StyledCard = styled(Card)(({ theme }) => ({ + textAlign: 'center', + padding: theme.spacing(3), + borderRadius: theme.spacing(3), + boxShadow: theme.shadows[3], + backgroundColor: theme.palette.background.paper, +})); + +export const DateContainer = styled(Box)(({ theme }) => ({ + margin: theme.spacing(2, 0), + display: 'flex', + flexDirection: 'column', + alignItems: 'center', +})); + +export const RegisterButton = styled(Button)(({ theme }) => ({ + marginTop: theme.spacing(2), + background: 'linear-gradient(90deg, #A44CE0, #6C63FF)', + color: '#fff', + padding: theme.spacing(1.2, 4), + borderRadius: (theme.shape.borderRadius as number) * 2, + '&:hover': { + opacity: 0.9, + }, +})); diff --git a/src/components/UpcomingQuizCard/UpcomingQuizCard.test.tsx b/src/components/UpcomingQuizCard/UpcomingQuizCard.test.tsx new file mode 100644 index 0000000..a0dac40 --- /dev/null +++ b/src/components/UpcomingQuizCard/UpcomingQuizCard.test.tsx @@ -0,0 +1,19 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; + +import UpcomingQuizCard from './UpcomingQuizCard'; + +describe('UpcomingQuizCard', () => { + it('renders title and formatted date', () => { + render( {}} />); + expect(screen.getByText('Quiz')).toBeInTheDocument(); + expect(screen.getByTestId('quiz-date')).toHaveTextContent('12 Aug, 2023'); + }); + + it('calls onRegister when button is clicked', () => { + const handleRegister = vi.fn(); + render(); + fireEvent.click(screen.getByTestId('register-button')); + expect(handleRegister).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/components/UpcomingQuizCard/UpcomingQuizCard.tsx b/src/components/UpcomingQuizCard/UpcomingQuizCard.tsx new file mode 100644 index 0000000..a07caea --- /dev/null +++ b/src/components/UpcomingQuizCard/UpcomingQuizCard.tsx @@ -0,0 +1,59 @@ +import { Typography, CardContent } from '@mui/material'; +import React from 'react'; + +import CalendarIcon from './icon/CalendarIcon'; +import { useUpcomingQuizCard } from './UpcomingQuizCard.hook'; +import { StyledCard, DateContainer, RegisterButton } from './UpcomingQuizCard.styles'; + +import type { UpcomingQuizCardProps } from './UpcomingQuizCard.types'; + +const UpcomingQuizCard: React.FC = ({ + title, + date, + description, + onRegister, + ...cardProps +}) => { + const { formattedDate } = useUpcomingQuizCard(date); + + const getLocalDay = (dateValue: string | Date) => { + if (typeof dateValue === 'string') { + // Usar UTC para consistencia con el hook + const [year, month, day] = dateValue.split('-').map(Number); + return new Date(Date.UTC(year, month - 1, day)).getUTCDate(); + } + return dateValue.getUTCDate(); + }; + + const day = getLocalDay(date); + + return ( + + + + {title} + + + + + + + {formattedDate} + + + + {description && ( + + {description} + + )} + + + Register Now + + + + ); +}; + +export default UpcomingQuizCard; diff --git a/src/components/UpcomingQuizCard/UpcomingQuizCard.types.ts b/src/components/UpcomingQuizCard/UpcomingQuizCard.types.ts new file mode 100644 index 0000000..9e3e029 --- /dev/null +++ b/src/components/UpcomingQuizCard/UpcomingQuizCard.types.ts @@ -0,0 +1,8 @@ +import type { CardProps } from '@mui/material'; + +export interface UpcomingQuizCardProps extends CardProps { + title: string; + date: string | Date; + description?: string; + onRegister: () => void; +} diff --git a/src/components/UpcomingQuizCard/UpcomingQuizCard.utils.ts b/src/components/UpcomingQuizCard/UpcomingQuizCard.utils.ts new file mode 100644 index 0000000..ccf283c --- /dev/null +++ b/src/components/UpcomingQuizCard/UpcomingQuizCard.utils.ts @@ -0,0 +1,10 @@ +import { format } from 'date-fns'; + +import { DEFAULT_DATE_FORMAT } from './UpcomingQuizCard.constants'; + +export const formatDate = ( + date: string | Date, + formatStr: string = DEFAULT_DATE_FORMAT +): string => { + return format(new Date(date), formatStr); +}; diff --git a/src/components/UpcomingQuizCard/icon/CalendarIcon.tsx b/src/components/UpcomingQuizCard/icon/CalendarIcon.tsx new file mode 100644 index 0000000..4e1cbd9 --- /dev/null +++ b/src/components/UpcomingQuizCard/icon/CalendarIcon.tsx @@ -0,0 +1,57 @@ +import React from 'react'; + +interface CalendarIconProps { + day?: number | string; // Día a mostrar (opcional) + fontSize?: 'small' | 'medium' | 'large'; + color?: string; +} + +const CalendarIcon: React.FC = ({ + day = new Date().getDate(), + fontSize = 'medium', + color = '#1976d2', // azul primario por defecto +}) => { + const sizeMap = { + small: 32, + medium: 48, + large: 64, + }; + + const size = sizeMap[fontSize] || 48; + + return ( + + {/* Fondo del calendario */} + + {/* Anillos superiores */} + + + {/* Línea divisoria */} + + {/* Número del día */} + + {day} + + + ); +}; + +export default CalendarIcon;