);
diff --git a/src/components/settings/sections/OthersSettings.tsx b/src/components/settings/sections/OthersSettings.tsx
index 6b64277..1213690 100644
--- a/src/components/settings/sections/OthersSettings.tsx
+++ b/src/components/settings/sections/OthersSettings.tsx
@@ -1,13 +1,12 @@
import DonateField from '../fields/DonateField';
import FeedbackField from '../fields/FeedbackField';
+import VersionField from '../fields/VersionField';
const OthersSettings: React.FC = () => (
-
-
Others
-
-
-
-
+
+
+
+
);
diff --git a/src/components/settings/sections/StatsSettings.tsx b/src/components/settings/sections/StatsSettings.tsx
new file mode 100644
index 0000000..8f3b886
--- /dev/null
+++ b/src/components/settings/sections/StatsSettings.tsx
@@ -0,0 +1,268 @@
+import React, { useState } from 'react';
+import { IoIosShareAlt } from 'react-icons/io';
+import { MdCheckCircle, MdDeleteOutline } from 'react-icons/md';
+import { useStatsStore } from '../../../store/statsStore';
+import { useThemeStore } from '../../../store/themeStore';
+import { getCardSurface } from '../../../utils/colorUtils';
+import Button from '../../common/Button';
+
+const StatsSettings: React.FC = () => {
+ const { selectedTheme, compColor } = useThemeStore();
+ const { sessions, getTodayMinutes, getWeeklyMinutes, getStreakDays, clearHistory } = useStatsStore();
+
+ const [isGeneratingShare, setIsGeneratingShare] = useState(false);
+ const todayMinutes = getTodayMinutes();
+ const weeklyMinutes = getWeeklyMinutes();
+ const streakDays = getStreakDays();
+
+ const { cardBg, cardBorder } = getCardSurface(
+ selectedTheme.color.main,
+ selectedTheme.color.sub,
+ selectedTheme.color.point
+ );
+
+ const formatHoursMins = (totalMinutes: number) => {
+ const hours = Math.floor(totalMinutes / 60);
+ const mins = totalMinutes % 60;
+ if (hours === 0) return `${mins}m`;
+ return `${hours}h ${mins}m`;
+ };
+
+ const handleShareCard = async () => {
+ setIsGeneratingShare(true);
+ try {
+ const canvas = document.createElement('canvas');
+ canvas.width = 1080;
+ canvas.height = 1080;
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return;
+
+ const gradient = ctx.createLinearGradient(0, 0, 1080, 1080);
+ gradient.addColorStop(0, selectedTheme.color.main);
+ gradient.addColorStop(1, selectedTheme.color.sub);
+ ctx.fillStyle = gradient;
+ ctx.fillRect(0, 0, 1080, 1080);
+
+ ctx.fillStyle = '#FFFFFF';
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.15)';
+ ctx.shadowBlur = 40;
+ ctx.shadowOffsetY = 20;
+ ctx.beginPath();
+ ctx.roundRect(90, 90, 900, 900, 48);
+ ctx.fill();
+ ctx.shadowColor = 'transparent';
+
+ ctx.fillStyle = selectedTheme.color.point;
+ ctx.font = 'bold 36px "Plus Jakarta Sans", sans-serif';
+ ctx.textAlign = 'center';
+ ctx.fillText('⏰ MELLOW VISUAL TIMER', 540, 190);
+
+ ctx.fillStyle = '#666666';
+ ctx.font = '500 28px "Plus Jakarta Sans", sans-serif';
+ const dateStr = new Date().toLocaleDateString('en-US', {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ });
+ ctx.fillText(dateStr, 540, 240);
+
+ ctx.lineWidth = 24;
+ ctx.strokeStyle = '#F0F0F0';
+ ctx.beginPath();
+ ctx.arc(540, 470, 150, 0, 2 * Math.PI);
+ ctx.stroke();
+
+ ctx.strokeStyle = selectedTheme.color.point;
+ ctx.lineCap = 'round';
+ ctx.beginPath();
+ ctx.arc(540, 470, 150, -Math.PI / 2, Math.PI * 0.9);
+ ctx.stroke();
+
+ ctx.fillStyle = '#1A1A1A';
+ ctx.font = 'bold 72px "Outfit", sans-serif';
+ ctx.fillText(formatHoursMins(todayMinutes), 540, 465);
+
+ ctx.fillStyle = '#888888';
+ ctx.font = '600 24px "Plus Jakarta Sans", sans-serif';
+ ctx.fillText('TODAY FOCUSED', 540, 510);
+
+ ctx.fillStyle = '#FFF5EB';
+ ctx.beginPath();
+ ctx.roundRect(160, 670, 360, 100, 24);
+ ctx.fill();
+ ctx.fillStyle = '#E65100';
+ ctx.font = 'bold 32px "Plus Jakarta Sans", sans-serif';
+ ctx.fillText(`🔥 ${streakDays} Day Streak`, 340, 730);
+
+ ctx.fillStyle = '#F0F9FF';
+ ctx.beginPath();
+ ctx.roundRect(560, 670, 360, 100, 24);
+ ctx.fill();
+ ctx.fillStyle = '#0284C7';
+ ctx.font = 'bold 32px "Plus Jakarta Sans", sans-serif';
+ ctx.fillText(`📊 ${formatHoursMins(weeklyMinutes)} This Week`, 740, 730);
+
+ ctx.fillStyle = '#444444';
+ ctx.font = 'italic 500 28px "Plus Jakarta Sans", sans-serif';
+ const quote = selectedTheme.text.replace(/\n/g, ' ');
+ ctx.fillText(`"${quote}"`, 540, 840);
+
+ ctx.fillStyle = '#999999';
+ ctx.font = '500 20px "Plus Jakarta Sans", sans-serif';
+ ctx.fillText('visual-timer • do0ori.github.io/visual-timer', 540, 930);
+
+ canvas.toBlob(async (blob) => {
+ if (!blob) return;
+ const file = new File([blob], `visual-timer-focus-${new Date().toISOString().split('T')[0]}.png`, {
+ type: 'image/png',
+ });
+
+ if (navigator.canShare && navigator.canShare({ files: [file] })) {
+ try {
+ await navigator.share({
+ files: [file],
+ title: 'Mellow Visual Timer Focus',
+ text: `I focused for ${formatHoursMins(todayMinutes)} today with Mellow Visual Timer! 🎯🔥`,
+ });
+ return;
+ } catch {
+ // fallback
+ }
+ }
+
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `mellow-focus-${Date.now()}.png`;
+ a.click();
+ URL.revokeObjectURL(url);
+ }, 'image/png');
+ } catch (err) {
+ console.error('Error generating share card:', err);
+ } finally {
+ setIsGeneratingShare(false);
+ }
+ };
+
+ return (
+
+ {/* KPI Cards — same white base as SNS share card */}
+
+ {/* Card 1: Today Focus */}
+
+
+ ⏱️
+
+
+ {formatHoursMins(todayMinutes)}
+
+
+ Today
+
+
+
+ {/* Card 2: Streak */}
+
+
+ 🔥
+
+
+ {streakDays}
+
+
+ Day Streak
+
+
+
+ {/* Card 3: 7 Days Total */}
+
+
+ 📈
+
+
+ {formatHoursMins(weeklyMinutes)}
+
+
+ 7 Days
+
+
+
+
+
+
+ {isGeneratingShare ? 'Generating Card...' : 'Save & Share Image Card'}
+
+
+ {/* Activity History */}
+
+
+
+ Completed Sessions History ({sessions.length})
+
+ {sessions.length > 0 && (
+
+ Clear History
+
+ )}
+
+
+ {sessions.length === 0 ? (
+
+
No sessions logged yet.
+
Finish a timer countdown to record focus time here!
+
+ ) : (
+
+ {sessions.slice(0, 30).map((session) => (
+
+
+
+
+
{session.timerTitle || 'Focus Session'}
+
+ {new Date(session.completedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} •{' '}
+ {new Date(session.completedAt).toLocaleDateString([], { month: 'short', day: 'numeric' })}
+
+
+
+
+ +{session.durationMinutes}m
+
+
+ ))}
+
+ )}
+
+
+ );
+};
+
+export default StatsSettings;
diff --git a/src/components/settings/sections/ThemeSettings.tsx b/src/components/settings/sections/ThemeSettings.tsx
index e23eaa3..cd530d9 100644
--- a/src/components/settings/sections/ThemeSettings.tsx
+++ b/src/components/settings/sections/ThemeSettings.tsx
@@ -2,12 +2,9 @@ import CustomThemeSelector from '../fields/CustomThemeSelector';
import DefaultThemeSelector from '../fields/DefaultThemeSelector';
const ThemeSettings: React.FC = () => (
-
-
Theme
-
-
-
-
+
+
+
);
diff --git a/src/components/settings/sections/TimerSettings.tsx b/src/components/settings/sections/TimerSettings.tsx
index 1d9e260..deb94de 100644
--- a/src/components/settings/sections/TimerSettings.tsx
+++ b/src/components/settings/sections/TimerSettings.tsx
@@ -1,11 +1,8 @@
import DirectionSelector from '../fields/DirectionSelector';
const TimerSettings: React.FC = () => (
-
-
Timer
-
-
-
+
+
);
diff --git a/src/components/stats/StatsOverlay.tsx b/src/components/stats/StatsOverlay.tsx
new file mode 100644
index 0000000..4b4c500
--- /dev/null
+++ b/src/components/stats/StatsOverlay.tsx
@@ -0,0 +1,322 @@
+import React, { useRef, useState } from 'react';
+import { IoIosShareAlt, IoMdClose, IoMdFlame, IoMdTime } from 'react-icons/io';
+import { MdCheckCircle, MdDeleteOutline, MdTrendingUp, MdWorkspacePremium } from 'react-icons/md';
+import { useOverlay } from '../../hooks/useOverlay';
+import { useStatsStore } from '../../store/statsStore';
+import { useThemeStore } from '../../store/themeStore';
+import Button from '../common/Button';
+
+export const StatsOverlay: React.FC = () => {
+ const { selectedTheme, compColor } = useThemeStore();
+ const { sessions, getTodayMinutes, getWeeklyMinutes, getStreakDays, clearHistory } = useStatsStore();
+ const { isOpen, close } = useOverlay('stats');
+
+ const [isGeneratingShare, setIsGeneratingShare] = useState(false);
+ const shareCardRef = useRef
(null);
+
+ const todayMinutes = getTodayMinutes();
+ const weeklyMinutes = getWeeklyMinutes();
+ const streakDays = getStreakDays();
+
+ const formatHoursMins = (totalMinutes: number) => {
+ const hours = Math.floor(totalMinutes / 60);
+ const mins = totalMinutes % 60;
+ if (hours === 0) return `${mins}m`;
+ return `${hours}h ${mins}m`;
+ };
+
+ const handleShareCard = async () => {
+ setIsGeneratingShare(true);
+ try {
+ const canvas = document.createElement('canvas');
+ canvas.width = 1080;
+ canvas.height = 1080;
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return;
+
+ // Background Gradient
+ const gradient = ctx.createLinearGradient(0, 0, 1080, 1080);
+ gradient.addColorStop(0, selectedTheme.color.main);
+ gradient.addColorStop(1, selectedTheme.color.sub);
+ ctx.fillStyle = gradient;
+ ctx.fillRect(0, 0, 1080, 1080);
+
+ // Card Container
+ ctx.fillStyle = '#FFFFFF';
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.15)';
+ ctx.shadowBlur = 40;
+ ctx.shadowOffsetY = 20;
+ ctx.beginPath();
+ ctx.roundRect(90, 90, 900, 900, 48);
+ ctx.fill();
+ ctx.shadowColor = 'transparent';
+
+ // Header App Title
+ ctx.fillStyle = selectedTheme.color.point;
+ ctx.font = 'bold 36px "Plus Jakarta Sans", sans-serif';
+ ctx.textAlign = 'center';
+ ctx.fillText('⏰ MELLOW VISUAL TIMER', 540, 190);
+
+ // Date
+ ctx.fillStyle = '#666666';
+ ctx.font = '500 28px "Plus Jakarta Sans", sans-serif';
+ const dateStr = new Date().toLocaleDateString('en-US', {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ });
+ ctx.fillText(dateStr, 540, 240);
+
+ // Decorative Dial Arc
+ ctx.lineWidth = 24;
+ ctx.strokeStyle = '#F0F0F0';
+ ctx.beginPath();
+ ctx.arc(540, 470, 150, 0, 2 * Math.PI);
+ ctx.stroke();
+
+ ctx.strokeStyle = selectedTheme.color.point;
+ ctx.lineCap = 'round';
+ ctx.beginPath();
+ ctx.arc(540, 470, 150, -Math.PI / 2, Math.PI * 0.9);
+ ctx.stroke();
+
+ // Big Time Number inside Dial
+ ctx.fillStyle = '#1A1A1A';
+ ctx.font = 'bold 72px "Outfit", sans-serif';
+ ctx.fillText(formatHoursMins(todayMinutes), 540, 465);
+
+ ctx.fillStyle = '#888888';
+ ctx.font = '600 24px "Plus Jakarta Sans", sans-serif';
+ ctx.fillText('TODAY FOCUSED', 540, 510);
+
+ // Streak & Weekly Stat Pills
+ ctx.fillStyle = '#FFF5EB';
+ ctx.beginPath();
+ ctx.roundRect(160, 670, 360, 100, 24);
+ ctx.fill();
+ ctx.fillStyle = '#E65100';
+ ctx.font = 'bold 32px "Plus Jakarta Sans", sans-serif';
+ ctx.fillText(`🔥 ${streakDays} Day Streak`, 340, 730);
+
+ ctx.fillStyle = '#F0F9FF';
+ ctx.beginPath();
+ ctx.roundRect(560, 670, 360, 100, 24);
+ ctx.fill();
+ ctx.fillStyle = '#0284C7';
+ ctx.font = 'bold 32px "Plus Jakarta Sans", sans-serif';
+ ctx.fillText(`📊 ${formatHoursMins(weeklyMinutes)} This Week`, 740, 730);
+
+ // Motivational Quote
+ ctx.fillStyle = '#444444';
+ ctx.font = 'italic 500 28px "Plus Jakarta Sans", sans-serif';
+ const quote = selectedTheme.text.replace(/\n/g, ' ');
+ ctx.fillText(`"${quote}"`, 540, 840);
+
+ // Branding Footer
+ ctx.fillStyle = '#999999';
+ ctx.font = '500 20px "Plus Jakarta Sans", sans-serif';
+ ctx.fillText('visual-timer • do0ori.github.io/visual-timer', 540, 930);
+
+ canvas.toBlob(async (blob) => {
+ if (!blob) return;
+ const file = new File([blob], `visual-timer-focus-${new Date().toISOString().split('T')[0]}.png`, {
+ type: 'image/png',
+ });
+
+ if (navigator.canShare && navigator.canShare({ files: [file] })) {
+ try {
+ await navigator.share({
+ files: [file],
+ title: 'Mellow Visual Timer Focus',
+ text: `I focused for ${formatHoursMins(todayMinutes)} today with Mellow Visual Timer! 🎯🔥`,
+ });
+ return;
+ } catch {
+ // fallback
+ }
+ }
+
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `mellow-focus-${Date.now()}.png`;
+ a.click();
+ URL.revokeObjectURL(url);
+ }, 'image/png');
+ } catch (err) {
+ console.error('Error generating share card:', err);
+ } finally {
+ setIsGeneratingShare(false);
+ }
+ };
+
+ if (!isOpen) return null;
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
Focus Stats & Achievement
+
+
+
+
+
+
+ {/* Content Body with 2-Column Desktop Grid */}
+
+
+ {/* Left Column: Share Card Studio */}
+
+
+ Daily SNS Share Card (Instagram / Twitter)
+
+
+
+
+ ✨
+
+ Daily Achievement Card
+
+
+
+
+
+ {formatHoursMins(todayMinutes)}
+
+
Total focus time logged today
+
+
+
+
+ 🔥 {streakDays} Day Streak
+
+
+ 📊 {formatHoursMins(weeklyMinutes)} This Week
+
+
+
+
+ "{selectedTheme.text.replace(/\n/g, ' ')}"
+
+
+
+
+ {isGeneratingShare ? 'Rendering High-Res Card...' : 'Download & Share Card'}
+
+
+
+
+ {/* Right Column: Statistics & Session History */}
+
+ {/* Metric KPI Cards */}
+
+
+
+ {formatHoursMins(todayMinutes)}
+ Today
+
+
+
+
+ {streakDays}
+ Day Streak
+
+
+
+
+ {formatHoursMins(weeklyMinutes)}
+ 7 Days
+
+
+
+ {/* Sessions History List */}
+
+
+
+ Activity History ({sessions.length})
+
+ {sessions.length > 0 && (
+
+ Clear History
+
+ )}
+
+
+ {sessions.length === 0 ? (
+
+
No completed sessions yet.
+
Complete your focus timers to see your accomplishments here!
+
+ ) : (
+
+ {sessions.slice(0, 30).map((session) => (
+
+
+
+
+
{session.timerTitle || 'Focus Session'}
+
+ {new Date(session.completedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} •{' '}
+ {new Date(session.completedAt).toLocaleDateString([], { month: 'short', day: 'numeric' })}
+
+
+
+
+ +{session.durationMinutes}m
+
+
+ ))}
+
+ )}
+
+
+
+
+
+
+ );
+};
+
+export default StatsOverlay;
diff --git a/src/components/timers/shared/TimerContent.tsx b/src/components/timers/shared/TimerContent.tsx
index d884e5c..aa929cf 100644
--- a/src/components/timers/shared/TimerContent.tsx
+++ b/src/components/timers/shared/TimerContent.tsx
@@ -1,53 +1,54 @@
+import React from 'react';
import { useAspectRatio } from '../../../hooks/useAspectRatio';
import Layout from '../../common/Layout';
export type TimerContentProps = {
- top: {
- leftChildren: React.ReactNode;
- rightChildren: React.ReactNode;
- };
- bottom: React.ReactNode;
- timerInfo: React.ReactNode;
- timer: React.ReactNode;
+ top: {
+ leftChildren: React.ReactNode;
+ rightChildren: React.ReactNode;
+ };
+ bottom: React.ReactNode;
+ timerInfo: React.ReactNode;
+ timer: React.ReactNode;
};
const TimerContent: React.FC = ({ top, bottom, timerInfo, timer }) => {
- const content = {
- top: (
-
- {top.leftChildren}
- {top.rightChildren}
-
- ),
- bottom: {bottom}
,
- timerInfo: {timerInfo}
,
- timer,
- };
+ const content = {
+ top: (
+
+ {top.leftChildren}
+ {top.rightChildren}
+
+ ),
+ bottom: {bottom}
,
+ timerInfo: {timerInfo}
,
+ timer,
+ };
- const aspectRatio = useAspectRatio();
+ const aspectRatio = useAspectRatio();
- return aspectRatio > 1 ? (
-
- {content.top}
- {content.timerInfo}
- {content.bottom}
-
- }
- />
- ) : (
-
-
- {content.top}
- {content.timerInfo}
-
{content.timer}
- {content.bottom}
-
-
- );
+ return aspectRatio > 1 ? (
+
+ {content.top}
+ {content.timerInfo}
+ {content.bottom}
+
+ }
+ />
+ ) : (
+
+
+ {content.top}
+ {content.timerInfo}
+
{content.timer}
+ {content.bottom}
+
+
+ );
};
export default TimerContent;
diff --git a/src/components/timers/shared/controls/ControlButtons.tsx b/src/components/timers/shared/controls/ControlButtons.tsx
index be94274..bc8bbd4 100644
--- a/src/components/timers/shared/controls/ControlButtons.tsx
+++ b/src/components/timers/shared/controls/ControlButtons.tsx
@@ -1,103 +1,99 @@
+import React from 'react';
import { IoAdd, IoList, IoPause, IoPlay, IoRefresh, IoSettingsSharp } from 'react-icons/io5';
import { Theme } from '../../../../store/types/theme';
import Button from '../../../common/Button';
-import SettingsOverlay from '../../../settings/SettingsOverlay';
-import TimerListOverlay from '../../timer-management/TimerListOverlay';
type ControlButtonsProps = {
- isMinutes: boolean;
- isRunning: boolean;
- isInitialized: boolean;
- currentTheme: Theme;
- start: () => void;
- stop: () => void;
- reset: () => void;
- add: (time: number) => void;
+ isMinutes: boolean;
+ isRunning: boolean;
+ isInitialized: boolean;
+ currentTheme: Theme;
+ start: () => void;
+ stop: () => void;
+ reset: () => void;
+ add: (time: number) => void;
};
const ControlButtons: React.FC
= ({
- isMinutes,
- isRunning,
- isInitialized,
- currentTheme,
- start,
- stop,
- reset,
- add,
+ isMinutes,
+ isRunning,
+ isInitialized,
+ currentTheme,
+ start,
+ stop,
+ reset,
+ add,
}) => {
- const startWithPermissionCheck = async () => {
- if (!('Notification' in window)) {
- alert('Your browser does not support notifications.');
- return;
- }
+ const handleStart = async () => {
+ if ('Notification' in window && Notification.permission === 'default') {
+ try {
+ await Notification.requestPermission();
+ } catch (err) {
+ console.debug('Notification permission request skipped:', err);
+ }
+ }
+ start();
+ };
- if (Notification.permission === 'denied') {
- alert('Notifications are blocked. Please enable notifications in your browser settings to use the timer.');
- return;
- }
+ return (
+
+ {/* Left Action: Timer List / Add Time */}
+
+ {isInitialized ? (
+
(window.location.hash = 'timer-list')}
+ aria-label="Timer List"
+ title="Timer Presets & Routines"
+ >
+
+
+ ) : (
+
add(isMinutes ? 1 : 10)}
+ aria-label="Add time"
+ currentTheme={currentTheme}
+ >
+
+
+ {isMinutes ? 1 : 10}
+
+
+ )}
+
- if (Notification.permission === 'default') {
- try {
- const permission = await Notification.requestPermission();
- if (permission !== 'granted') {
- alert('You need to allow notifications to use the timer.');
- return;
- }
- } catch (error) {
- console.error('Notification request failed:', error);
- alert('Failed to request notification permissions. Please try again.');
- return;
- }
- }
+ {/* Center Action: Start / Stop Button (Exact Center) */}
+
+
+ {isRunning ? : }
+
+
- start(); // If permission is granted, start the timer
- };
-
- return (
-
- {isInitialized ? (
- <>
- {/* Timer List Button */}
-
(window.location.hash = 'timer-list')} aria-label="Timer List">
-
-
-
- >
- ) : (
- /* Add Button */
-
add(isMinutes ? 1 : 10)} aria-label="Add one" currentTheme={currentTheme}>
-
-
- {isMinutes ? 1 : 10}
-
-
- )}
-
- {/* Stop/Start Button */}
-
- {isRunning ? : }
-
-
- {isInitialized ? (
- <>
- {/* Settings Button */}
-
(window.location.hash = 'settings')} aria-label="Settings">
-
-
-
- >
- ) : (
- /* Reset Button */
-
-
-
- )}
-
- );
+ {/* Right Action: Settings / Reset Button */}
+
+ {isInitialized ? (
+ (window.location.hash = 'settings')}
+ aria-label="Settings"
+ title="Settings"
+ >
+
+
+ ) : (
+
+
+
+ )}
+
+
+ );
};
export default ControlButtons;
diff --git a/src/components/timers/shared/controls/HomeButton.tsx b/src/components/timers/shared/controls/HomeButton.tsx
index c7a0253..70e05ae 100644
--- a/src/components/timers/shared/controls/HomeButton.tsx
+++ b/src/components/timers/shared/controls/HomeButton.tsx
@@ -1,9 +1,22 @@
+import React from 'react';
import { HiMiniHome } from 'react-icons/hi2';
+import Button from '../../../common/Button';
-const HomeButton = ({ isVisible, onClick }: { isVisible: boolean; onClick: () => void }) => (
-
-
-
-);
+type HomeButtonProps = {
+ isVisible: boolean;
+ onClick: () => void;
+};
+
+const HomeButton: React.FC = ({ isVisible, onClick }) => {
+ if (!isVisible) {
+ return
;
+ }
+
+ return (
+
+
+
+ );
+};
export default HomeButton;
diff --git a/src/components/timers/timer-management/TimerItemOverlay.tsx b/src/components/timers/timer-management/TimerItemOverlay.tsx
index a442e1b..2dfcef9 100644
--- a/src/components/timers/timer-management/TimerItemOverlay.tsx
+++ b/src/components/timers/timer-management/TimerItemOverlay.tsx
@@ -1,63 +1,83 @@
-import { useEffect, useState } from 'react';
+import React, { useEffect, useState } from 'react';
+import { IoMdClose } from 'react-icons/io';
import { TIMER_TYPE, TIMER_TYPE_CONFIG, TimerType } from '../../../config/timer/type';
import { useOverlay } from '../../../hooks/useOverlay';
import { useThemeStore } from '../../../store/themeStore';
import { BaseTimerData, RoutineTimerData, TimerData } from '../../../store/types/timer';
-import TopBar from '../../common/TopBar';
import BaseTimerForm from './forms/BaseTimerForm';
import RoutineTimerForm from './forms/RoutineTimerForm';
type TimerItemOverlayProps = {
- initialTimerData: TimerData | null;
- mode: 'add' | 'edit';
- onClose: () => void;
+ initialTimerData: TimerData | null;
+ mode: 'add' | 'edit';
+ onClose: () => void;
};
const TimerItemOverlay: React.FC = ({ initialTimerData, mode, onClose }) => {
- const { selectedTheme } = useThemeStore();
- const [timerType, setTimerType] = useState(initialTimerData?.type || TIMER_TYPE.BASE);
+ const { selectedTheme, compColor } = useThemeStore();
+ const [timerType, setTimerType] = useState(initialTimerData?.type || TIMER_TYPE.BASE);
- const { isOpen, close } = useOverlay('timer-item', onClose);
+ const { isOpen, close } = useOverlay('timer-item', onClose);
- useEffect(() => {
- if (isOpen) {
- setTimerType(initialTimerData?.type || TIMER_TYPE.BASE);
- }
- }, [isOpen]);
+ useEffect(() => {
+ if (isOpen) {
+ setTimerType(initialTimerData?.type || TIMER_TYPE.BASE);
+ }
+ }, [isOpen, initialTimerData]);
- if (!isOpen) return null;
+ if (!isOpen) return null;
- return (
+ return (
+
+
+ {/* Header */}
-
+ {mode === 'add' ? 'Create New' : 'Edit'} {TIMER_TYPE_CONFIG[timerType].label}
+
+
+
+
+
+
+ {/* Content Body */}
+
+ {timerType === TIMER_TYPE.BASE && (
+
+ )}
- {timerType === TIMER_TYPE.BASE && (
-
- )}
-
- {timerType === TIMER_TYPE.ROUTINE && (
-
- )}
+ {timerType === TIMER_TYPE.ROUTINE && (
+
+ )}
- );
+
+
+ );
};
export default TimerItemOverlay;
diff --git a/src/components/timers/timer-management/TimerListOverlay.tsx b/src/components/timers/timer-management/TimerListOverlay.tsx
index 72919fe..11e81bc 100644
--- a/src/components/timers/timer-management/TimerListOverlay.tsx
+++ b/src/components/timers/timer-management/TimerListOverlay.tsx
@@ -1,145 +1,418 @@
-import { useState } from 'react';
-import { IoMdAdd } from 'react-icons/io';
-import { MdDeleteOutline, MdEdit } from 'react-icons/md';
+import React, { useState } from 'react';
+import { IoMdAdd, IoMdClose, IoMdFlash, IoMdSearch } from 'react-icons/io';
+import { MdDeleteOutline, MdEdit, MdHourglassTop, MdPlayArrow } from 'react-icons/md';
+import { getCardSurface, getTextColor } from '../../../utils/colorUtils';
import { TIMER_TYPE, TIMER_TYPE_CONFIG } from '../../../config/timer/type';
import { useOverlay } from '../../../hooks/useOverlay';
import { useBaseTimerStore } from '../../../store/baseTimerStore';
import { useRoutineTimerStore } from '../../../store/routineTimerStore';
import { useSelectedTimerStore } from '../../../store/selectedTimerStore';
import { useThemeStore } from '../../../store/themeStore';
-import { TimerData } from '../../../store/types/timer';
+import { BaseTimerData, RoutineTimerData, TimerData } from '../../../store/types/timer';
import { getTimerPointColor } from '../../../utils/themeUtils';
import Button from '../../common/Button';
-import TopBar from '../../common/TopBar';
import TimerItemOverlay from './TimerItemOverlay';
+const QUICK_TEMPLATES: {
+ title: string;
+ desc: string;
+ badge: string;
+ badgeColor: string;
+ create: (pointColorIdx: number) => TimerData;
+}[] = [
+ {
+ title: '🍅 Pomodoro 25/5',
+ desc: '4x (25m Focus + 5m Break) + 20m Long Rest',
+ badge: 'Pomodoro',
+ badgeColor: 'bg-red-500/15 text-red-700 dark:text-red-300',
+ create: (idx) => ({
+ id: `routine_pomodoro_${Date.now()}`,
+ type: TIMER_TYPE.ROUTINE,
+ title: '🍅 Classic Pomodoro',
+ pointColorIndex: idx,
+ items: [
+ { id: `p1_${Date.now()}`, type: TIMER_TYPE.BASE, title: 'Focus 1', time: 25, isMinutes: true, pointColorIndex: 7, interval: 5 },
+ { id: `b1_${Date.now()}`, type: TIMER_TYPE.BASE, title: 'Short Break', time: 5, isMinutes: true, pointColorIndex: 3, interval: 5 },
+ { id: `p2_${Date.now()}`, type: TIMER_TYPE.BASE, title: 'Focus 2', time: 25, isMinutes: true, pointColorIndex: 7, interval: 5 },
+ { id: `b2_${Date.now()}`, type: TIMER_TYPE.BASE, title: 'Short Break', time: 5, isMinutes: true, pointColorIndex: 3, interval: 5 },
+ { id: `p3_${Date.now()}`, type: TIMER_TYPE.BASE, title: 'Focus 3', time: 25, isMinutes: true, pointColorIndex: 7, interval: 5 },
+ { id: `lb_${Date.now()}`, type: TIMER_TYPE.BASE, title: 'Long Break', time: 20, isMinutes: true, pointColorIndex: 2, interval: 5 },
+ ],
+ }),
+ },
+ {
+ title: '⚡ Deep Focus 50/10',
+ desc: '50m Deep Work + 10m Refresh Rest',
+ badge: 'Deep Work',
+ badgeColor: 'bg-blue-500/15 text-blue-700 dark:text-blue-300',
+ create: (idx) => ({
+ id: `routine_deep_${Date.now()}`,
+ type: TIMER_TYPE.ROUTINE,
+ title: '⚡ Deep Work Session',
+ pointColorIndex: idx,
+ items: [
+ { id: `dw1_${Date.now()}`, type: TIMER_TYPE.BASE, title: 'Deep Focus', time: 50, isMinutes: true, pointColorIndex: 4, interval: 5 },
+ { id: `dwb_${Date.now()}`, type: TIMER_TYPE.BASE, title: 'Rest & Stretch', time: 10, isMinutes: true, pointColorIndex: 1, interval: 5 },
+ ],
+ }),
+ },
+ {
+ title: '💪 HIIT Workout 45/15',
+ desc: '4 rounds of 45s Exercise + 15s Rest',
+ badge: 'Workout',
+ badgeColor: 'bg-amber-500/15 text-amber-700 dark:text-amber-300',
+ create: (idx) => ({
+ id: `routine_hiit_${Date.now()}`,
+ type: TIMER_TYPE.ROUTINE,
+ title: '💪 HIIT Workout',
+ pointColorIndex: idx,
+ items: Array.from({ length: 4 }, (_, i) => [
+ { id: `ex_${i}_${Date.now()}`, type: TIMER_TYPE.BASE, title: `Exercise Round ${i + 1}`, time: 45, isMinutes: false, pointColorIndex: 7, interval: 3 },
+ { id: `rst_${i}_${Date.now()}`, type: TIMER_TYPE.BASE, title: `Rest`, time: 15, isMinutes: false, pointColorIndex: 3, interval: 3 },
+ ]).flat(),
+ }),
+ },
+ {
+ title: '☕ Power Nap 15m',
+ desc: '15 minutes quick recharge',
+ badge: 'Single',
+ badgeColor: 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-300',
+ create: (idx) => ({
+ id: `base_nap_${Date.now()}`,
+ type: TIMER_TYPE.BASE,
+ title: '☕ Power Nap',
+ time: 15,
+ isMinutes: true,
+ pointColorIndex: idx,
+ }),
+ },
+];
+
const TimerListOverlay: React.FC = () => {
- const { selectedTheme } = useThemeStore();
+ const { selectedTheme, compColor } = useThemeStore();
+ const selectTimer = useSelectedTimerStore((state) => state.selectTimer);
+ const { timers: baseTimers, addTimer: addBaseTimer, removeTimer: removeBaseTimer } = useBaseTimerStore();
+ const { timers: routineTimers, addTimer: addRoutineTimer, removeTimer: removeRoutineTimer } = useRoutineTimerStore();
- const selectTimer = useSelectedTimerStore((state) => state.selectTimer);
- const { timers: baseTimers, removeTimer: removeBaseTimer } = useBaseTimerStore();
- const { timers: routineTimers, removeTimer: removeRoutineTimer } = useRoutineTimerStore();
- const [targetTimer, setTargetTimer] = useState(null);
- const [mode, setMode] = useState<'add' | 'edit'>('add');
+ const [targetTimer, setTargetTimer] = useState(null);
+ const [mode, setMode] = useState<'add' | 'edit'>('add');
+ const [activeTab, setActiveTab] = useState<'all' | 'base' | 'routine'>('all');
+ const [searchQuery, setSearchQuery] = useState('');
- const { isOpen, close } = useOverlay('timer-list');
+ const { isOpen, close } = useOverlay('timer-list');
- const timers = [...baseTimers, ...routineTimers];
+ const timers = [...baseTimers, ...routineTimers];
+ const filteredTimers = timers
+ .filter((t) => {
+ if (activeTab === 'base') return t.type === TIMER_TYPE.BASE;
+ if (activeTab === 'routine') return t.type === TIMER_TYPE.ROUTINE;
+ return true;
+ })
+ .filter((t) => {
+ if (!searchQuery.trim()) return true;
+ return t.title.toLowerCase().includes(searchQuery.toLowerCase());
+ });
- const handleSelectTimer = (timerId: string) => {
- selectTimer(timerId);
- };
+ const handleSelectTimer = (timerId: string) => {
+ selectTimer(timerId);
+ close();
+ };
- const openOverlay = (timer?: TimerData) => {
- setTargetTimer(timer || null);
- setMode(timer ? 'edit' : 'add');
- window.location.hash = 'timer-list&timer-item';
- };
+ const openOverlay = (timer?: TimerData) => {
+ setTargetTimer(timer || null);
+ setMode(timer ? 'edit' : 'add');
+ window.location.hash = 'timer-list&timer-item';
+ };
+
+ const closeOverlay = () => {
+ setTargetTimer(null);
+ };
- const closeOverlay = () => {
- setTargetTimer(null);
+ const getTimerIcon = (timer: TimerData) => {
+ const config = TIMER_TYPE_CONFIG[timer.type];
+ const Icon = config.icon;
+
+ const commonProps = {
+ size: 46,
+ className: 'rounded-full shrink-0 drop-shadow-sm',
+ stroke: getTimerPointColor(selectedTheme, timer.pointColorIndex),
};
- const getTimerIcon = (timer: TimerData) => {
- const config = TIMER_TYPE_CONFIG[timer.type];
- const Icon = config.icon;
+ if (timer.type === TIMER_TYPE.BASE) {
+ return ;
+ }
- const commonProps = {
- size: 50,
- className: 'rounded-full',
- stroke: getTimerPointColor(selectedTheme, timer.pointColorIndex),
- };
+ return ;
+ };
- if (timer.type === TIMER_TYPE.BASE) {
- return ;
- }
+ const handleEditTimer = (e: React.MouseEvent, timer: TimerData) => {
+ e.stopPropagation();
+ openOverlay(timer);
+ };
- return ;
- };
+ const handleDeleteTimer = (e: React.MouseEvent, timer: TimerData) => {
+ e.stopPropagation();
+ if (timer.type === TIMER_TYPE.BASE) {
+ removeBaseTimer(timer.id);
+ } else {
+ removeRoutineTimer(timer.id);
+ }
+ };
- const handleEditTimer = (e: React.MouseEvent, timer: TimerData) => {
- e.stopPropagation();
- openOverlay(timer);
- };
+ const handleAddTemplate = (tpl: (typeof QUICK_TEMPLATES)[0]) => {
+ const newTimer = tpl.create(4);
+ if (newTimer.type === TIMER_TYPE.BASE) {
+ addBaseTimer(newTimer as BaseTimerData);
+ } else {
+ addRoutineTimer(newTimer as RoutineTimerData);
+ }
+ handleSelectTimer(newTimer.id);
+ };
- const handleDeleteTimer = (e: React.MouseEvent, timer: TimerData) => {
- e.stopPropagation();
- if (timer.type === TIMER_TYPE.BASE) {
- removeBaseTimer(timer.id);
- } else {
- removeRoutineTimer(timer.id);
- }
- };
+ if (!isOpen) return null;
- if (!isOpen) return null;
+ const { cardBg, cardBorder } = getCardSurface(
+ selectedTheme.color.main,
+ selectedTheme.color.sub,
+ selectedTheme.color.point
+ );
+ const isDark = getTextColor(selectedTheme.color.main) === 'white';
- return (
- <>
-
-
-
-
-
-
-
openOverlay()}
- aria-label="Add Timer"
- className="h-10 w-full rounded-2xl"
+ return (
+ <>
+ {/* Responsive Backdrop (Desktop: Centered Modal, Mobile: Fullscreen) */}
+
+ {/* Main Dialog Window */}
+
+ {/* Header */}
+
+
+
Timer Presets & Routines
+
+
+
+
+
+
+
+
+ {/* Search & Filter Bar */}
+
+
+ {/* Category Segmented Control */}
+
+ {(['all', 'base', 'routine'] as const).map((tab) => (
+ setActiveTab(tab)}
+ className="py-2 px-3 sm:px-4 text-xs sm:text-sm rounded-xl transition-all text-center"
+ style={{
+ backgroundColor: activeTab === tab ? 'rgba(255,255,255,0.95)' : 'transparent',
+ color: activeTab === tab ? selectedTheme.color.point : compColor,
+ fontWeight: activeTab === tab ? 700 : 500,
+ opacity: activeTab === tab ? 1 : 0.65,
+ boxShadow: activeTab === tab ? '0 1px 4px rgba(0,0,0,0.12)' : 'none',
+ }}
+ >
+ {tab === 'all' ? 'All' : tab === 'base' ? 'Single' : 'Routines'}
+
+ ))}
+
+
+ {/* Search Bar & Mobile Actions */}
+
+
+
+ setSearchQuery(e.target.value)}
+ className="w-full pl-10 pr-4 py-2 text-sm rounded-xl border focus:outline-none focus:ring-2"
+ style={{
+ backgroundColor: '#FFFFFF',
+ borderColor: 'rgba(0,0,0,0.09)',
+ color: '#1A1A1A',
+ }}
+ />
+
+
+ {/* Desktop Create Button */}
+
openOverlay()}
+ className="hidden sm:flex items-center justify-center gap-2 px-4 py-2 rounded-xl text-sm font-bold text-white shadow-soft transition-transform active:scale-95 shrink-0"
+ style={{ backgroundColor: selectedTheme.color.point }}
+ >
+
+ New
+
+
+
+
+
+ {/* Scrollable Gallery Content */}
+
+ {/* Quick 1-Click Templates */}
+ {!searchQuery && (
+
+
+
+ Popular Preset Templates
+
+
+ {QUICK_TEMPLATES.map((tpl, i) => (
+
handleAddTemplate(tpl)}
+ className="btn-tactile group text-left p-4 rounded-2xl border shadow-soft hover:shadow-dial transition-all flex flex-col justify-between"
+ style={{ backgroundColor: '#FFFFFF', borderColor: 'rgba(0,0,0,0.09)', color: '#1A1A1A' }}
>
-
-
+
+
+ {tpl.badge}
+
+
{tpl.title}
+
{tpl.desc}
+
+
+ Use Template
+
+
+ ))}
+
+ )}
+
+ {/* User Timers Grid */}
+
+
+
+ Your Timers ({filteredTimers.length})
+
+
+
+ {filteredTimers.length === 0 ? (
+
+
+
No timers found
+
+ {searchQuery
+ ? 'No results match your search query.'
+ : 'Create your first custom basic timer or routine timer to get started.'}
+
+
openOverlay()}
+ className="mt-2 inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-bold text-white shadow-soft"
+ style={{ backgroundColor: selectedTheme.color.point }}
+ >
+ Create Timer
+
+
+ ) : (
+
+ {filteredTimers.map((timer: TimerData) => (
+
handleSelectTimer(timer.id)}
+ className="btn-tactile group cursor-pointer p-4 rounded-2xl border shadow-soft hover:shadow-dial transition-all flex flex-col justify-between gap-3"
+ style={{ backgroundColor: '#FFFFFF', borderColor: 'rgba(0,0,0,0.09)', color: '#1A1A1A' }}
+ >
+
+
{getTimerIcon(timer)}
+
+
+ {timer.title || 'Untitled Timer'}
+
+
+
+ {timer.type === TIMER_TYPE.BASE
+ ? `${timer.time} ${timer.isMinutes ? 'min' : 'sec'}`
+ : `${timer.items.length} steps (${timer.items
+ .reduce(
+ (acc, it) => acc + (it.isMinutes ? it.time : Math.round(it.time / 60)),
+ 0
+ )}m total)`}
+
+
+
+ {timer.type}
+
+
+
+
+ {/* Card Action Footer */}
+
+
+ Start Timer
+
+
e.stopPropagation()}>
+ handleEditTimer(e, timer)}
+ aria-label="Edit Timer"
+ className="p-2 rounded-xl transition-colors opacity-70 hover:opacity-100 border"
+ style={{ backgroundColor: '#FFFFFF', borderColor: 'rgba(0,0,0,0.09)', color: '#1A1A1A' }}
+ >
+
+
+ handleDeleteTimer(e, timer)}
+ aria-label="Delete Timer"
+ className="p-2 rounded-xl hover:bg-red-500/20 text-red-500 transition-colors opacity-75 hover:opacity-100"
+ >
+
+
+
+
+
+ ))}
+
+ )}
+
+
+ {/* Mobile Bottom Bar */}
+
+ openOverlay()}
+ aria-label="Create Custom Timer"
+ className="h-12 w-full rounded-2xl font-bold text-base flex items-center justify-center gap-2 shadow-float"
+ >
+
+ Create New Timer
+
+
+
+
-
- >
- );
+
+ >
+ );
};
export default TimerListOverlay;
diff --git a/src/components/timers/timer-management/fields/TimerTypeSelector.tsx b/src/components/timers/timer-management/fields/TimerTypeSelector.tsx
index c079dfb..9b531c6 100644
--- a/src/components/timers/timer-management/fields/TimerTypeSelector.tsx
+++ b/src/components/timers/timer-management/fields/TimerTypeSelector.tsx
@@ -1,5 +1,6 @@
import { NUM_TIMER_TYPES, TIMER_TYPE_CONFIG, TimerType } from '../../../../config/timer/type';
import { useThemeStore } from '../../../../store/themeStore';
+import { getTextColor } from '../../../../utils/colorUtils';
import Tooltip from '../../../common/Tooltip';
type TimerTypeSelectorProps = {
@@ -8,24 +9,28 @@ type TimerTypeSelectorProps = {
};
const TimerTypeSelector: React.FC = ({ selectedType, onTypeSelect }) => {
- const { selectedTheme } = useThemeStore();
+ const { selectedTheme, compColor } = useThemeStore();
+ const isDark = getTextColor(selectedTheme.color.main) === 'white';
return (
{Object.entries(TIMER_TYPE_CONFIG).map(([type, config]) => (
onTypeSelect(type as TimerType)}
- className={`flex cursor-pointer flex-col items-center gap-2 rounded-lg border-2 p-4 ${
- selectedType === type ? '' : 'border-transparent hover:scale-105'
- }`}
+ className="flex cursor-pointer flex-col items-center gap-2 rounded-xl p-4 transition-all"
style={{
- borderColor: selectedType === type ? selectedTheme.color.point : undefined,
+ backgroundColor: selectedType === type ? 'rgba(255,255,255,0.95)' : 'transparent',
+ color: selectedType === type ? selectedTheme.color.point : compColor,
+ fontWeight: selectedType === type ? 700 : 500,
+ opacity: selectedType === type ? 1 : 0.65,
+ boxShadow: selectedType === type ? '0 1px 4px rgba(0,0,0,0.12)' : 'none',
}}
>
diff --git a/src/components/timers/timer-management/forms/BaseTimerForm.tsx b/src/components/timers/timer-management/forms/BaseTimerForm.tsx
index e3fc7be..f0a000e 100644
--- a/src/components/timers/timer-management/forms/BaseTimerForm.tsx
+++ b/src/components/timers/timer-management/forms/BaseTimerForm.tsx
@@ -1,3 +1,4 @@
+import React from 'react';
import { useForm } from 'react-hook-form';
import { IoMdCheckmark } from 'react-icons/io';
import { MdOutlinePalette, MdOutlineTimer, MdTextFields } from 'react-icons/md';
@@ -7,7 +8,6 @@ import { useBaseTimerStore } from '../../../../store/baseTimerStore';
import { useThemeStore } from '../../../../store/themeStore';
import { BaseTimerData } from '../../../../store/types/timer';
import Button from '../../../common/Button';
-import TimeDisplay from '../../shared/displays/TimeDisplay';
import PointColorSelector from '../fields/PointColorSelector';
import TimerTypeSelector from '../fields/TimerTypeSelector';
import TimeSelector from '../fields/TimeSelector';
@@ -15,116 +15,170 @@ import TimeSelector from '../fields/TimeSelector';
type BaseTimerFormData = Omit
;
type BaseTimerFormProps = {
- initialData?: BaseTimerData | null;
- mode: 'add' | 'edit';
- timerType: TimerType;
- setTimerType: React.Dispatch>;
- close: () => void;
+ initialData?: BaseTimerData | null;
+ mode: 'add' | 'edit';
+ timerType: TimerType;
+ setTimerType: React.Dispatch>;
+ close: () => void;
};
const BaseTimerForm: React.FC = ({ initialData, mode, timerType, setTimerType, close }) => {
- const { selectedTheme } = useThemeStore();
- const { defaultPointColorIndex } = useTheme();
- const { addTimer, updateTimer } = useBaseTimerStore();
-
- const { register, handleSubmit, watch, setValue } = useForm({
- defaultValues: {
- title: initialData?.title || '',
- pointColorIndex: initialData?.pointColorIndex ?? defaultPointColorIndex,
- isMinutes: initialData?.isMinutes || false,
- time: initialData?.time || 5,
- },
- });
- const { title, pointColorIndex, time, isMinutes } = watch();
- const { selectedThemeCopy } = useTheme(pointColorIndex);
-
- const onSubmit = (data: BaseTimerFormData) => {
- const timerData: BaseTimerData = {
- ...data,
- id: initialData?.id || crypto.randomUUID(),
- title: data.title?.trim() || `Timer-${data.time}`,
- type: TIMER_TYPE.BASE,
- };
-
- if (mode === 'add') {
- addTimer(timerData);
- } else {
- updateTimer(timerData.id, timerData);
- }
- close();
+ const { selectedTheme } = useThemeStore();
+ const { defaultPointColorIndex } = useTheme();
+ const { addTimer, updateTimer } = useBaseTimerStore();
+
+ const { register, handleSubmit, watch, setValue } = useForm({
+ defaultValues: {
+ title: initialData?.title || '',
+ pointColorIndex: initialData?.pointColorIndex ?? defaultPointColorIndex,
+ isMinutes: initialData?.isMinutes ?? true,
+ time: initialData?.time || 15,
+ },
+ });
+
+ const { title, pointColorIndex, time, isMinutes } = watch();
+ const { selectedThemeCopy } = useTheme(pointColorIndex);
+
+ const onSubmit = (data: BaseTimerFormData) => {
+ const timerData: BaseTimerData = {
+ ...data,
+ id: initialData?.id || `base_${Date.now()}_${Math.random().toString(36).substring(2, 6)}`,
+ title: data.title?.trim() || `${data.time} ${data.isMinutes ? 'Min' : 'Sec'} Timer`,
+ type: TIMER_TYPE.BASE,
};
- return (
-