Skip to content

Commit fb7b54b

Browse files
committed
feat(calendar): add calendar & todos
1 parent 148ace0 commit fb7b54b

13 files changed

Lines changed: 539 additions & 6 deletions

File tree

bun.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
"axios": "1.7.9",
1919
"chart.js": "^4.4.7",
2020
"jalali-moment": "3.3.11",
21+
"moment": "^2.30.1",
22+
"moment-hijri": "^3.0.0",
2123
"motion": "12.3.1",
2224
"ms": "2.1.3",
2325
"react": "18.3.1",
@@ -34,6 +36,7 @@
3436
"@biomejs/biome": "1.9.4",
3537
"@eslint/js": "9.17.0",
3638
"@tailwindcss/vite": "4.0.0",
39+
"@types/moment-hijri": "^2.1.4",
3740
"@types/ms": "2.1.0",
3841
"@types/node": "22.13.1",
3942
"@types/react": "18.2.19",

src/layouts/calendar/calendar.tsx

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import jalaliMoment from 'jalali-moment'
2+
import type React from 'react'
3+
import { useState } from 'react'
4+
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa6'
5+
import { useGetEvents } from '../../services/getMethodHooks/getEvents.hook'
6+
import { DayItem } from './components/day'
7+
import { Events } from './components/events/event'
8+
import { Todos } from './components/todos/todos'
9+
import type { Todo } from './interface/todo.interface'
10+
import { formatDateStr } from './utils'
11+
12+
const PERSIAN_MONTHS = [
13+
'فروردین',
14+
'اردیبهشت',
15+
'خرداد',
16+
'تیر',
17+
'مرداد',
18+
'شهریور',
19+
'مهر',
20+
'آبان',
21+
'آذر',
22+
'دی',
23+
'بهمن',
24+
'اسفند',
25+
]
26+
27+
const WEEKDAYS = ['شنبه', 'یکشنبه', 'دوشنبه', 'سه‌شنبه', 'چهارشنبه', 'پنج‌شنبه', 'جمعه']
28+
29+
export const PersianCalendar: React.FC = () => {
30+
const today = jalaliMoment()
31+
const [currentDate, setCurrentDate] = useState(today)
32+
const [selectedDate, setSelectedDate] = useState(today.clone())
33+
34+
const firstDayOfMonth = currentDate.clone().startOf('jMonth').day()
35+
36+
const { data: events } = useGetEvents()
37+
38+
const daysInMonth = currentDate.clone().endOf('jMonth').jDate()
39+
40+
const emptyDays = (firstDayOfMonth + 1) % 7
41+
42+
const changeMonth = (delta: number) => {
43+
setCurrentDate((prev) => prev.clone().add(delta, 'jMonth'))
44+
}
45+
46+
const selectedDateStr = formatDateStr(selectedDate)
47+
48+
const todos: Todo[] = []
49+
return (
50+
<div className="grid gap-4 md:grid-cols-5" dir="rtl">
51+
<div className="p-4 md:col-span-3 bg-gray-800/50 rounded-xl backdrop-blur-sm lg:h-96">
52+
<div className="flex items-center justify-between mb-4">
53+
<h3 className="text-xl font-medium text-gray-200">
54+
{PERSIAN_MONTHS[currentDate.jMonth()]} {currentDate.jYear()}
55+
</h3>
56+
<div className="flex gap-2">
57+
<button
58+
onClick={() => changeMonth(-1)}
59+
className="flex flex-row-reverse items-center gap-1 p-2 text-gray-400 rounded-lg hover:text-gray-200 hover:bg-gray-700/50"
60+
>
61+
ماه قبل
62+
<FaChevronRight />
63+
</button>
64+
<button
65+
onClick={() => changeMonth(1)}
66+
className="flex flex-row-reverse items-center gap-1 p-2 text-gray-400 rounded-lg hover:text-gray-200 hover:bg-gray-700/50"
67+
>
68+
<FaChevronLeft />
69+
ماه بعد
70+
</button>
71+
</div>
72+
</div>
73+
74+
<div className="grid grid-cols-7 gap-2">
75+
{WEEKDAYS.map((day) => (
76+
<div key={day} className="py-2 text-xs text-center text-gray-400">
77+
{day}
78+
</div>
79+
))}
80+
81+
{Array.from({ length: emptyDays }).map((_, i) => (
82+
<div key={`empty-${i}`} className="p-2" />
83+
))}
84+
85+
{Array.from({ length: daysInMonth }, (_, i) => {
86+
return (
87+
<DayItem
88+
currentDate={currentDate}
89+
day={i + 1}
90+
events={events}
91+
selectedDateStr={selectedDateStr}
92+
setSelectedDate={setSelectedDate}
93+
todos={todos}
94+
key={i + 1}
95+
/>
96+
)
97+
})}
98+
</div>
99+
</div>
100+
101+
<div className="p-4 md:col-span-2 bg-gray-800/50 rounded-xl backdrop-blur-sm">
102+
<h3 className="mb-4 text-xl font-medium text-gray-200">
103+
{PERSIAN_MONTHS[selectedDate.jMonth()]} {selectedDate.jDate()}
104+
</h3>
105+
106+
{/* todos */}
107+
<Todos currentDate={selectedDate} />
108+
109+
{/* events */}
110+
<Events events={events} currentDate={selectedDate} />
111+
</div>
112+
</div>
113+
)
114+
}
115+
116+
const CalendarLayout = () => {
117+
return (
118+
<section className="p-2 mx-1 overflow-y-auto rounded lg:mx-4 max-h-[calc(100vh-4rem)]">
119+
<div className="flex items-center justify-between w-full px-1 mb-4">
120+
<h2 className="text-lg font-semibold text-gray-200 font-[balooTamma]">
121+
📅 Calender
122+
</h2>
123+
<div
124+
className="text-xs text-gray-400 font-[balooTamma] font-semibold flex items-center gap-1
125+
hover:text-gray-300 cursor-pointer"
126+
>
127+
<span>-</span>
128+
</div>
129+
</div>
130+
<PersianCalendar />
131+
</section>
132+
)
133+
}
134+
135+
export default CalendarLayout
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import jalaliMoment from 'jalali-moment'
2+
import type { FetchedAllEvents } from '../../../services/getMethodHooks/getEvents.hook'
3+
import type { Todo } from '../interface/todo.interface'
4+
import {
5+
convertShamsiToHijri,
6+
formatDateStr,
7+
getHijriEvents,
8+
getShamsiEvents,
9+
} from '../utils'
10+
interface DayItemProps {
11+
day: number
12+
currentDate: jalaliMoment.Moment
13+
events: FetchedAllEvents
14+
todos: Todo[]
15+
selectedDateStr: string
16+
setSelectedDate: (date: jalaliMoment.Moment) => void
17+
}
18+
export function DayItem({
19+
day,
20+
currentDate,
21+
events,
22+
todos,
23+
selectedDateStr,
24+
setSelectedDate,
25+
}: DayItemProps) {
26+
const cellDate = currentDate.clone().jDate(day)
27+
28+
const dateStr = formatDateStr(cellDate)
29+
30+
const todayShamsiEvent = getShamsiEvents(events, cellDate)
31+
const todayHijriEvent = getHijriEvents(events, cellDate)
32+
33+
const hasEvent = todayShamsiEvent.length || todayHijriEvent.length
34+
35+
const hasTodo = todos.some((todo) => todo.date === dateStr)
36+
37+
const isSelected = selectedDateStr === dateStr
38+
39+
const isHoliday =
40+
cellDate.day() === 5 ||
41+
todayShamsiEvent.some((event) => event.isHoliday) ||
42+
todayHijriEvent.some((event) => event.isHoliday)
43+
44+
return (
45+
<button
46+
key={day}
47+
onClick={() => setSelectedDate(cellDate)}
48+
className={`
49+
relative p-2 rounded-lg text-sm transition-colors
50+
${isHoliday ? 'text-red-400' : 'text-gray-300'}
51+
${isSelected ? 'bg-blue-500/20' : 'hover:bg-gray-700/50'}
52+
${todayShamsiEvent || hasTodo ? 'font-bold' : ''}
53+
${isToday(cellDate) ? 'ring-2 ring-blue-500' : ''}
54+
`}
55+
>
56+
{day}
57+
<div className="absolute flex gap-1 -translate-x-1/2 bottom-1 left-1/2">
58+
{hasEvent ? <span className="w-1 h-1 bg-blue-500 rounded-full" /> : null}
59+
{hasTodo ? <span className="w-1 h-1 bg-green-500 rounded-full" /> : null}
60+
</div>
61+
</button>
62+
)
63+
}
64+
65+
const isToday = (date: jalaliMoment.Moment) => {
66+
const today = jalaliMoment()
67+
return (
68+
date.jDate() === today.jDate() &&
69+
date.jMonth() === today.jMonth() &&
70+
date.jYear() === today.jYear()
71+
)
72+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type jalaliMoment from 'jalali-moment'
2+
import type { FetchedAllEvents } from '../../../../services/getMethodHooks/getEvents.hook'
3+
import type { Event } from '../../interface/event.interface'
4+
import { getGregorianEvents, getHijriEvents, getShamsiEvents } from '../../utils'
5+
6+
interface Prop {
7+
// events: Event[]
8+
events: FetchedAllEvents
9+
currentDate: jalaliMoment.Moment
10+
}
11+
12+
const getEventTypeColor = (type: Event['type']) => {
13+
switch (type) {
14+
case 'holiday':
15+
return 'bg-red-500/10 text-red-500'
16+
case 'event':
17+
return 'bg-blue-500/10 text-blue-500'
18+
case 'news':
19+
return 'bg-yellow-500/10 text-yellow-500'
20+
default:
21+
return 'bg-gray-500/10 text-gray-500'
22+
}
23+
}
24+
25+
export function Events({ events, currentDate }: Prop) {
26+
const shamsiEvents = getShamsiEvents(events, currentDate)
27+
28+
const gregorianEvents = getGregorianEvents(events, currentDate)
29+
30+
const hijriEvents = getHijriEvents(events, currentDate)
31+
32+
const selectedEvents = [...shamsiEvents, ...gregorianEvents, ...hijriEvents]
33+
34+
return (
35+
<div>
36+
<h4 className="mb-2 text-lg text-gray-300">رویدادها</h4>
37+
<div className="h-40 space-y-3 overflow-y-auto lg:h-32">
38+
{selectedEvents.length > 0 ? (
39+
selectedEvents.map((event, index) => (
40+
<div
41+
key={index}
42+
className={`p-3 rounded-lg ${getEventTypeColor(event.isHoliday ? 'holiday' : 'event')}`}
43+
>
44+
<div className="font-medium">{event.title}</div>
45+
{/* {event.description && (
46+
<p className="mt-1 text-sm text-gray-300">{event.description}</p>
47+
)} */}
48+
</div>
49+
))
50+
) : (
51+
<div className="py-4 text-center text-gray-400">
52+
رویدادی برای این روز ثبت نشده است
53+
</div>
54+
)}
55+
</div>
56+
</div>
57+
)
58+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { useState } from 'react'
2+
3+
interface Prop {
4+
onAdd: (text: string) => void
5+
}
6+
7+
export function TodoInput({ onAdd }: Prop) {
8+
const [text, setText] = useState('')
9+
10+
const handleSubmit = (e: React.FormEvent) => {
11+
e.preventDefault()
12+
if (text.trim()) {
13+
onAdd(text.trim())
14+
setText('')
15+
}
16+
}
17+
18+
return (
19+
<form onSubmit={handleSubmit} className="flex gap-2 mb-4">
20+
<input
21+
type="text"
22+
value={text}
23+
onChange={(e) => setText(e.target.value)}
24+
placeholder="یادداشت جدید..."
25+
className="flex-1 px-3 py-2 text-gray-200 placeholder-gray-400 rounded-lg bg-gray-700/50 focus:outline-none focus:ring-2 focus:ring-blue-500"
26+
/>
27+
<button
28+
type="submit"
29+
className="px-4 py-2 text-white transition-colors bg-blue-500 rounded-lg hover:bg-blue-600"
30+
>
31+
افزودن
32+
</button>
33+
</form>
34+
)
35+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { Todo } from '../../interface/todo.interface'
2+
3+
interface Prop {
4+
todo: Todo
5+
toggleTodo: (id: string) => void
6+
deleteTodo: (id: string) => void
7+
}
8+
9+
export function TodoItem({ todo, deleteTodo, toggleTodo }: Prop) {
10+
return (
11+
<div
12+
key={todo.id}
13+
className="flex items-center gap-2 p-3 rounded-lg bg-gray-700/30 group"
14+
>
15+
<input
16+
type="checkbox"
17+
checked={todo.completed}
18+
onChange={() => toggleTodo(todo.id)}
19+
className="w-4 h-4 border-gray-500 rounded"
20+
/>
21+
<span
22+
className={`flex-1 text-gray-200 ${todo.completed ? 'line-through text-gray-400' : ''}`}
23+
>
24+
{todo.text}
25+
</span>
26+
<button
27+
onClick={() => deleteTodo(todo.id)}
28+
className="text-red-400 transition-opacity opacity-0 group-hover:opacity-100 hover:text-red-300"
29+
>
30+
31+
</button>
32+
</div>
33+
)
34+
}

0 commit comments

Comments
 (0)