Skip to content

Commit

Permalink
refactor: replace context with zustand store
Browse files Browse the repository at this point in the history
  • Loading branch information
BrunoAseff authored Jan 18, 2025
1 parent c975d04 commit bf10c32
Show file tree
Hide file tree
Showing 11 changed files with 451 additions and 85 deletions.
32 changes: 31 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@
"tailwindcss-animate": "^1.0.7",
"typescript-eslint": "8.12.2",
"uploadthing": "^7.3.0",
"zod": "^3.23.3"
"zod": "^3.23.3",
"zustand": "^5.0.3"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",
Expand Down
5 changes: 1 addition & 4 deletions src/components/features/Space.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import BreathingExercise from "./breathingExercise/BreathingExercise";
import Reminder from "./Reminder";
import SyncingInfo from "../syncingInfo";
import { SpaceSidebarMobile } from "../sidebar/SpaceSidebarMobile";
import { CyclesContextProvider } from "@/contexts/cycleContext";
import { useSession } from "next-auth/react";
import { AutoSaveProvider } from "../autoSaveProvider";
import FullscreenButton from "../FullScreenButton";
Expand Down Expand Up @@ -206,9 +205,7 @@ export default function Space() {
</div>
<div className="relative z-10">
<Clock {...space.clock} />
<CyclesContextProvider>
<Pomodoro {...space.pomodoro} />
</CyclesContextProvider>
<Pomodoro {...space.pomodoro} />

<Quote {...space.quote} />
<BreathingExercise {...space.breathingExercise} />
Expand Down
75 changes: 42 additions & 33 deletions src/components/features/pomodoro/Countdown.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { CyclesContext } from "@/contexts/cycleContext";
import { useSpacesContext } from "@/contexts/spaceContext";
import { useCallback, useContext, useEffect, useRef, memo } from "react";
import { useCycleStore } from "@/stores/useCycleStore";
import { useCallback, useEffect, useRef, memo } from "react";

// Separate the display component to prevent unnecessary re-renders
// Separate the display component remains the same
const TimeDisplay = memo(function TimeDisplay({
minutes,
seconds,
Expand All @@ -22,29 +22,26 @@ const TimeDisplay = memo(function TimeDisplay({
});

export function Countdown() {
// Split context into separate variables to prevent unnecessary re-renders
const {
activeCycle,
currentTab,
isPaused,
setSecondsPassed,
amountSecondsPassed,
toggleTab,
togglePause,
} = useContext(CyclesContext);
const activeCycle = useCycleStore((state) => state.activeCycle);
const currentTab = useCycleStore((state) => state.currentTab);
const isPaused = useCycleStore((state) => state.isPaused);
const setSecondsPassed = useCycleStore((state) => state.setSecondsPassed);
const amountSecondsPassed = useCycleStore(
(state) => state.amountSecondsPassed,
);
const toggleTab = useCycleStore((state) => state.toggleTab);
const togglePause = useCycleStore((state) => state.togglePause);

const { spaces, selectedTab, playPomodoroAlarm } = useSpacesContext();

const intervalRef = useRef<number | null>(null);
const lastTickRef = useRef<number>(Date.now());

// Memoize space settings
const currentSpace = spaces.find((space) => space.id === selectedTab);
const shortBreakDuration = currentSpace?.pomodoro.shortBreakDuration ?? 5;
const longBreakDuration = currentSpace?.pomodoro.longBreakDuration ?? 15;
const autoStart = currentSpace?.pomodoro.autoStart ?? false;

// Memoize total seconds calculation
const totalSeconds = useCallback(() => {
switch (currentTab) {
case "Focus":
Expand All @@ -61,17 +58,26 @@ export function Countdown() {
shortBreakDuration,
])();

// Memoize the timer update callback
const updateTimer = useCallback(() => {
const now = Date.now();
const deltaSeconds = Math.floor((now - lastTickRef.current) / 1000);
lastTickRef.current = now;

if (deltaSeconds > 0) {
setSecondsPassed((seconds: number) => {
const newTime = seconds + deltaSeconds;
if (newTime >= totalSeconds) {
clearInterval(intervalRef.current!);
const elapsedTime = now - lastTickRef.current;

// Only update if at least 1 second has passed
if (elapsedTime >= 1000) {
// Calculate how many whole seconds have passed
const deltaSeconds = Math.floor(elapsedTime / 1000);

// Update the last tick time by the exact number of seconds processed
lastTickRef.current = now - (elapsedTime % 1000);

setSecondsPassed((prev: number) => {
const newSeconds = prev + deltaSeconds;

if (newSeconds >= totalSeconds) {
// Clear interval and handle session completion
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
toggleTab();
playPomodoroAlarm();

Expand All @@ -80,25 +86,31 @@ export function Countdown() {
}
return 0;
}
return newTime;

return newSeconds;
});
}
}, [
totalSeconds,
setSecondsPassed,
toggleTab,
playPomodoroAlarm,
autoStart,
togglePause,
setSecondsPassed,
]);

useEffect(() => {
if (!isPaused) {
if (activeCycle && !isPaused) {
// Reset lastTickRef when starting or unpausing
lastTickRef.current = Date.now();
}

if (activeCycle && !isPaused) {
intervalRef.current = window.setInterval(updateTimer, 1000);
// Clear any existing interval first
if (intervalRef.current) {
clearInterval(intervalRef.current);
}

// Start new interval - checking more frequently for accuracy
intervalRef.current = window.setInterval(updateTimer, 100);
}

return () => {
Expand All @@ -108,20 +120,17 @@ export function Countdown() {
};
}, [activeCycle, isPaused, updateTimer]);

// Calculate time values only when needed
const currentSeconds = activeCycle ? totalSeconds - amountSecondsPassed : 0;
const minutesAmount = Math.floor(currentSeconds / 60);
const secondsAmount = currentSeconds % 60;
const minutes = String(minutesAmount).padStart(2, "0");
const seconds = String(secondsAmount).padStart(2, "0");

// Update document title
useEffect(() => {
if (activeCycle) {
document.title = `${minutes}:${seconds} - Nova`;
}
}, [minutes, seconds, activeCycle]);

// Only render TimeDisplay component with the values it needs
return <TimeDisplay minutes={minutes} seconds={seconds} />;
}
33 changes: 18 additions & 15 deletions src/components/features/pomodoro/FocusTimer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import { zodResolver } from "@hookform/resolvers/zod";
import * as zod from "zod";
import { NewCycleForm } from "./Form";
import { Countdown } from "./Countdown";
import { useContext, useRef } from "react";
import { useRef } from "react";
import { motion } from "framer-motion";
import { CyclesContext } from "@/contexts/cycleContext";
import { Stop } from "@/components/icons/Stop";
import { Play } from "@/components/icons/Play";
import IconBtn from "@/components/nova/buttons/IconBtn";
Expand All @@ -21,6 +20,7 @@ import { useSpacesContext } from "@/contexts/spaceContext";
import { Button } from "@/components/nova/buttons/Button";
import { Air } from "@/components/icons/Air";
import PictureInPictureButton from "./PictureInPictureButton";
import { useCycleStore } from "@/stores/useCycleStore";

const newCycleFormValidationSchema = zod.object({
task: zod.string().min(1, "Please enter the task"),
Expand All @@ -33,19 +33,21 @@ const newCycleFormValidationSchema = zod.object({
type NewCycleFormData = zod.infer<typeof newCycleFormValidationSchema>;

export default function FocusTimer() {
const {
activeCycle,
createNewCycle,
interruptCurrentCycle,
isPaused,
togglePause,
falsePause,
currentTab,
resetCurrentSession,
cycleCounter,
completedCycles,
skipCurrentSession,
} = useContext(CyclesContext);
const activeCycle = useCycleStore((state) => state.activeCycle);
const createNewCycle = useCycleStore((state) => state.createNewCycle);
const interruptCurrentCycle = useCycleStore(
(state) => state.interruptCurrentCycle,
);
const isPaused = useCycleStore((state) => state.isPaused);
const togglePause = useCycleStore((state) => state.togglePause);
const falsePause = useCycleStore((state) => state.falsePause);
const currentTab = useCycleStore((state) => state.currentTab);
const resetCurrentSession = useCycleStore(
(state) => state.resetCurrentSession,
);
const cycleCounter = useCycleStore((state) => state.cycleCounter);
const completedCycles = useCycleStore((state) => state.completedCycles);
const skipCurrentSession = useCycleStore((state) => state.skipCurrentSession);

const { spaces, selectedTab, stopPomodoroAlarm } = useSpacesContext();
const containerRef = useRef<HTMLFormElement>(null);
Expand All @@ -60,6 +62,7 @@ export default function FocusTimer() {
togglePause();
}
}

const newCycleForm = useForm<NewCycleFormData>({
resolver: zodResolver(newCycleFormValidationSchema),
defaultValues: {
Expand Down
10 changes: 7 additions & 3 deletions src/components/features/pomodoro/FocusingOnMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { CyclesContext } from "@/contexts/cycleContext";
import { useState, useRef, useContext } from "react";
import { useCycleStore } from "@/stores/useCycleStore";
import { useState, useRef } from "react";

export default function FocusingOnMessage() {
const { focusingOnMessage, setfocusingOnMessage } = useContext(CyclesContext);
const focusingOnMessage = useCycleStore((state) => state.focusingOnMessage);
const setfocusingOnMessage = useCycleStore(
(state) => state.setfocusingOnMessage,
);

const [isEditing, setIsEditing] = useState(false);
const [tempMessage, setTempMessage] = useState(focusingOnMessage);
const inputRef = useRef<HTMLInputElement>(null);
Expand Down
11 changes: 5 additions & 6 deletions src/components/features/pomodoro/Form.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import { useFormContext } from "react-hook-form";
import { useContext, useState, useEffect } from "react";
import { CyclesContext } from "@/contexts/cycleContext";
import { useState, useEffect } from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useCycleStore } from "@/stores/useCycleStore";

export function NewCycleForm() {
const { activeCycle } = useContext(CyclesContext);
// Replace context with store selector
const activeCycle = useCycleStore((state) => state.activeCycle);

const { register, setValue } = useFormContext();
// Input display state
const [minutesAmountDisplay, setMinutesAmountDisplay] = useState("25");

useEffect(() => {
// Set the initial value of `minutesAmount` in the form state
setValue("minutesAmount", parseInt(minutesAmountDisplay));
}, [minutesAmountDisplay, setValue]);

const handleMinutesAmountChange = (value: string) => {
// Allow empty string or numbers only
if (value === "" || /^\d{1,2}$/.test(value)) {
setMinutesAmountDisplay(value);
}
Expand Down
35 changes: 19 additions & 16 deletions src/components/features/pomodoro/InfoCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,25 @@ import {
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/HoverCard";
import { CyclesContext } from "@/contexts/cycleContext";
import { useContext, useEffect } from "react";
import { useEffect } from "react";
import { differenceInSeconds } from "date-fns";
import { useCycleStore } from "@/stores/useCycleStore";

export default function InfoCard() {
const {
cycleCounter,
completedCycles,
startTime,
focusedTimeStat,
breakTimeStat,
overallTimeStat,
updateOverallTimeStat,
activeCycle,
initialStartTime,
currentTab,
setFocusedTimeStat,
setBreakTimeStat,
} = useContext(CyclesContext);
const cycleCounter = useCycleStore((state) => state.cycleCounter);
const completedCycles = useCycleStore((state) => state.completedCycles);
const startTime = useCycleStore((state) => state.startTime);
const focusedTimeStat = useCycleStore((state) => state.focusedTimeStat);
const breakTimeStat = useCycleStore((state) => state.breakTimeStat);
const overallTimeStat = useCycleStore((state) => state.overallTimeStat);
const updateOverallTimeStat = useCycleStore(
(state) => state.updateOverallTimeStat,
);
const activeCycle = useCycleStore((state) => state.activeCycle);
const initialStartTime = useCycleStore((state) => state.initialStartTime);
const currentTab = useCycleStore((state) => state.currentTab);
const setFocusedTimeStat = useCycleStore((state) => state.setFocusedTimeStat);
const setBreakTimeStat = useCycleStore((state) => state.setBreakTimeStat);

useEffect(() => {
const interval = setInterval(() => {
Expand Down Expand Up @@ -70,6 +70,9 @@ export default function InfoCard() {
return `${remainingSeconds}s`;
};

console.log(cycleCounter);
console.log(completedCycles);

return (
<HoverCard openDelay={500}>
<HoverCardTrigger>
Expand Down
6 changes: 3 additions & 3 deletions src/components/features/pomodoro/PictureInPictureButton.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { useEffect, useRef, useState } from "react";
import IconBtn from "@/components/nova/buttons/IconBtn";
import { PictureInPicture } from "@phosphor-icons/react";
import { useContext } from "react";
import { CyclesContext } from "@/contexts/cycleContext";
import html2canvas from "html2canvas";
import { useCycleStore } from "@/stores/useCycleStore";

interface PictureInPictureButtonProps {
containerRef: React.RefObject<HTMLElement>;
Expand All @@ -15,7 +14,8 @@ export default function PictureInPictureButton({
const [isPiPActive, setIsPiPActive] = useState(false);
const videoRef = useRef<HTMLVideoElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const { activeCycle } = useContext(CyclesContext);

const activeCycle = useCycleStore((state) => state.activeCycle);
const pipContainerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
Expand Down
Loading

0 comments on commit bf10c32

Please sign in to comment.