Skip to content

Commit

Permalink
added timer main functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
ginny100 committed Dec 27, 2024
1 parent 4f237f6 commit f0d9d4e
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 13 deletions.
18 changes: 18 additions & 0 deletions packages/storage/lib/impl/zenStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ interface ZenSettings {
focusMinutes: number;
breakMinutes: number;
blockedApps: string[];
timerActive: boolean;
currentSession: number;
timerState: 'focus' | 'break';
timeLeft: number;
lastTimestamp: number;
}

type ZenStorage = BaseStorage<ZenSettings> & {
Expand All @@ -15,13 +20,19 @@ type ZenStorage = BaseStorage<ZenSettings> & {
updateBreakMinutes: (minutes: number) => Promise<void>;
addBlockedApp: (appName: string) => Promise<void>;
removeBlockedApp: (appName: string) => Promise<void>;
updateTimerState: (timerState: Partial<Pick<ZenSettings, 'timerActive' | 'currentSession' | 'timerState' | 'timeLeft'>>) => Promise<void>;
};

const defaultSettings: ZenSettings = {
sessions: 0,
focusMinutes: 0,
breakMinutes: 0,
blockedApps: [],
timerActive: false,
currentSession: 1,
timerState: 'focus',
timeLeft: 0,
lastTimestamp: 0,
};

const storage = createStorage<ZenSettings>('zen-storage-key', defaultSettings, {
Expand Down Expand Up @@ -61,4 +72,11 @@ export const zenStorage: ZenStorage = {
blockedApps: current.blockedApps.filter(app => app !== appName),
}));
},
updateTimerState: async (timerState: Partial<Pick<ZenSettings, 'timerActive' | 'currentSession' | 'timerState' | 'timeLeft'>>) => {
await storage.set(current => ({
...current,
...timerState,
lastTimestamp: Date.now(),
}));
},
};
24 changes: 20 additions & 4 deletions pages/popup/src/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,16 @@ const Popup = () => {
setBlockedApps(blockedApps.filter(app => app !== appToRemove));
};

if (isTimerView) {
return <Timer onBack={() => setIsTimerView(false)} />;
// Show timer view if timer is active
if (zenSettings.timerActive || isTimerView) {
return (
<Timer
onBack={() => setIsTimerView(false)}
sessions={sessions}
focusMinutes={focusMinutes}
breakMinutes={breakMinutes}
/>
);
}

return (
Expand Down Expand Up @@ -197,10 +205,18 @@ const Popup = () => {
</div>
</div>

{/* Start Button - Matching position with Back button */}
{/* Start Button */}
<div className="mt-4 flex justify-center">
<button
onClick={() => setIsTimerView(true)}
onClick={() => {
zenStorage.updateTimerState({
timerActive: true,
currentSession: 1,
timerState: 'focus',
timeLeft: focusMinutes * 60
});
setIsTimerView(true);
}}
className={`rounded-full px-8 py-2 text-xl font-bold shadow-lg shadow-black/20 transition-colors ${
isLight
? 'bg-[#39A2DB] hover:bg-[#769FCD] text-[#1E1E1E]'
Expand Down
128 changes: 119 additions & 9 deletions pages/popup/src/Timer.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,110 @@
import { useStorage } from '@extension/shared';
import { themeStorage } from '@extension/storage';
import ThemeSwitcher from './components/ThemeSwitcher';
import { useEffect, useState } from 'react';
import { zenStorage } from '@extension/storage';

interface TimerProps {
onBack: () => void;
sessions: number;
focusMinutes: number;
breakMinutes: number;
}

const Timer = ({ onBack }: TimerProps) => {
type TimerState = 'focus' | 'break';

const Timer = ({ onBack, sessions, focusMinutes, breakMinutes }: TimerProps) => {
const theme = useStorage(themeStorage);
const zenSettings = useStorage(zenStorage);
const isLight = theme === 'light';

// Initialize state from storage or props
const [currentSession, setCurrentSession] = useState(() =>
zenSettings.timerActive ? zenSettings.currentSession : 1
);
const [timerState, setTimerState] = useState<TimerState>(() =>
zenSettings.timerActive ? zenSettings.timerState : 'focus'
);
const [timeLeft, setTimeLeft] = useState(() => {
if (!zenSettings.timerActive) return focusMinutes * 60;

// Calculate elapsed time since last update
const elapsedSeconds = Math.floor((Date.now() - zenSettings.lastTimestamp) / 1000);
return Math.max(0, zenSettings.timeLeft - elapsedSeconds);
});
const [isRunning, setIsRunning] = useState(zenSettings.timerActive);

// Persist timer state changes
useEffect(() => {
zenStorage.updateTimerState({
timerActive: isRunning,
currentSession,
timerState,
timeLeft,
});
}, [isRunning, currentSession, timerState, timeLeft]);

// Timer logic
useEffect(() => {
if (!isRunning) return;

const timer = setInterval(() => {
setTimeLeft(prev => {
if (prev <= 1) {
if (timerState === 'focus') {
if (currentSession < sessions) {
setTimerState('break');
return breakMinutes * 60;
} else {
setIsRunning(false);
return 0;
}
} else {
setTimerState('focus');
setCurrentSession(prev => prev + 1);
return focusMinutes * 60;
}
}
return prev - 1;
});
}, 1000);

return () => clearInterval(timer);
}, [isRunning, timerState, currentSession, sessions, focusMinutes, breakMinutes]);

// Calculate progress for circle animation (0 to 360 degrees)
const totalSeconds = timerState === 'focus' ? focusMinutes * 60 : breakMinutes * 60;
const progress = 360 - ((timeLeft / totalSeconds) * 360); // Inverted back to show elapsed time

// Format time as MM:SS
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};

const handleBackClick = () => {
if (isRunning) {
if (window.confirm('Timer is still running. Are you sure you want to go back?')) {
zenStorage.updateTimerState({
timerActive: false,
currentSession: 1,
timerState: 'focus',
timeLeft: 0
});
onBack();
}
} else {
zenStorage.updateTimerState({
timerActive: false,
currentSession: 1,
timerState: 'focus',
timeLeft: 0
});
onBack();
}
};

return (
<div className={`App size-full overflow-hidden p-2 transition-colors ${
isLight ? 'bg-[#CDE8F6]' : 'bg-[#364E68]'
Expand All @@ -29,27 +124,42 @@ const Timer = ({ onBack }: TimerProps) => {

{/* Timer Circle */}
<div className="my-9 relative flex size-64 items-center justify-center">
<div className={`absolute size-full rounded-full border-[12px] ${
isLight ? 'border-[#769FCD]' : 'border-[#B9D7EA]'
}`} />
{/* Background Circle - White */}
<div className="absolute size-full rounded-full border-[12px] border-white" />

{/* Progress Circle - Shows elapsed time in blue */}
<div
className={`absolute size-full rounded-full border-[12px] transition-all ${
isLight ? 'border-[#769FCD]' : 'border-[#B9D7EA]'
}`}
style={{
clipPath: `polygon(50% 50%, 50% 0%, ${progress <= 180
? `${50 + 50 * Math.sin(progress * Math.PI / 180)}% ${50 - 50 * Math.cos(progress * Math.PI / 180)}%`
: '100% 0%'}, ${progress > 180
? `100% 100%, ${50 + 50 * Math.sin((progress - 180) * Math.PI / 180)}% ${50 + 50 * Math.cos((progress - 180) * Math.PI / 180)}%`
: ''})`
}}
/>
{/* Timer Display */}
<div className={`font-['Inria_Sans'] text-7xl font-normal ${
isLight ? 'text-[#1E1E1E]' : 'text-[#F8FAFC]'
}`}>
00:00
{formatTime(timeLeft)}
</div>
</div>

{/* Lotus Icons */}
<div className="flex gap-4">
{Array(6).fill('🪷').map((lotus, index) => (
{Array(sessions).fill('🪷').map((lotus, index) => (
<span
key={index}
role="img"
aria-label="lotus"
className="text-4xl"
style={{
filter: 'saturate(1.5) brightness(1.1)',
transform: 'scale(1.2)'
transform: 'scale(1.2)',
opacity: index < currentSession ? 0.4 : 1
}}
>
{lotus}
Expand All @@ -59,10 +169,10 @@ const Timer = ({ onBack }: TimerProps) => {
</div>
</div>

{/* Back Button - Outside the white box */}
{/* Back Button */}
<div className="mt-4 flex justify-center">
<button
onClick={onBack}
onClick={handleBackClick}
className={`rounded-full px-8 py-2 text-xl font-bold shadow-lg shadow-black/20 transition-colors ${
isLight
? 'bg-[#39A2DB] hover:bg-[#769FCD] text-[#1E1E1E]'
Expand Down

0 comments on commit f0d9d4e

Please sign in to comment.