diff --git a/package-lock.json b/package-lock.json
index 39d711b..aaa5f16 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
"dependencies": {
"chart.js": "^4.5.0",
"clsx": "^2.1.1",
+ "date-fns": "^4.1.0",
"highlight.js": "^11.11.1",
"i18next": "^23.11.5",
"i18next-browser-languagedetector": "^7.2.1",
@@ -2695,6 +2696,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/date-fns": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
+ "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/kossnocorp"
+ }
+ },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
diff --git a/package.json b/package.json
index 4154a22..a98c3f9 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"dependencies": {
"chart.js": "^4.5.0",
"clsx": "^2.1.1",
+ "date-fns": "^4.1.0",
"highlight.js": "^11.11.1",
"i18next": "^23.11.5",
"i18next-browser-languagedetector": "^7.2.1",
diff --git a/src/app/components/cards/CalendarCard.jsx b/src/app/components/cards/CalendarCard.jsx
new file mode 100644
index 0000000..6fb9330
--- /dev/null
+++ b/src/app/components/cards/CalendarCard.jsx
@@ -0,0 +1,183 @@
+'use client';
+
+import { useState } from 'react';
+import PropTypes from 'prop-types';
+import { format, startOfMonth, endOfMonth, eachDayOfInterval, isToday, isSameMonth } from 'date-fns';
+
+const CalendarCard = ({ events = [], birthdays = [], meetings = [], className = '' }) => {
+ const [currentDate, setCurrentDate] = useState(new Date());
+ const [selectedDate, setSelectedDate] = useState(null);
+
+ // Generate days for the current month
+ const monthStart = startOfMonth(currentDate);
+ const monthEnd = endOfMonth(currentDate);
+ const days = eachDayOfInterval({ start: monthStart, end: monthEnd });
+
+ // Function to get all events for a specific date
+ const getDateEvents = (date) => {
+ const formattedDate = format(date, 'yyyy-MM-dd');
+ return {
+ events: events.filter(event => format(new Date(event.date), 'yyyy-MM-dd') === formattedDate),
+ birthdays: birthdays.filter(birthday => format(new Date(birthday.date), 'yyyy-MM-dd') === formattedDate),
+ meetings: meetings.filter(meeting => format(new Date(meeting.date), 'yyyy-MM-dd') === formattedDate)
+ };
+ };
+
+ // Function to check if a date has any events
+ const getDateHighlights = (date) => {
+ const { events: dateEvents, birthdays: dateBirthdays, meetings: dateMeetings } = getDateEvents(date);
+ return {
+ hasEvent: dateEvents.length > 0,
+ hasBirthday: dateBirthdays.length > 0,
+ hasMeeting: dateMeetings.length > 0
+ };
+ };
+
+ // Function to handle month navigation
+ const changeMonth = (increment) => {
+ setCurrentDate(prev => new Date(prev.getFullYear(), prev.getMonth() + increment, 1));
+ };
+
+ return (
+
+ {/* Calendar Header */}
+
+
+
+
+ {format(currentDate, 'MMMM yyyy')}
+
+
+
+
+
+ {/* Weekday Headers */}
+
+ {['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((day) => (
+
+ {day}
+
+ ))}
+
+
+ {/* Calendar Grid */}
+
+ {days.map((day) => {
+ const { hasEvent, hasBirthday, hasMeeting } = getDateHighlights(day);
+ const isCurrentMonth = isSameMonth(day, currentDate);
+
+ return (
+
setSelectedDate(day)}
+ className={`
+ group relative p-2 h-14 border border-gray-200 dark:border-gray-700 rounded
+ ${isCurrentMonth ? 'bg-white dark:bg-gray-800' : 'bg-gray-50 dark:bg-gray-900'}
+ ${isToday(day) ? 'ring-2 ring-blue-500' : ''}
+ ${selectedDate && format(day, 'yyyy-MM-dd') === format(selectedDate, 'yyyy-MM-dd')
+ ? 'bg-blue-50 dark:bg-blue-900' : ''}
+ cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors
+ `}
+ >
+
+ {format(day, 'd')}
+
+
+ {/* Highlight Indicators */}
+
+ {hasEvent && (
+
+ )}
+ {hasBirthday && (
+
+ )}
+ {hasMeeting && (
+
+ )}
+
+
+ {/* Tooltip */}
+ {(hasEvent || hasBirthday || hasMeeting) && (
+
+ {getDateEvents(day).events.map((event, idx) => (
+
+ ))}
+ {getDateEvents(day).birthdays.map((birthday, idx) => (
+
+
+
{birthday.name}'s Birthday
+
+ ))}
+ {getDateEvents(day).meetings.map((meeting, idx) => (
+
+ ))}
+
+
+ )}
+
+ );
+ })}
+
+
+ {/* Legend */}
+
+
+ );
+};
+
+CalendarCard.propTypes = {
+ events: PropTypes.arrayOf(PropTypes.shape({
+ date: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]).isRequired,
+ title: PropTypes.string.isRequired
+ })),
+ birthdays: PropTypes.arrayOf(PropTypes.shape({
+ date: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]).isRequired,
+ name: PropTypes.string.isRequired
+ })),
+ meetings: PropTypes.arrayOf(PropTypes.shape({
+ date: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]).isRequired,
+ title: PropTypes.string.isRequired
+ })),
+ className: PropTypes.string
+};
+
+export default CalendarCard;
\ No newline at end of file
diff --git a/src/app/components/cards/CalendarCard.test.jsx b/src/app/components/cards/CalendarCard.test.jsx
new file mode 100644
index 0000000..ca6e030
--- /dev/null
+++ b/src/app/components/cards/CalendarCard.test.jsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import CalendarCard from './CalendarCard';
+
+describe('CalendarCard', () => {
+ const mockEvents = [
+ { date: '2025-10-30', title: 'Test Event' }
+ ];
+ const mockBirthdays = [
+ { date: '2025-10-31', name: 'John Doe' }
+ ];
+ const mockMeetings = [
+ { date: '2025-10-29', title: 'Team Meeting' }
+ ];
+
+ it('renders calendar with current month and year', () => {
+ render();
+ expect(screen.getByText('October 2025')).toBeInTheDocument();
+ });
+
+ it('shows event indicators when events are provided', () => {
+ render(
+
+ );
+
+ // Check for legend items
+ expect(screen.getByText('Event')).toBeInTheDocument();
+ expect(screen.getByText('Birthday')).toBeInTheDocument();
+ expect(screen.getByText('Meeting')).toBeInTheDocument();
+ });
+
+ it('navigates between months when clicking navigation buttons', () => {
+ render();
+
+ // Initial month
+ expect(screen.getByText('October 2025')).toBeInTheDocument();
+
+ // Click next month button
+ fireEvent.click(screen.getByLabelText('Next month'));
+ expect(screen.getByText('November 2025')).toBeInTheDocument();
+
+ // Click previous month button
+ fireEvent.click(screen.getByLabelText('Previous month'));
+ expect(screen.getByText('October 2025')).toBeInTheDocument();
+ });
+
+ it('applies custom className when provided', () => {
+ const customClass = 'custom-calendar';
+ const { container } = render();
+ expect(container.firstChild).toHaveClass(customClass);
+ });
+
+ it('renders all days of the current month', () => {
+ render();
+ // Check if weekday headers are present
+ const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
+ weekdays.forEach(day => {
+ expect(screen.getByText(day)).toBeInTheDocument();
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/app/components/page.jsx b/src/app/components/page.jsx
index 2d55af9..4dc3ddf 100644
--- a/src/app/components/page.jsx
+++ b/src/app/components/page.jsx
@@ -28,6 +28,7 @@ import PricingCard from "./cards/PricingCard";
import DataCard from "./cards/DataCard";
import SmartCard from "./cards/SmartCard";
import UserCard from "@/app/components/cards/UserCard";
+import CalendarCard from "./cards/CalendarCard";
// Inputs
import TextInput from "./inputs/TextInput";
@@ -239,6 +240,30 @@ export default function Page() {
},
],
cards: [
+ {
+ name: "Calendar Card",
+ component: (
+
+ ),
+ keywords: ["calendar", "date", "events", "meetings", "birthdays"],
+ },
{
name: t('cards.simple.name'),
component: (