diff --git a/src/components/Form/Settings/DatePicker/DatePicker.jsx b/src/components/Form/Settings/DatePicker/DatePicker.jsx
new file mode 100644
index 00000000..5970a2cf
--- /dev/null
+++ b/src/components/Form/Settings/DatePicker/DatePicker.jsx
@@ -0,0 +1,196 @@
+import { memo, useState, useCallback, useRef, useEffect } from 'react';
+import { createPortal } from 'react-dom';
+import { MdExpandMore, MdChevronLeft, MdChevronRight } from 'react-icons/md';
+
+import './DatePicker.scss';
+
+const DatePicker = memo((props) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const [isClosing, setIsClosing] = useState(false);
+ const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0, width: 0 });
+ const [viewDate, setViewDate] = useState(props.value || new Date());
+ const containerRef = useRef(null);
+ const controlRef = useRef(null);
+ const menuRef = useRef(null);
+
+ const closeDropdown = useCallback(() => {
+ setIsClosing(true);
+ setTimeout(() => {
+ setIsOpen(false);
+ setIsClosing(false);
+ }, 200);
+ }, []);
+
+ useEffect(() => {
+ const handleClickOutside = (event) => {
+ if (
+ containerRef.current &&
+ !containerRef.current.contains(event.target) &&
+ menuRef.current &&
+ !menuRef.current.contains(event.target)
+ ) {
+ closeDropdown();
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, [closeDropdown]);
+
+ const calculatePosition = useCallback(() => {
+ if (controlRef.current) {
+ const rect = controlRef.current.getBoundingClientRect();
+ const gap = 4;
+ const viewportHeight = window.innerHeight;
+ const estimatedMenuHeight = 320;
+ const spaceBelow = viewportHeight - rect.bottom - gap;
+ const spaceAbove = rect.top - gap;
+ const shouldFlipUp = spaceBelow < estimatedMenuHeight && spaceAbove > spaceBelow;
+
+ return {
+ top: shouldFlipUp ? rect.top - gap : rect.bottom + gap,
+ left: rect.left,
+ width: rect.width,
+ maxHeight: shouldFlipUp ? Math.min(320, spaceAbove) : Math.min(320, spaceBelow),
+ flipped: shouldFlipUp,
+ };
+ }
+ return { top: 0, left: 0, width: 0, maxHeight: 320, flipped: false };
+ }, []);
+
+ const openDropdown = useCallback(() => {
+ const position = calculatePosition();
+ setMenuPosition(position);
+ setIsOpen(true);
+ }, [calculatePosition]);
+
+ const formatDate = (date) => {
+ if (!date) return 'Select Date';
+ const month = String(date.getMonth() + 1).padStart(2, '0');
+ const day = String(date.getDate()).padStart(2, '0');
+ const year = date.getFullYear();
+ return props.hideYear ? `${month}/${day}` : `${month}/${day}/${year}`;
+ };
+
+ const getDaysInMonth = (date) => {
+ return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
+ };
+
+ const getFirstDayOfMonth = (date) => {
+ return new Date(date.getFullYear(), date.getMonth(), 1).getDay();
+ };
+
+ const handleDateSelect = (day) => {
+ const newDate = new Date(viewDate.getFullYear(), viewDate.getMonth(), day);
+ if (props.onChange) {
+ props.onChange(newDate);
+ }
+ closeDropdown();
+ };
+
+ const handlePreviousMonth = () => {
+ setViewDate(new Date(viewDate.getFullYear(), viewDate.getMonth() - 1, 1));
+ };
+
+ const handleNextMonth = () => {
+ setViewDate(new Date(viewDate.getFullYear(), viewDate.getMonth() + 1, 1));
+ };
+
+ const monthNames = [
+ 'January', 'February', 'March', 'April', 'May', 'June',
+ 'July', 'August', 'September', 'October', 'November', 'December'
+ ];
+
+ const renderCalendar = () => {
+ const daysInMonth = getDaysInMonth(viewDate);
+ const firstDay = getFirstDayOfMonth(viewDate);
+ const days = [];
+ const today = new Date();
+ const selectedDate = props.value;
+
+ // Empty cells for days before the first day of the month
+ for (let i = 0; i < firstDay; i++) {
+ days.push(
);
+ }
+
+ // Days of the month
+ for (let day = 1; day <= daysInMonth; day++) {
+ const date = new Date(viewDate.getFullYear(), viewDate.getMonth(), day);
+ const isToday = date.toDateString() === today.toDateString();
+ const isSelected = selectedDate && date.toDateString() === selectedDate.toDateString();
+
+ days.push(
+ handleDateSelect(day)}
+ >
+ {day}
+
+ );
+ }
+
+ return days;
+ };
+
+ return (
+ e.stopPropagation()}>
+
{
+ if (isOpen) {
+ closeDropdown();
+ } else {
+ openDropdown();
+ }
+ }}
+ >
+ {formatDate(props.value)}
+
+
+ {(isOpen || isClosing) &&
+ createPortal(
+
+
+
+
+ {monthNames[viewDate.getMonth()]}{props.hideYear ? '' : ` ${viewDate.getFullYear()}`}
+
+
+
+
+
Su
+
Mo
+
Tu
+
We
+
Th
+
Fr
+
Sa
+
+
+ {renderCalendar()}
+
+
,
+ document.body,
+ )}
+
+ );
+});
+
+DatePicker.displayName = 'DatePicker';
+
+export { DatePicker as default, DatePicker };
diff --git a/src/components/Form/Settings/DatePicker/DatePicker.scss b/src/components/Form/Settings/DatePicker/DatePicker.scss
new file mode 100644
index 00000000..6429e40f
--- /dev/null
+++ b/src/components/Form/Settings/DatePicker/DatePicker.scss
@@ -0,0 +1,256 @@
+@use 'scss/variables' as *;
+@use 'scss/mixins' as *;
+
+@include keyframes(datepickerSlideIn) {
+ 0% {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+
+ 100% {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@include keyframes(datepickerSlideOut) {
+ 0% {
+ opacity: 1;
+ transform: translateY(0);
+ }
+
+ 100% {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+}
+
+@include keyframes(datepickerSlideInUp) {
+ 0% {
+ opacity: 0;
+ transform: translateY(-100%) translateY(10px);
+ }
+
+ 100% {
+ opacity: 1;
+ transform: translateY(-100%);
+ }
+}
+
+@include keyframes(datepickerSlideOutUp) {
+ 0% {
+ opacity: 1;
+ transform: translateY(-100%);
+ }
+
+ 100% {
+ opacity: 0;
+ transform: translateY(-100%) translateY(10px);
+ }
+}
+
+.datepicker {
+ position: relative;
+ width: 100%;
+ min-width: 200px;
+ max-width: 300px;
+
+ .datepicker-control {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ height: 56px;
+ padding: 0 16px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ outline: none;
+
+ @include themed {
+ background: t($modal-sidebar);
+ border: 1px solid t($modal-sidebarActive);
+ border-radius: t($borderRadius);
+ color: t($color);
+
+ &:hover {
+ border-color: t($color);
+ }
+ }
+
+ &:focus-visible {
+ outline: none;
+
+ @include themed {
+ border-color: t($link);
+ box-shadow: 0 0 0 3px t($link);
+ }
+ }
+ }
+
+ .datepicker-value {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ transition: color 0.2s ease;
+ }
+
+ .datepicker-arrow {
+ flex-shrink: 0;
+ font-size: 24px;
+ transition: all 0.2s ease;
+ cursor: pointer;
+ padding: 4px;
+ border-radius: 50%;
+ margin: -4px;
+
+ @include themed {
+ color: t($subColor);
+
+ &:hover {
+ background: t($modal-sidebarActive);
+ color: t($color);
+ }
+ }
+
+ &.open {
+ transform: rotate(180deg);
+ }
+ }
+}
+
+.datepicker-menu {
+ z-index: 9999;
+ @include animation(datepickerSlideIn 0.2s ease-out);
+ will-change: transform, opacity;
+ padding: 16px;
+ min-width: 280px;
+ box-sizing: border-box;
+
+ @include themed {
+ background: t($modal-background);
+ border: 1px solid t($modal-sidebarActive);
+ border-radius: t($borderRadius);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ }
+
+ &.flipped {
+ @include animation(datepickerSlideInUp 0.2s ease-out);
+
+ &.closing {
+ @include animation(datepickerSlideOutUp 0.2s ease-out forwards);
+ }
+ }
+
+ &.closing:not(.flipped) {
+ @include animation(datepickerSlideOut 0.2s ease-out forwards);
+ }
+}
+
+.calendar-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 12px;
+ gap: 8px;
+
+ .calendar-nav {
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ padding: 4px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s ease;
+
+ @include themed {
+ color: t($subColor);
+
+ &:hover {
+ background: t($modal-sidebarActive);
+ color: t($color);
+ }
+ }
+
+ svg {
+ font-size: 20px;
+ }
+ }
+
+ .calendar-month {
+ font-weight: 600;
+ font-size: 14px;
+ flex: 1;
+ text-align: center;
+
+ @include themed {
+ color: t($color);
+ }
+ }
+}
+
+.calendar-weekdays {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ gap: 4px;
+ margin-bottom: 8px;
+
+ div {
+ text-align: center;
+ font-size: 12px;
+ font-weight: 500;
+ padding: 8px 4px;
+
+ @include themed {
+ color: t($subColor);
+ }
+ }
+}
+
+.calendar-grid {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ gap: 4px;
+
+ .calendar-day {
+ aspect-ratio: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ border-radius: 50%;
+ font-size: 14px;
+ transition: all 0.15s ease;
+ min-width: 32px;
+ min-height: 32px;
+
+ @include themed {
+ color: t($color);
+
+ &:hover:not(.empty) {
+ background: t($modal-sidebarActive);
+ }
+
+ &.today {
+ border: 2px solid t($link);
+ }
+
+ &.selected {
+ background: t($link);
+ color: white;
+ font-weight: 600;
+
+ &:hover {
+ background: t($link);
+ opacity: 0.9;
+ }
+ }
+
+ &.empty {
+ cursor: default;
+ }
+ }
+ }
+}
diff --git a/src/components/Form/Settings/DatePicker/index.jsx b/src/components/Form/Settings/DatePicker/index.jsx
new file mode 100644
index 00000000..a48b62e4
--- /dev/null
+++ b/src/components/Form/Settings/DatePicker/index.jsx
@@ -0,0 +1 @@
+export * from './DatePicker';
diff --git a/src/components/Form/Settings/Text/Text.scss b/src/components/Form/Settings/Text/Text.scss
index eb44c009..8e20c128 100644
--- a/src/components/Form/Settings/Text/Text.scss
+++ b/src/components/Form/Settings/Text/Text.scss
@@ -75,6 +75,10 @@
color: t($subColor);
}
}
+
+ &.event-name-input {
+ max-width: 300px;
+ }
}
.text-field-textarea {
diff --git a/src/features/greeting/options/GreetingOptions.jsx b/src/features/greeting/options/GreetingOptions.jsx
index 15606e9a..6776b3bf 100644
--- a/src/features/greeting/options/GreetingOptions.jsx
+++ b/src/features/greeting/options/GreetingOptions.jsx
@@ -10,6 +10,7 @@ import {
Section,
} from 'components/Layout/Settings';
import { Checkbox, Switch, Text } from 'components/Form/Settings';
+import { DatePicker } from 'components/Form/Settings/DatePicker';
import { Button } from 'components/Elements';
import { toast } from 'react-toastify';
@@ -146,11 +147,12 @@ const GreetingOptions = ({ currentSubSection, onSubSectionChange, sectionName })
{variables.getMessage(`${GREETING_SECTION}.birthday_date`)}
- {
+ localStorage.setItem('birthday', newDate);
+ setBirthday(newDate);
+ }}
/>
@@ -193,7 +195,7 @@ const GreetingOptions = ({ currentSubSection, onSubSectionChange, sectionName })
{
@@ -205,43 +207,18 @@ const GreetingOptions = ({ currentSubSection, onSubSectionChange, sectionName })
-
-
- {
- const value = parseInt(e.target.value, 10);
- if (value >= 1 && value <= 31) {
- const updatedEvent = { ...event, date: value };
- updateEvent(index, updatedEvent);
- }
- }}
- />
-
-
- {
- const value = parseInt(e.target.value, 10);
- if (value >= 1 && value <= 12) {
- const updatedEvent = { ...event, month: value };
- updateEvent(index, updatedEvent);
- }
- }}
- />
-
+
{
+ const updatedEvent = {
+ ...event,
+ month: newDate.getMonth() + 1,
+ date: newDate.getDate(),
+ };
+ updateEvent(index, updatedEvent);
+ }}
+ />