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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Database Configuration
POSTGRES_USER=username
POSTGRES_PASSWORD=password
POSTGRES_DB=workout-cool
POSTGRES_DB=workout_cool
DB_HOST=localhost
DB_PORT=5432
# Format: postgresql://USER:PASSWORD@HOST:PORT/DATABASE
Expand Down
22 changes: 22 additions & 0 deletions app/[locale]/(admin)/admin/exercises/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Suspense } from "react";

import { ExercisesList } from "@/features/admin/exercises/ui/exercises-list";
import { CreateExerciseButton } from "@/features/admin/exercises/ui/create-exercise-button";

export default function AdminExercises() {
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">Exercises</h1>
<p className="text-muted-foreground">Create, edit, view and delete exercises.</p>
</div>
<CreateExerciseButton />
</div>

<Suspense fallback={<div>Loading exercises...</div>}>
<ExercisesList />
</Suspense>
</div>
);
}
85 changes: 85 additions & 0 deletions src/features/admin/exercises/actions/create-exercise.action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"use server";

import { headers } from "next/headers";
import { revalidatePath } from "next/cache";
import { UserRole } from "@prisma/client";

import { prisma } from "@/shared/lib/prisma";
import { auth } from "@/features/auth/lib/better-auth";

import { CreateExerciseData } from "../types/exercise.types";

export async function createExercise(data: CreateExerciseData) {
const session = await auth.api.getSession({
headers: await headers(),
});

if (!session || session.user?.role !== UserRole.admin) {
throw new Error("Unauthorized");
}

// Generate slugs
const slug = data.name
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");

const slugEn = data.nameEn
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");

// Check if slugs already exist
const existingExercise = await prisma.exercise.findFirst({
where: {
OR: [{ slug }, { slugEn }],
},
});

if (existingExercise) {
throw new Error("Un exercice avec ce nom existe dΓ©jΓ ");
}

// Create exercise
const exercise = await prisma.exercise.create({
data: {
name: data.name,
nameEn: data.nameEn,
description: data.description,
descriptionEn: data.descriptionEn,
fullVideoUrl: data.fullVideoUrl,
fullVideoImageUrl: data.fullVideoImageUrl,
introduction: data.introduction,
introductionEn: data.introductionEn,
slug,
slugEn,
},
});

// Add attributes if provided
if (data.attributeIds && data.attributeIds.length > 0) {
// Get the attribute values with their names to create proper relationships
const attributeValues = await prisma.exerciseAttributeValue.findMany({
where: {
id: {
in: data.attributeIds,
},
},
include: {
attributeName: true,
},
});

// Create exercise attributes with proper name/value relationships
await prisma.exerciseAttribute.createMany({
data: attributeValues.map((attributeValue) => ({
exerciseId: exercise.id,
attributeNameId: attributeValue.attributeNameId,
attributeValueId: attributeValue.id,
})),
});
}

revalidatePath("/admin/exercises");
return exercise;
}
57 changes: 57 additions & 0 deletions src/features/admin/exercises/actions/delete-exercise.action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"use server";

import { headers } from "next/headers";
import { revalidatePath } from "next/cache";
import { UserRole } from "@prisma/client";

import { prisma } from "@/shared/lib/prisma";
import { auth } from "@/features/auth/lib/better-auth";

export async function deleteExercise(id: string) {
const session = await auth.api.getSession({
headers: await headers(),
});

if (!session || session.user?.role !== UserRole.admin) {
throw new Error("Unauthorized");
}

// Check if exercise is used in any program or workout
const usage = await prisma.exercise.findUnique({
where: { id },
include: {
_count: {
select: {
ProgramSessionExercise: true,
WorkoutSessionExercise: true,
},
},
},
});

if (!usage) {
throw new Error("Exercise not found");
}

if (usage._count.ProgramSessionExercise > 0 || usage._count.WorkoutSessionExercise > 0) {
throw new Error("Cannot delete exercise: it is being used in programs or workouts");
}

try {
// Delete exercise attributes first
await prisma.exerciseAttribute.deleteMany({
where: { exerciseId: id },
});

// Delete exercise
await prisma.exercise.delete({
where: { id },
});

revalidatePath("/admin/exercises");
return { success: true };
} catch (error) {
console.error("Error deleting exercise:", error);
throw new Error("Erreur lors de la suppression de l'exercice");
}
}
109 changes: 109 additions & 0 deletions src/features/admin/exercises/actions/get-exercises.action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"use server";

import { headers } from "next/headers";
import { UserRole } from "@prisma/client";

import { prisma } from "@/shared/lib/prisma";
import { auth } from "@/features/auth/lib/better-auth";

import { ExerciseWithStats } from "../types/exercise.types";

export async function getExercises(search?: string): Promise<ExerciseWithStats[]> {
const session = await auth.api.getSession({
headers: await headers(),
});

if (!session || session.user?.role !== UserRole.admin) {
throw new Error("Unauthorized");
}

const where = search
? {
OR: [{ name: { contains: search, mode: "insensitive" as const } }, { nameEn: { contains: search, mode: "insensitive" as const } }],
}
: {};

const exercises = await prisma.exercise.findMany({
where,
include: {
attributes: {
include: {
attributeName: true,
attributeValue: true,
},
},
_count: {
select: {
ProgramSessionExercise: true,
WorkoutSessionExercise: true,
favoritesByUsers: true,
},
},
},
orderBy: { createdAt: "desc" },
});

return exercises.map((exercise) => ({
...exercise,
totalProgramUsage: exercise._count.ProgramSessionExercise,
totalWorkoutUsage: exercise._count.WorkoutSessionExercise,
totalFavorites: exercise._count.favoritesByUsers,
}));
}

export async function getExerciseById(id: string) {
const session = await auth.api.getSession({
headers: await headers(),
});

if (!session || session.user?.role !== UserRole.admin) {
throw new Error("Unauthorized");
}

const exercise = await prisma.exercise.findUnique({
where: { id },
include: {
attributes: {
include: {
attributeName: true,
attributeValue: true,
},
},
_count: {
select: {
ProgramSessionExercise: true,
WorkoutSessionExercise: true,
favoritesByUsers: true,
},
},
},
});

if (!exercise) {
throw new Error("Exercise not found");
}

return {
...exercise,
totalProgramUsage: exercise._count.ProgramSessionExercise,
totalWorkoutUsage: exercise._count.WorkoutSessionExercise,
totalFavorites: exercise._count.favoritesByUsers,
};
}

export async function getExerciseAttributeNames() {
const session = await auth.api.getSession({
headers: await headers(),
});

if (!session || session.user?.role !== UserRole.admin) {
throw new Error("Unauthorized");
}

return await prisma.exerciseAttributeName.findMany({
include: {
values: true,
},
orderBy: { name: "asc" },
});
}
92 changes: 92 additions & 0 deletions src/features/admin/exercises/actions/update-exercise.action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"use server";

import { headers } from "next/headers";
import { revalidatePath } from "next/cache";
import { UserRole } from "@prisma/client";

import { prisma } from "@/shared/lib/prisma";
import { auth } from "@/features/auth/lib/better-auth";

import { UpdateExerciseData } from "../types/exercise.types";

export async function updateExercise(data: UpdateExerciseData) {
const session = await auth.api.getSession({
headers: await headers(),
});

if (!session || session.user?.role !== UserRole.admin) {
throw new Error("Unauthorized");
}

const updateData: any = {};

// Update basic fields
if (data.name) updateData.name = data.name;
if (data.nameEn) updateData.nameEn = data.nameEn;
if (data.description !== undefined) updateData.description = data.description;
if (data.descriptionEn !== undefined) updateData.descriptionEn = data.descriptionEn;
if (data.fullVideoUrl !== undefined) updateData.fullVideoUrl = data.fullVideoUrl;
if (data.fullVideoImageUrl !== undefined) updateData.fullVideoImageUrl = data.fullVideoImageUrl;
if (data.introduction !== undefined) updateData.introduction = data.introduction;
if (data.introductionEn !== undefined) updateData.introductionEn = data.introductionEn;

// Update slugs if name changed
if (data.name) {
updateData.slug = data.name
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}

if (data.nameEn) {
updateData.slugEn = data.nameEn
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}

try {
const exercise = await prisma.exercise.update({
where: { id: data.id },
data: updateData,
});

// Update attributes if provided
if (data.attributeIds) {
// Remove existing attributes
await prisma.exerciseAttribute.deleteMany({
where: { exerciseId: data.id },
});

// Add new attributes
if (data.attributeIds.length > 0) {
// Get the attribute values with their names to create proper relationships
const attributeValues = await prisma.exerciseAttributeValue.findMany({
where: {
id: {
in: data.attributeIds,
},
},
include: {
attributeName: true,
},
});

// Create exercise attributes with proper name/value relationships
await prisma.exerciseAttribute.createMany({
data: attributeValues.map((attributeValue) => ({
exerciseId: data.id,
attributeNameId: attributeValue.attributeNameId,
attributeValueId: attributeValue.id,
})),
});
}
}

revalidatePath("/admin/exercises");
return exercise;
} catch (error) {
console.error("Error updating exercise:", error);
throw new Error("Erreur lors de la mise Γ  jour de l'exercice");
}
}
Loading