Skip to content

Commit 2f76c42

Browse files
committed
feat(calendar): add pomodoro tab #14
1 parent 50e0538 commit 2f76c42

11 files changed

+736
-100
lines changed

src/layouts/calendar/calendar.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { CalendarHeader } from './components/calendar-header'
1111
import { DaySummary } from './components/day-summary'
1212
import { TabNavigation } from './components/tab-navigation'
1313

14-
export type TabType = 'events' | 'todos' | 'todo-stats'
14+
export type TabType = 'events' | 'todos' | 'todo-stats' | 'pomodoro'
1515

1616
const PersianCalendar: React.FC = () => {
1717
const { themeUtils } = useTheme()

src/layouts/calendar/components/calendar-content.tsx

+26-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { motion } from 'framer-motion'
22
import type jalaliMoment from 'jalali-moment'
33
import type React from 'react'
4+
import { useEffect, useState } from 'react'
45
import { useGetEvents } from '../../../services/getMethodHooks/getEvents.hook'
56
import type { TabType } from '../calendar'
67
import { DaySlider } from './day-slider'
78
import { Events } from './events/event'
9+
import { PomodoroTimer } from './pomodoro/pomodoro-timer'
810
import { TodoStats } from './todos/todo-stats'
911
import { Todos } from './todos/todos'
1012

@@ -23,12 +25,22 @@ export const CalendarContent: React.FC<CalendarContentProps> = ({
2325
setCurrentDate,
2426
}) => {
2527
const { data: events } = useGetEvents()
28+
const [showSlider, setShowSlider] = useState(true)
2629

30+
useEffect(() => {
31+
if (activeTab === 'pomodoro') {
32+
setShowSlider(false)
33+
} else {
34+
setShowSlider(true)
35+
}
36+
}, [activeTab])
2737
return (
2838
<>
29-
<div className="mb-4">
30-
<DaySlider currentDate={selectedDate} onDateChange={setSelectedDate} />
31-
</div>
39+
{showSlider ? (
40+
<div className="mb-4">
41+
<DaySlider currentDate={selectedDate} onDateChange={setSelectedDate} />
42+
</div>
43+
) : null}
3244

3345
{activeTab === 'events' && (
3446
<motion.div
@@ -66,6 +78,17 @@ export const CalendarContent: React.FC<CalendarContentProps> = ({
6678
<TodoStats />
6779
</motion.div>
6880
)}
81+
82+
{activeTab === 'pomodoro' && (
83+
<motion.div
84+
key="pomodoro-view"
85+
initial={{ opacity: 0 }}
86+
animate={{ opacity: 1 }}
87+
exit={{ opacity: 0 }}
88+
>
89+
<PomodoroTimer />
90+
</motion.div>
91+
)}
6992
</>
7093
)
7194
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { motion } from 'framer-motion'
2+
import type React from 'react'
3+
4+
interface ControlButtonProps {
5+
icon: React.ReactNode
6+
onClick: () => void
7+
color: string
8+
}
9+
10+
export const ControlButton: React.FC<ControlButtonProps> = ({ icon, onClick, color }) => {
11+
return (
12+
<motion.button
13+
whileHover={{ scale: 1.1 }}
14+
whileTap={{ scale: 0.95 }}
15+
onClick={onClick}
16+
className={`p-3 text-white rounded-full shadow-md transition-colors ${color}`}
17+
>
18+
{icon}
19+
</motion.button>
20+
)
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { motion } from 'framer-motion'
2+
import type React from 'react'
3+
import { FiCheck } from 'react-icons/fi'
4+
import { modeLabels } from '../constants'
5+
import type { TimerMode } from '../types'
6+
7+
interface ModeButtonProps {
8+
mode: TimerMode
9+
currentMode: TimerMode
10+
onClick: () => void
11+
getStyle: (mode: TimerMode) => string
12+
}
13+
14+
export const ModeButton: React.FC<ModeButtonProps> = ({
15+
mode,
16+
currentMode,
17+
onClick,
18+
getStyle,
19+
}) => {
20+
const isActive = mode === currentMode
21+
22+
return (
23+
<motion.button
24+
whileHover={{ scale: 1.05 }}
25+
whileTap={{ scale: 0.95 }}
26+
onClick={onClick}
27+
className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${getStyle(mode)}`}
28+
>
29+
{modeLabels[mode]} {isActive && <FiCheck className="inline" />}
30+
</motion.button>
31+
)
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { motion } from 'framer-motion'
2+
import type React from 'react'
3+
import Modal from '../../../../../components/modal'
4+
import type { PomodoroSettings } from '../types'
5+
6+
interface SettingInputProps {
7+
label: string
8+
value: number
9+
onChange: (value: number) => void
10+
max: number
11+
getInputStyle: () => string
12+
theme: string
13+
}
14+
15+
const SettingInput: React.FC<SettingInputProps> = ({
16+
label,
17+
value,
18+
onChange,
19+
max,
20+
getInputStyle,
21+
theme,
22+
}) => {
23+
return (
24+
<div className="flex items-center justify-between">
25+
<label
26+
className={`text-xs ${theme === 'light' ? 'text-gray-600' : 'text-gray-300'}`}
27+
>
28+
{label}
29+
</label>
30+
<input
31+
type="number"
32+
min="1"
33+
max={max}
34+
value={value}
35+
onChange={(e) => {
36+
const value = Number.parseInt(e.target.value)
37+
if (value > 0 && value <= max) {
38+
onChange(value)
39+
}
40+
}}
41+
className={`w-16 px-2 py-1 text-sm rounded border transition-colors ${getInputStyle()} ${theme === 'light' ? 'text-gray-700' : 'text-gray-200'}`}
42+
/>
43+
</div>
44+
)
45+
}
46+
47+
interface PomodoroSettingsPanelProps {
48+
isOpen: boolean
49+
onClose: () => void
50+
settings: PomodoroSettings
51+
onUpdateSettings: (newSettings: PomodoroSettings) => void
52+
onReset: () => void
53+
getTextStyle: () => string
54+
getInputStyle: () => string
55+
theme: string
56+
}
57+
58+
export const PomodoroSettingsPanel: React.FC<PomodoroSettingsPanelProps> = ({
59+
isOpen,
60+
onClose,
61+
settings,
62+
onUpdateSettings,
63+
onReset,
64+
getTextStyle,
65+
getInputStyle,
66+
theme,
67+
}) => {
68+
const handleSettingChange = (key: keyof PomodoroSettings, value: number) => {
69+
onUpdateSettings({
70+
...settings,
71+
[key]: value,
72+
})
73+
}
74+
75+
const handleSaveAndClose = () => {
76+
onClose()
77+
onReset()
78+
}
79+
80+
return (
81+
<Modal
82+
isOpen={isOpen}
83+
onClose={onClose}
84+
closeOnBackdropClick={false}
85+
title="تنظیمات تایمر پومودورو"
86+
direction="rtl"
87+
>
88+
<motion.div
89+
initial={{ opacity: 0, height: 0 }}
90+
animate={{ opacity: 1, height: 'auto' }}
91+
exit={{ opacity: 0, height: 0 }}
92+
transition={{ duration: 0.3 }}
93+
className="mt-6 overflow-hidden"
94+
>
95+
<div className={'p-4 rounded-xl'}>
96+
<h4 className={`text-sm font-medium mb-3 ${getTextStyle()}`}>
97+
تنظیمات زمان (دقیقه)
98+
</h4>
99+
100+
<div className="space-y-3">
101+
<SettingInput
102+
label="زمان کار:"
103+
value={settings.workTime}
104+
onChange={(value) => {
105+
handleSettingChange('workTime', value)
106+
}}
107+
max={60}
108+
getInputStyle={getInputStyle}
109+
theme={theme}
110+
/>
111+
112+
<SettingInput
113+
label="استراحت کوتاه:"
114+
value={settings.shortBreakTime}
115+
onChange={(value) => {
116+
handleSettingChange('shortBreakTime', value)
117+
}}
118+
max={30}
119+
getInputStyle={getInputStyle}
120+
theme={theme}
121+
/>
122+
123+
<SettingInput
124+
label="استراحت بلند:"
125+
value={settings.longBreakTime}
126+
onChange={(value) => {
127+
handleSettingChange('longBreakTime', value)
128+
}}
129+
max={60}
130+
getInputStyle={getInputStyle}
131+
theme={theme}
132+
/>
133+
134+
<SettingInput
135+
label="تعداد دوره قبل از استراحت بلند:"
136+
value={settings.cyclesBeforeLongBreak}
137+
onChange={(value) => {
138+
handleSettingChange('cyclesBeforeLongBreak', value)
139+
}}
140+
max={10}
141+
getInputStyle={getInputStyle}
142+
theme={theme}
143+
/>
144+
145+
<div className="text-center">
146+
<button
147+
onClick={handleSaveAndClose}
148+
className={`px-4 py-1 text-sm font-medium rounded-md transition-colors ${
149+
theme === 'light'
150+
? 'bg-blue-500 text-white hover:bg-blue-600'
151+
: 'bg-blue-600 text-gray-200 hover:bg-blue-700'
152+
}`}
153+
>
154+
ذخیره و بستن
155+
</button>
156+
</div>
157+
</div>
158+
</div>
159+
</motion.div>
160+
</Modal>
161+
)
162+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { motion } from 'framer-motion'
2+
import type React from 'react'
3+
import { modeFullLabels } from '../constants'
4+
import type { TimerMode } from '../types'
5+
6+
interface TimerDisplayProps {
7+
timeLeft: number
8+
progress: number
9+
mode: TimerMode
10+
theme: string
11+
getProgressColor: () => string
12+
getTextStyle: () => string
13+
cycles: number
14+
cyclesBeforeLongBreak: number
15+
}
16+
17+
export const TimerDisplay: React.FC<TimerDisplayProps> = ({
18+
timeLeft,
19+
progress,
20+
mode,
21+
theme,
22+
getProgressColor,
23+
getTextStyle,
24+
cycles,
25+
cyclesBeforeLongBreak,
26+
}) => {
27+
const formatTime = (seconds: number) => {
28+
const mins = Math.floor(seconds / 60)
29+
const secs = seconds % 60
30+
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
31+
}
32+
33+
return (
34+
<motion.div
35+
initial={{ scale: 0.9 }}
36+
animate={{ scale: 1 }}
37+
transition={{ type: 'spring', stiffness: 200, damping: 20 }}
38+
className="relative w-48 h-48 mx-auto mb-6"
39+
>
40+
<svg className="w-full h-full" viewBox="0 0 100 100">
41+
{/* Background circle */}
42+
<circle
43+
cx="50"
44+
cy="50"
45+
r="45"
46+
fill="none"
47+
stroke={theme === 'light' ? '#f3f4f6' : 'rgba(255,255,255,0.1)'}
48+
strokeWidth="5"
49+
/>
50+
51+
{/* Progress circle with animation */}
52+
<motion.circle
53+
initial={{ strokeDashoffset: 283 }}
54+
animate={{ strokeDashoffset: 283 - (283 * progress) / 100 }}
55+
transition={{ duration: 0.5 }}
56+
cx="50"
57+
cy="50"
58+
r="45"
59+
fill="none"
60+
stroke={getProgressColor()}
61+
strokeWidth="5"
62+
strokeDasharray="283"
63+
transform="rotate(-90 50 50)"
64+
/>
65+
66+
{/* Timer text */}
67+
<text
68+
x="50"
69+
y="50"
70+
className={getTextStyle()}
71+
textAnchor="middle"
72+
dominantBaseline="middle"
73+
fontSize="16"
74+
fontWeight="bold"
75+
fill="currentColor"
76+
>
77+
{formatTime(timeLeft)}
78+
</text>
79+
80+
{/* Current mode text */}
81+
<text
82+
x="50"
83+
y="65"
84+
textAnchor="middle"
85+
fontSize="6"
86+
fill={theme === 'light' ? '#6b7280' : '#9ca3af'}
87+
>
88+
{modeFullLabels[mode]}
89+
</text>
90+
</svg>
91+
92+
{/* Cycle indicator */}
93+
<div className="absolute flex space-x-1 -translate-x-1/2 left-1/2">
94+
{Array.from({ length: cyclesBeforeLongBreak }).map((_, i) => (
95+
<motion.div
96+
key={i}
97+
initial={{ scale: 0.8 }}
98+
animate={{ scale: i < cycles % cyclesBeforeLongBreak ? 1 : 0.8 }}
99+
className={`w-2 h-2 rounded-full ${
100+
i < (cycles % cyclesBeforeLongBreak)
101+
? 'bg-blue-500'
102+
: theme === 'light'
103+
? 'bg-gray-200'
104+
: 'bg-gray-700'
105+
}`}
106+
/>
107+
))}
108+
</div>
109+
</motion.div>
110+
)
111+
}

0 commit comments

Comments
 (0)