Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DEFAULT_DATE_FORMAT = 'dd MMM, yyyy';
42 changes: 42 additions & 0 deletions src/components/UpcomingQuizCard/UpcomingQuizCard.docs.mdx
Original file line number Diff line number Diff line change
@@ -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 (
<UpcomingQuizCard
title="Upcoming Quiz Competition"
date="2023-08-12"
description="Join our next exciting quiz and test your knowledge!"
onRegister={() => alert('Registered!')}
/>
);
}
29 changes: 29 additions & 0 deletions src/components/UpcomingQuizCard/UpcomingQuizCard.hook.ts
Original file line number Diff line number Diff line change
@@ -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);
};
25 changes: 25 additions & 0 deletions src/components/UpcomingQuizCard/UpcomingQuizCard.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import UpcomingQuizCard from './UpcomingQuizCard';

import type { Meta, StoryObj } from '@storybook/react';

const meta: Meta<typeof UpcomingQuizCard> = {
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<typeof UpcomingQuizCard>;

export const Default: Story = {};

export const WithoutDescription: Story = {
args: {
description: undefined,
},
};
28 changes: 28 additions & 0 deletions src/components/UpcomingQuizCard/UpcomingQuizCard.styles.ts
Original file line number Diff line number Diff line change
@@ -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,
},
}));
19 changes: 19 additions & 0 deletions src/components/UpcomingQuizCard/UpcomingQuizCard.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<UpcomingQuizCard title="Quiz" date="2023-08-12" onRegister={() => {}} />);
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(<UpcomingQuizCard title="Quiz" date="2023-08-12" onRegister={handleRegister} />);
fireEvent.click(screen.getByTestId('register-button'));
expect(handleRegister).toHaveBeenCalledTimes(1);
});
});
59 changes: 59 additions & 0 deletions src/components/UpcomingQuizCard/UpcomingQuizCard.tsx
Original file line number Diff line number Diff line change
@@ -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<UpcomingQuizCardProps> = ({
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 (
<StyledCard {...cardProps} data-testid="quiz-card">
<CardContent>
<Typography variant="h6" gutterBottom>
{title}
</Typography>

<DateContainer>
<CalendarIcon fontSize="large" color="#1976d2" day={day} />

<Typography variant="subtitle1" color="textSecondary" data-testid="quiz-date">
{formattedDate}
</Typography>
</DateContainer>

{description && (
<Typography variant="body2" color="textSecondary" gutterBottom>
{description}
</Typography>
)}

<RegisterButton variant="contained" onClick={onRegister} data-testid="register-button">
Register Now
</RegisterButton>
</CardContent>
</StyledCard>
);
};

export default UpcomingQuizCard;
8 changes: 8 additions & 0 deletions src/components/UpcomingQuizCard/UpcomingQuizCard.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { CardProps } from '@mui/material';

export interface UpcomingQuizCardProps extends CardProps {
title: string;
date: string | Date;
description?: string;
onRegister: () => void;
}
10 changes: 10 additions & 0 deletions src/components/UpcomingQuizCard/UpcomingQuizCard.utils.ts
Original file line number Diff line number Diff line change
@@ -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);
};
57 changes: 57 additions & 0 deletions src/components/UpcomingQuizCard/icon/CalendarIcon.tsx
Original file line number Diff line number Diff line change
@@ -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<CalendarIconProps> = ({
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 (
<svg width={size} height={size} viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
{/* Fondo del calendario */}
<rect
x="4"
y="8"
width="56"
height="52"
rx="8"
ry="8"
fill="#f0f0f0"
stroke="#ccc"
strokeWidth="2"
/>
{/* Anillos superiores */}
<circle cx="20" cy="8" r="3" fill={color} />
<circle cx="44" cy="8" r="3" fill={color} />
{/* Línea divisoria */}
<line x1="4" y1="20" x2="60" y2="20" stroke="#ccc" strokeWidth="2" />
{/* Número del día */}
<text
x="32"
y="46"
textAnchor="middle"
fontSize="24"
fontWeight="bold"
fill={color}
fontFamily="Arial, sans-serif"
>
{day}
</text>
</svg>
);
};

export default CalendarIcon;