Эх сурвалжийг харах

feat(profile): implement workout session fetching and state management using new service
feat(workout-session): create actions for fetching and syncing workout sessions
refactor(workout-session): replace local storage handling with service abstraction for better maintainability
chore(workout-session): remove outdated workout session service file to streamline codebase

Mathias 1 сар өмнө
parent
commit
491a6143ce

+ 14 - 2
app/[locale]/profile/page.tsx

@@ -1,17 +1,29 @@
 "use client";
+import { useEffect, useState } from "react";
 import { useRouter } from "next/navigation";
 
 import { useI18n } from "locales/client";
-import { workoutSessionLocal } from "@/shared/lib/workout-session/workout-session.local";
+import { useWorkoutSessionService } from "@/shared/lib/workout-session/use-workout-session.service";
 import { WorkoutSessionList } from "@/features/workout-session/ui/workout-session-list";
 import { WorkoutSessionHeatmap } from "@/features/workout-session/ui/workout-session-heatmap";
 import { Button } from "@/components/ui/button";
 
+import type { WorkoutSession } from "@/shared/lib/workout-session/types/workout-session";
+
 export default function ProfilePage() {
   const router = useRouter();
   const t = useI18n();
+  const [sessions, setSessions] = useState<WorkoutSession[]>([]);
+  const { getAll } = useWorkoutSessionService();
+
+  useEffect(() => {
+    const loadSessions = async () => {
+      const loadedSessions = await getAll();
+      setSessions(loadedSessions);
+    };
+    loadSessions();
+  }, []);
 
-  const sessions = typeof window !== "undefined" ? workoutSessionLocal.getAll() : [];
   const values: Record<string, number> = {};
   sessions.forEach((session) => {
     const date = session.startedAt.slice(0, 10);

+ 48 - 0
src/features/workout-session/actions/get-workout-sessions.action.ts

@@ -0,0 +1,48 @@
+"use server";
+
+import { z } from "zod";
+
+import { prisma } from "@/shared/lib/prisma";
+import { actionClient } from "@/shared/api/safe-actions";
+
+const getWorkoutSessionsSchema = z.object({
+  userId: z.string().optional(),
+});
+
+export const getWorkoutSessionsAction = actionClient.schema(getWorkoutSessionsSchema).action(async ({ parsedInput }) => {
+  try {
+    const { userId } = parsedInput;
+
+    if (!userId) {
+      return { serverError: "User ID is required" };
+    }
+
+    const sessions = await prisma.workoutSession.findMany({
+      where: { userId },
+      include: {
+        exercises: {
+          include: {
+            exercise: {
+              include: {
+                attributes: {
+                  include: {
+                    attributeName: true,
+                    attributeValue: true,
+                  },
+                },
+              },
+            },
+            sets: true,
+          },
+        },
+      },
+      orderBy: {
+        startedAt: "desc",
+      },
+    });
+    return { sessions };
+  } catch (error) {
+    console.error("Error fetching workout sessions:", error);
+    return { serverError: "Failed to fetch workout sessions" };
+  }
+});

+ 1 - 2
src/features/workout-session/actions/sync-workout-sessions.action.ts

@@ -41,7 +41,7 @@ export const syncWorkoutSessionAction = actionClient.schema(syncWorkoutSessionSc
   try {
     const { session } = parsedInput;
 
-    const { status, ...sessionData } = session;
+    const { status: _s, ...sessionData } = session;
 
     const result = await prisma.workoutSession.upsert({
       where: { id: session.id },
@@ -72,7 +72,6 @@ export const syncWorkoutSessionAction = actionClient.schema(syncWorkoutSessionSc
         startedAt: sessionData.startedAt,
         endedAt: sessionData.endedAt,
         userId: sessionData.userId,
-        // 1. Supprimer les exercices existants (et donc les sets en cascade)
         exercises: {
           deleteMany: {},
           create: session.exercises.map((exercise) => ({

+ 19 - 0
src/features/workout-session/model/use-workout-sessions.ts

@@ -0,0 +1,19 @@
+"use client";
+
+import { useQuery } from "@tanstack/react-query";
+
+import { useWorkoutSessionService } from "@/shared/lib/workout-session/use-workout-session.service";
+import { useSession } from "@/features/auth/lib/auth-client";
+
+export function useWorkoutSessions() {
+  const { data: session } = useSession();
+
+  const { getAll } = useWorkoutSessionService();
+
+  return useQuery({
+    queryKey: ["workout-sessions", session?.user?.id],
+    queryFn: async () => {
+      return getAll();
+    },
+  });
+}

+ 9 - 9
src/features/workout-session/ui/workout-session-list.tsx

@@ -1,15 +1,12 @@
-import { useState } from "react";
 import { useRouter } from "next/navigation";
 import { Repeat2, Trash2 } from "lucide-react";
 
 import { useCurrentLocale, useI18n } from "locales/client";
-import { workoutSessionLocal } from "@/shared/lib/workout-session/workout-session.local";
+import { useWorkoutSessions } from "@/features/workout-session/model/use-workout-sessions";
 import { useWorkoutBuilderStore } from "@/features/workout-builder/model/workout-builder.store";
 import { InlineTooltip } from "@/components/ui/tooltip";
 import { Button } from "@/components/ui/button";
 
-import type { WorkoutSession } from "@/shared/lib/workout-session/types/workout-session";
-
 const BADGE_COLORS = [
   "bg-blue-100 text-blue-700 border-blue-300",
   "bg-green-100 text-green-700 border-green-300",
@@ -25,13 +22,16 @@ export function WorkoutSessionList() {
   const router = useRouter();
   const loadFromSession = useWorkoutBuilderStore((s) => s.loadFromSession);
 
-  const [sessions, setSessions] = useState<WorkoutSession[]>(() =>
-    workoutSessionLocal.getAll().sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()),
-  );
+  // const [sessions, setSessions] = useState<WorkoutSession[]>(() =>
+  //   workoutSessionLocal.getAll().sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()),
+  // );
+
+  const { data: sessions = [], isLoading } = useWorkoutSessions();
+  console.log("sessions:", sessions);
 
   const handleDelete = (id: string) => {
-    workoutSessionLocal.remove(id);
-    setSessions(workoutSessionLocal.getAll().sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()));
+    // TODO: delete by service
+    // workoutSessionLocal.remove(id);
   };
 
   const handleRepeat = (id: string) => {

+ 114 - 0
src/shared/lib/workout-session/use-workout-session.service.ts

@@ -0,0 +1,114 @@
+import { syncWorkoutSessionAction } from "@/features/workout-session/actions/sync-workout-sessions.action";
+import { getWorkoutSessionsAction } from "@/features/workout-session/actions/get-workout-sessions.action";
+import { useSession } from "@/features/auth/lib/auth-client";
+
+import { workoutSessionLocal } from "./workout-session.local";
+
+import type { WorkoutSession } from "./types/workout-session";
+
+// This is an abstraction layer to handle the local storage and/or the API calls.
+// He's the orchestrator.
+
+// TODO: replace with auth context
+function isUserLoggedIn(): boolean {
+  return !!localStorage.getItem("userToken");
+}
+
+export const useWorkoutSessionService = () => {
+  const { data: session } = useSession();
+  const userId = session?.user?.id;
+
+  const getAll = async (): Promise<WorkoutSession[]> => {
+    if (userId) {
+      const result = await getWorkoutSessionsAction({ userId });
+      if (result?.serverError) throw new Error(result.serverError);
+
+      const serverSessions = (result?.data?.sessions || []).map((session) => ({
+        ...session,
+        startedAt: session.startedAt instanceof Date ? session.startedAt.toISOString() : session.startedAt,
+        endedAt:
+          session.endedAt instanceof Date
+            ? session.endedAt.toISOString()
+            : typeof session.endedAt === "string"
+              ? session.endedAt
+              : undefined,
+        duration: session.duration === null ? undefined : session.duration,
+        exercises: session.exercises.map(({ exercise, order, sets }) => ({
+          ...exercise,
+          order,
+          sets: sets.map((set) => ({
+            ...set,
+            valueInt: set.valueInt === null ? undefined : set.valueInt,
+            valueSec: set.valueSec === null ? undefined : set.valueSec,
+            unit: set.unit === null ? undefined : set.unit,
+            units: set.units === null ? undefined : set.units,
+          })),
+        })),
+      }));
+      const localSessions = workoutSessionLocal.getAll().filter((s) => s.status !== "synced");
+
+      return [...localSessions, ...serverSessions].sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime());
+    }
+
+    return workoutSessionLocal.getAll().sort((a, b) => {
+      const dateA = typeof a.startedAt === "string" ? new Date(a.startedAt) : a.startedAt;
+      const dateB = typeof b.startedAt === "string" ? new Date(b.startedAt) : b.startedAt;
+      return dateB.getTime() - dateA.getTime();
+    });
+  };
+
+  const add = async (session: WorkoutSession) => {
+    if (userId) {
+      // Utiliser l'action de synchronisation
+      const result = await syncWorkoutSessionAction({
+        session: {
+          ...session,
+          userId: "current-user-id", // TODO: passer le vrai userId
+          status: "synced",
+        },
+      });
+
+      if (result?.serverError) throw new Error(result.serverError);
+
+      if (result?.data?.data) {
+        workoutSessionLocal.markSynced(session.id, result.data.data.id);
+      }
+    }
+
+    return workoutSessionLocal.add(session);
+  };
+
+  const update = async (id: string, data: Partial<WorkoutSession>) => {
+    if (userId) {
+      // TODO: Créer une action updateWorkoutSessionAction
+      const result = await updateWorkoutSessionAction({ id, data });
+      if (result.serverError) throw new Error(result.serverError);
+    }
+    return workoutSessionLocal.update(id, data);
+  };
+
+  const complete = async (id: string) => {
+    const data = {
+      status: "completed" as const,
+      endedAt: new Date().toISOString(),
+    };
+
+    if (isUserLoggedIn()) {
+      const result = await completeWorkoutSessionAction({ id });
+      if (result.serverError) throw new Error(result.serverError);
+    }
+
+    return workoutSessionLocal.update(id, data);
+  };
+
+  const remove = async (id: string) => {
+    if (isUserLoggedIn()) {
+      // TODO: Créer une action deleteWorkoutSessionAction
+      const result = await deleteWorkoutSessionAction({ id });
+      if (result.serverError) throw new Error(result.serverError);
+    }
+    workoutSessionLocal.remove(id);
+  };
+
+  return { getAll, add, update, complete, remove };
+};

+ 0 - 31
src/shared/lib/workout-session/workout-session.service.ts

@@ -1,31 +0,0 @@
-import { workoutSessionLocal } from "./workout-session.local";
-import { workoutSessionApi } from "./workout-session.api";
-
-import type { WorkoutSession } from "./types/workout-session";
-
-// This is an abstraction layer to handle the local storage and/or the API calls.
-// He's the orchestrator.
-
-// TODO: replace with auth context
-function isUserLoggedIn(): boolean {
-  return !!localStorage.getItem("userToken");
-}
-
-export const workoutSessionService = {
-  getAll: async (): Promise<WorkoutSession[]> => {
-    if (isUserLoggedIn()) return workoutSessionApi.getAll();
-    return workoutSessionLocal.getAll();
-  },
-  add: async (session: WorkoutSession) => {
-    if (isUserLoggedIn()) return workoutSessionApi.create(session);
-    return workoutSessionLocal.add(session);
-  },
-  update: async (id: string, data: Partial<WorkoutSession>) => {
-    if (isUserLoggedIn()) return workoutSessionApi.update(id, data);
-    return workoutSessionLocal.update(id, data);
-  },
-  complete: async (id: string) => {
-    if (isUserLoggedIn()) return workoutSessionApi.complete(id);
-    return workoutSessionLocal.update(id, { status: "completed", endedAt: new Date().toISOString() });
-  },
-};