Skip to content

Commit 50e0538

Browse files
committed
feat(calendar): add calendar components including grid, content, navigation, and day slider
1 parent dc4adb5 commit 50e0538

11 files changed

+809
-430
lines changed

src/layouts/calendar/calendar.tsx

+58-283
Original file line numberDiff line numberDiff line change
@@ -1,315 +1,90 @@
1-
import { motion } from 'framer-motion'
2-
import { AnimatePresence } from 'framer-motion'
1+
import { AnimatePresence, motion } from 'framer-motion'
32
import jalaliMoment from 'jalali-moment'
43
import type React from 'react'
5-
import { useCallback, useMemo, useState } from 'react'
6-
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa6'
7-
import { FiCalendar } from 'react-icons/fi'
8-
import { v4 as uuidv4 } from 'uuid'
4+
import { useCallback, useState } from 'react'
95
import { useTheme } from '../../context/theme.context'
10-
import { TodoProvider, useTodoStore } from '../../context/todo.context'
11-
import { useGetEvents } from '../../services/getMethodHooks/getEvents.hook'
12-
import { DayItem } from './components/day'
13-
import { Events } from './components/events/event'
14-
import { TodoStats } from './components/todos/todo-stats'
15-
import { Todos } from './components/todos/todos'
16-
import { formatDateStr } from './utils'
17-
18-
const PERSIAN_MONTHS = [
19-
'فروردین',
20-
'اردیبهشت',
21-
'خرداد',
22-
'تیر',
23-
'مرداد',
24-
'شهریور',
25-
'مهر',
26-
'آبان',
27-
'آذر',
28-
'دی',
29-
'بهمن',
30-
'اسفند',
31-
]
32-
33-
const WEEKDAYS = ['ش', 'ی', 'د', 'س', 'چ', 'پ', 'ج']
34-
35-
export const PersianCalendar: React.FC = () => {
36-
const { theme, themeUtils } = useTheme()
6+
import { TodoProvider } from '../../context/todo.context'
7+
import { CalendarContainer } from './components/calendar-container'
8+
import { CalendarContent } from './components/calendar-content'
9+
import { CalendarGrid } from './components/calendar-grid'
10+
import { CalendarHeader } from './components/calendar-header'
11+
import { DaySummary } from './components/day-summary'
12+
import { TabNavigation } from './components/tab-navigation'
13+
14+
export type TabType = 'events' | 'todos' | 'todo-stats'
15+
16+
const PersianCalendar: React.FC = () => {
17+
const { themeUtils } = useTheme()
3718
const today = jalaliMoment().locale('fa').utc().add(3.5, 'hours')
3819
const [currentDate, setCurrentDate] = useState(today)
3920
const [selectedDate, setSelectedDate] = useState(today.clone())
21+
const [activeTab, setActiveTab] = useState<TabType>('events')
4022

41-
const isCurrentMonthToday = useMemo(() => {
42-
const realToday = jalaliMoment().locale('fa').utc().add(3.5, 'hours')
43-
return (
44-
currentDate.jMonth() === realToday.jMonth() &&
45-
currentDate.jYear() === realToday.jYear()
46-
)
47-
}, [currentDate])
48-
49-
const isTodaySelected = useMemo(() => {
50-
const realToday = jalaliMoment().locale('fa').utc().add(3.5, 'hours')
51-
return (
52-
selectedDate.jDate() === realToday.jDate() &&
53-
selectedDate.jMonth() === realToday.jMonth() &&
54-
selectedDate.jYear() === realToday.jYear()
55-
)
56-
}, [selectedDate])
57-
58-
const showTodayButton = !isCurrentMonthToday || !isTodaySelected
23+
const handleTabClick = useCallback((tab: TabType) => {
24+
setActiveTab(tab)
25+
}, [])
5926

6027
const goToToday = useCallback(() => {
6128
const realToday = jalaliMoment().locale('fa').utc().add(3.5, 'hours')
6229
setCurrentDate(realToday.clone())
6330
setSelectedDate(realToday.clone())
6431
}, [])
6532

66-
const firstDayOfMonth = currentDate.clone().startOf('jMonth').day()
67-
const { data: events } = useGetEvents()
68-
const daysInMonth = currentDate.clone().endOf('jMonth').jDate()
69-
const emptyDays = (firstDayOfMonth + 1) % 7
70-
const selectedDateStr = formatDateStr(selectedDate)
71-
const { todos } = useTodoStore()
72-
73-
const changeMonth = (delta: number) => {
74-
setCurrentDate((prev) => prev.clone().add(delta, 'jMonth'))
75-
}
76-
77-
// Theme-specific styles
78-
const getHeaderTextStyle = () => {
79-
switch (theme) {
80-
case 'light':
81-
return 'text-gray-700'
82-
default:
83-
return 'text-gray-200'
84-
}
85-
}
86-
87-
const getMonthNavigationStyle = () => {
88-
switch (theme) {
89-
case 'light':
90-
return 'text-gray-600 hover:bg-gray-100/80'
91-
case 'dark':
92-
return 'text-gray-300 hover:bg-neutral-800/50'
93-
default: // glass
94-
return 'text-gray-300 hover:bg-white/10'
95-
}
96-
}
97-
98-
const getWeekdayHeaderStyle = () => {
99-
switch (theme) {
100-
case 'light':
101-
return 'text-gray-500'
102-
default:
103-
return 'text-gray-400'
104-
}
105-
}
106-
107-
const getEventsSectionStyle = () => {
108-
switch (theme) {
109-
case 'light':
110-
return 'border-gray-200/50'
111-
case 'dark':
112-
return 'border-neutral-800/50'
113-
default: // glass
114-
return 'border-white/10'
115-
}
116-
}
117-
11833
return (
11934
<div className="flex flex-col justify-center w-full gap-3 mb-1 md:flex-row" dir="rtl">
120-
{/* Calendar Grid */}
121-
<div
122-
className={`w-full overflow-hidden md:flex-1 rounded-xl ${themeUtils.getCardBackground()}`}
123-
>
124-
<div className="flex items-center justify-between p-3 md:p-4">
125-
<h3 className={`font-medium text-md ${getHeaderTextStyle()}`}>
126-
{PERSIAN_MONTHS[currentDate.jMonth()]} {currentDate.jYear()}
127-
</h3>
128-
<div className="flex gap-1">
129-
{showTodayButton && (
130-
<motion.button
131-
initial={{ opacity: 0, scale: 0.9 }}
132-
animate={{ opacity: 1, scale: 1 }}
133-
onClick={goToToday}
134-
className="flex items-center gap-1 px-2 py-1 text-sm font-medium text-blue-400 transition-colors rounded-lg cursor-pointer bg-blue-500/10 hover:bg-blue-500/20"
135-
>
136-
<FiCalendar size={14} />
137-
<span>امروز</span>
138-
</motion.button>
139-
)}
140-
<button
141-
onClick={() => changeMonth(-1)}
142-
className={`flex items-center gap-1 px-2 py-1 text-sm rounded-lg cursor-pointer transition-colors ${getMonthNavigationStyle()}`}
143-
>
144-
<FaChevronRight size={12} />
145-
<span>ماه قبل</span>
146-
</button>
147-
<button
148-
onClick={() => changeMonth(1)}
149-
className={`flex items-center gap-1 px-2 py-1 text-sm rounded-lg cursor-pointer transition-colors ${getMonthNavigationStyle()}`}
150-
>
151-
<span>ماه بعد</span>
152-
<FaChevronLeft size={12} />
153-
</button>
35+
<CalendarContainer className="w-full overflow-hidden md:flex-1">
36+
<CalendarHeader
37+
currentDate={currentDate}
38+
setCurrentDate={setCurrentDate}
39+
selectedDate={selectedDate}
40+
goToToday={goToToday}
41+
/>
42+
43+
<CalendarGrid
44+
currentDate={currentDate}
45+
selectedDate={selectedDate}
46+
setSelectedDate={setSelectedDate}
47+
/>
48+
49+
<div className={`px-3 md:px-4 mt-2 border-t ${themeUtils.getBorderColor()}`}>
50+
<div className="mt-2">
51+
<TabNavigation activeTab={activeTab} onTabClick={handleTabClick} />
15452
</div>
155-
</div>
156-
157-
<div className="grid grid-cols-7 px-3 text-center md:px-4">
158-
{WEEKDAYS.map((day) => (
159-
<div key={day} className={`py-2 text-sm ${getWeekdayHeaderStyle()}`}>
160-
{day}
161-
</div>
162-
))}
163-
164-
{Array.from({ length: emptyDays }).map((_, i) => (
165-
<div key={`empty-${i}`} className="p-2" />
166-
))}
16753

168-
{Array.from({ length: daysInMonth }, (_, i) => (
169-
<DayItem
170-
currentDate={currentDate}
171-
day={i + 1}
172-
events={events}
173-
selectedDateStr={selectedDateStr}
174-
setSelectedDate={setSelectedDate}
175-
todos={todos}
176-
key={uuidv4()}
177-
/>
178-
))}
54+
<DaySummary selectedDate={selectedDate} onTabClick={handleTabClick} />
17955
</div>
56+
</CalendarContainer>
18057

181-
<div className={`px-3 md:px-4 ${getEventsSectionStyle()}`}>
182-
<Events events={events} currentDate={selectedDate} />
183-
</div>
184-
</div>
185-
186-
{/* Tasks */}
187-
<div
188-
className={`w-full md:w-80 p-3 md:p-4 h-auto min-h-[16rem] md:h-[27rem] backdrop-blur-sm rounded-xl ${themeUtils.getCardBackground()}`}
189-
>
190-
<h3
191-
className={`mb-3 text-lg font-medium md:mb-4 md:text-xl ${getHeaderTextStyle()}`}
192-
>
193-
{PERSIAN_MONTHS[selectedDate.jMonth()]} {selectedDate.jDate()}
194-
</h3>
195-
196-
<Todos currentDate={selectedDate} />
197-
</div>
58+
<CalendarContainer className="w-full md:w-80 p-3 md:p-4 h-auto min-h-[16rem] md:h-[27rem]">
59+
<AnimatePresence mode="wait">
60+
<CalendarContent
61+
activeTab={activeTab}
62+
selectedDate={selectedDate}
63+
setSelectedDate={setSelectedDate}
64+
currentDate={currentDate}
65+
setCurrentDate={setCurrentDate}
66+
/>
67+
</AnimatePresence>
68+
</CalendarContainer>
19869
</div>
19970
)
20071
}
20172

20273
const CalendarLayout = () => {
203-
const { theme } = useTheme()
204-
const [activeTab, setActiveTab] = useState<'calendar' | 'stats'>('calendar')
205-
206-
// Theme-specific styles for layout
207-
const getStatsContainerStyle = () => {
208-
switch (theme) {
209-
case 'light':
210-
return 'bg-gray-100/90 shadow-sm'
211-
case 'dark':
212-
return 'bg-neutral-900/70'
213-
default: // glass
214-
return 'bg-black/30'
215-
}
216-
}
217-
218-
const getTabContainerStyle = () => {
219-
switch (theme) {
220-
case 'light':
221-
return 'bg-gray-100/90'
222-
case 'dark':
223-
return 'bg-neutral-900/70'
224-
default: // glass
225-
return 'bg-black/50'
226-
}
227-
}
228-
229-
const getInactiveTabStyle = () => {
230-
switch (theme) {
231-
case 'light':
232-
return 'text-gray-500 hover:text-gray-700'
233-
default:
234-
return 'text-gray-400 hover:text-gray-300'
235-
}
236-
}
237-
238-
const getHeaderTextStyle = () => {
239-
switch (theme) {
240-
case 'light':
241-
return 'text-gray-700'
242-
default:
243-
return 'text-gray-200'
244-
}
245-
}
246-
24774
return (
24875
<section dir="rtl">
24976
<TodoProvider>
250-
<AnimatePresence mode="wait">
251-
{activeTab === 'calendar' ? (
252-
<motion.div
253-
key="calendar"
254-
className="min-h-[16rem] md:min-h-96 md:max-h-96"
255-
initial={{ opacity: 0, x: -20 }}
256-
animate={{ opacity: 1, x: 0 }}
257-
exit={{ opacity: 0, x: 20 }}
258-
transition={{ duration: 0.3 }}
259-
>
260-
<PersianCalendar />
261-
</motion.div>
262-
) : (
263-
<motion.div
264-
key="stats"
265-
initial={{ opacity: 0, x: -20 }}
266-
animate={{ opacity: 1, x: 0 }}
267-
exit={{ opacity: 0, x: 20 }}
268-
transition={{ duration: 0.3 }}
269-
className={`w-full max-w-2xl p-3 md:p-4 mx-auto min-h-[16rem] md:min-h-96 md:max-h-96 backdrop-blur-sm rounded-xl ${getStatsContainerStyle()}`}
270-
>
271-
<h2
272-
className={`mb-3 text-lg font-medium md:mb-4 md:text-xl ${getHeaderTextStyle()}`}
273-
>
274-
آمار و تحلیل یادداشت‌ها
275-
</h2>
276-
<TodoStats />
277-
</motion.div>
278-
)}
279-
</AnimatePresence>
280-
</TodoProvider>
281-
282-
<motion.div
283-
className="relative justify-center hidden mb-3 md:mb-4 md:flex"
284-
initial={{ opacity: 0, y: 20 }}
285-
animate={{ opacity: 1, y: 0 }}
286-
transition={{ duration: 0.3, delay: 0.2 }}
287-
>
288-
<div
289-
className={`absolute inline-flex p-1 left-3 bottom-[10rem] md:bottom-[20.9rem] backdrop-blur-sm rounded-xl ${getTabContainerStyle()}`}
77+
<motion.div
78+
key="calendar"
79+
className="min-h-[16rem] md:min-h-96 md:max-h-[38rem]"
80+
initial={{ opacity: 0, x: -20 }}
81+
animate={{ opacity: 1, x: 0 }}
82+
exit={{ opacity: 0, x: 20 }}
83+
transition={{ duration: 0.3 }}
29084
>
291-
<motion.button
292-
whileHover={{ scale: 1.05 }}
293-
whileTap={{ scale: 0.95 }}
294-
onClick={() => setActiveTab('calendar')}
295-
className={`px-3 md:px-4 py-2 rounded-lg transition-colors cursor-pointer ${
296-
activeTab === 'calendar' ? 'bg-blue-500 text-white' : getInactiveTabStyle()
297-
}`}
298-
>
299-
تقویم
300-
</motion.button>
301-
<motion.button
302-
whileHover={{ scale: 1.05 }}
303-
whileTap={{ scale: 0.95 }}
304-
onClick={() => setActiveTab('stats')}
305-
className={`px-3 md:px-4 py-2 rounded-lg transition-colors cursor-pointer ${
306-
activeTab === 'stats' ? 'bg-blue-500 text-white' : getInactiveTabStyle()
307-
}`}
308-
>
309-
آمار
310-
</motion.button>
311-
</div>
312-
</motion.div>
85+
<PersianCalendar />
86+
</motion.div>
87+
</TodoProvider>
31388
</section>
31489
)
31590
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type React from 'react'
2+
import { useTheme } from '../../../context/theme.context'
3+
4+
interface CalendarContainerProps {
5+
children: React.ReactNode
6+
className?: string
7+
}
8+
9+
export const CalendarContainer: React.FC<CalendarContainerProps> = ({
10+
children,
11+
className = '',
12+
}) => {
13+
const { themeUtils } = useTheme()
14+
15+
return (
16+
<div className={`rounded-xl ${themeUtils.getCardBackground()} ${className}`}>
17+
{children}
18+
</div>
19+
)
20+
}

0 commit comments

Comments
 (0)