Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,8 @@ export function ProgramSessionClient({ program, week, session, isAuthenticated,
if (!workoutSession || !sessionProgressId) return;

try {
// Complete the workout
completeWorkout();
// Complete the workout (now async and will call our API)
await completeWorkout();

// Save to database and mark session as complete
const { isCompleted, nextWeek, nextSession } = await completeProgramSession(sessionProgressId, workoutSession.id);
Expand Down
48 changes: 48 additions & 0 deletions app/api/workout-sessions/[sessionId]/complete/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { NextRequest, NextResponse } from "next/server";

import { getMobileCompatibleSession } from "@/shared/api/mobile-auth";
import { completeWorkoutSessionAction } from "@/features/workout-session/actions/complete-workout-session.action";

export async function PATCH(request: NextRequest, { params }: { params: Promise<{ sessionId: string }> }) {
try {
const { sessionId } = await params;

// Check authentication
const session = await getMobileCompatibleSession(request);
if (!session?.user) {
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
}

const body = await request.json();

// Use the complete server action
const result = await completeWorkoutSessionAction({
id: sessionId,
endedAt: body.endedAt ? new Date(body.endedAt) : new Date(),
duration: body.duration,
rating: body.rating,
ratingComment: body.ratingComment,
});

if (result?.serverError) {
if (result.serverError === "NOT_AUTHENTICATED") {
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
}
if (result.serverError === "Unauthorized") {
return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
}
if (result.serverError === "Session not found") {
return NextResponse.json({ error: "Session not found" }, { status: 404 });
}
return NextResponse.json({ error: result.serverError }, { status: 500 });
}

return NextResponse.json({
success: true,
data: result.data
});
} catch (error) {
console.error("Error completing workout session:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}
42 changes: 42 additions & 0 deletions app/api/workout-sessions/[sessionId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,48 @@ import { NextRequest, NextResponse } from "next/server";

import { getMobileCompatibleSession } from "@/shared/api/mobile-auth";
import { deleteWorkoutSessionAction } from "@/features/workout-session/actions/delete-workout-session.action";
import { updateWorkoutSessionAction } from "@/features/workout-session/actions/update-workout-session.action";

export async function PATCH(request: NextRequest, { params }: { params: Promise<{ sessionId: string }> }) {
try {
const { sessionId } = await params;

// Check authentication
const session = await getMobileCompatibleSession(request);
if (!session?.user) {
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
}

const body = await request.json();

// Use the update server action
const result = await updateWorkoutSessionAction({
id: sessionId,
data: body,
});

if (result?.serverError) {
if (result.serverError === "NOT_AUTHENTICATED") {
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
}
if (result.serverError === "Unauthorized") {
return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
}
if (result.serverError === "Session not found") {
return NextResponse.json({ error: "Session not found" }, { status: 404 });
}
return NextResponse.json({ error: result.serverError }, { status: 500 });
}

return NextResponse.json({
success: true,
data: result.data
});
} catch (error) {
console.error("Error updating workout session:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}

export async function DELETE(request: NextRequest, { params }: { params: Promise<{ sessionId: string }> }) {
try {
Expand Down
119 changes: 119 additions & 0 deletions app/api/workout-sessions/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { NextRequest, NextResponse } from "next/server";

import { getMobileCompatibleSession } from "@/shared/api/mobile-auth";
import { createWorkoutSessionAction } from "@/features/workout-session/actions/create-workout-session.action";
import { getWorkoutSessionsAction } from "@/features/workout-session/actions/get-workout-sessions.action";

export async function GET(request: NextRequest) {
try {
// Check authentication
const session = await getMobileCompatibleSession(request);

if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

// Fetch workout sessions for the authenticated user
const result = await getWorkoutSessionsAction({ userId: session.user.id });

if (result?.serverError) {
return NextResponse.json({ error: result.serverError }, { status: 500 });
}

// Transform to match the expected response format
const formattedSessions = result?.data?.sessions?.map((session) => ({
id: session.id,
userId: session.userId,
startedAt: session.startedAt.toISOString(),
endedAt: session.endedAt?.toISOString() || null,
duration: session.duration || null,
muscles: session.muscles || [],
rating: session.rating || null,
ratingComment: session.ratingComment || null,
exercises: session.exercises.map((sessionExercise) => ({
id: sessionExercise.exerciseId,
order: sessionExercise.order,
exercise: {
...sessionExercise.exercise,
createdAt: sessionExercise.exercise.createdAt.toISOString(),
updatedAt: sessionExercise.exercise.updatedAt.toISOString(),
attributes: sessionExercise.exercise.attributes.map((attr) => ({
id: attr.id,
exerciseId: attr.exerciseId,
attributeNameId: attr.attributeNameId,
attributeValueId: attr.attributeValueId,
createdAt: attr.createdAt.toISOString(),
updatedAt: attr.updatedAt.toISOString(),
attributeName: {
id: attr.attributeName.id,
name: attr.attributeName.name,
createdAt: attr.attributeName.createdAt.toISOString(),
updatedAt: attr.attributeName.updatedAt.toISOString(),
},
attributeValue: {
id: attr.attributeValue.id,
attributeNameId: attr.attributeValue.attributeNameId,
value: attr.attributeValue.value,
createdAt: attr.attributeValue.createdAt.toISOString(),
updatedAt: attr.attributeValue.updatedAt.toISOString(),
},
})),
},
sets: sessionExercise.sets.map((set) => ({
id: set.id,
workoutSessionExerciseId: set.workoutSessionExerciseId,
setIndex: set.setIndex,
type: set.type,
types: set.types || [],
valuesInt: set.valuesInt || [],
valuesSec: set.valuesSec || [],
units: set.units || [],
completed: set.completed,
})),
})),
}));

return NextResponse.json(formattedSessions);
} catch (error) {
console.error("Error fetching workout sessions:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}

export async function POST(request: NextRequest) {
try {
// Check authentication
const session = await getMobileCompatibleSession(request);

if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const body = await request.json();

// Validate required fields
if (!body.startedAt) {
return NextResponse.json({ error: "startedAt is required" }, { status: 400 });
}

// Create the workout session
const result = await createWorkoutSessionAction({
userId: session.user.id,
startedAt: new Date(body.startedAt),
exercises: body.exercises || [],
muscles: body.muscles || [],
});

if (result?.serverError) {
return NextResponse.json({ error: result.serverError }, { status: 500 });
}

return NextResponse.json({
success: true,
data: result.data
});
} catch (error) {
console.error("Error creating workout session:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"use server";

import { z } from "zod";

import { prisma } from "@/shared/lib/prisma";
import { actionClient } from "@/shared/api/safe-actions";

const completeWorkoutSessionSchema = z.object({
id: z.string(),
endedAt: z.date(),
duration: z.number().optional(),
rating: z.number().min(1).max(5).optional(),
ratingComment: z.string().optional(),
});

export const completeWorkoutSessionAction = actionClient
.schema(completeWorkoutSessionSchema)
.action(async ({ parsedInput }) => {
try {
const { id, endedAt, duration, rating, ratingComment } = parsedInput;

// First, check if the session exists
const existingSession = await prisma.workoutSession.findFirst({
where: { id },
});

if (!existingSession) {
return { serverError: "Session not found" };
}

// Calculate duration if not provided
let calculatedDuration = duration;
if (!calculatedDuration && existingSession.startedAt) {
calculatedDuration = Math.floor((endedAt.getTime() - existingSession.startedAt.getTime()) / 1000);
}

// Update the workout session to mark it as completed
const completedSession = await prisma.workoutSession.update({
where: { id },
data: {
endedAt,
duration: calculatedDuration,
rating,
ratingComment,
},
include: {
exercises: {
include: {
exercise: {
include: {
attributes: {
include: {
attributeName: true,
attributeValue: true,
},
},
},
},
sets: true,
},
},
},
});

return { data: completedSession };
} catch (error) {
console.error("Error completing workout session:", error);
return { serverError: "Failed to complete workout session" };
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"use server";

import { z } from "zod";
import { ExerciseAttributeValueEnum } from "@prisma/client";

import { prisma } from "@/shared/lib/prisma";
import { actionClient } from "@/shared/api/safe-actions";

const createWorkoutSessionSchema = z.object({
userId: z.string(),
startedAt: z.date(),
endedAt: z.date().optional(),
duration: z.number().optional(),
exercises: z.array(z.object({
exerciseId: z.string(),
order: z.number(),
sets: z.array(z.object({
setIndex: z.number(),
type: z.string(),
types: z.array(z.string()).optional(),
valuesInt: z.array(z.number()).optional(),
valuesSec: z.array(z.number()).optional(),
units: z.array(z.string()).optional(),
completed: z.boolean().optional(),
})).optional(),
})).optional(),
muscles: z.array(z.nativeEnum(ExerciseAttributeValueEnum)).optional(),
rating: z.number().min(1).max(5).optional(),
ratingComment: z.string().optional(),
});

export const createWorkoutSessionAction = actionClient
.schema(createWorkoutSessionSchema)
.action(async ({ parsedInput }) => {
try {
const { userId, startedAt, endedAt, duration, exercises, muscles, rating, ratingComment } = parsedInput;

// Create the workout session with exercises and sets
const workoutSession = await prisma.workoutSession.create({
data: {
userId,
startedAt,
endedAt,
duration,
muscles: muscles || [],
rating,
ratingComment,
exercises: exercises ? {
create: exercises.map((exercise) => ({
exerciseId: exercise.exerciseId,
order: exercise.order,
sets: exercise.sets ? {
create: exercise.sets.map((set) => ({
setIndex: set.setIndex,
type: set.type as any, // Will be validated by Prisma
types: set.types || [],
valuesInt: set.valuesInt || [],
valuesSec: set.valuesSec || [],
units: set.units || [],
completed: set.completed || false,
})),
} : undefined,
})),
} : undefined,
},
include: {
exercises: {
include: {
exercise: {
include: {
attributes: {
include: {
attributeName: true,
attributeValue: true,
},
},
},
},
sets: true,
},
},
},
});

return { data: workoutSession };
} catch (error) {
console.error("Error creating workout session:", error);
return { serverError: "Failed to create workout session" };
}
});
Loading