Compare commits
4 Commits
48446b483c
...
feat/phase
| Author | SHA1 | Date | |
|---|---|---|---|
| 566daedd01 | |||
| 55bf7cbac1 | |||
| 2389376721 | |||
| 4646c6ed1a |
@@ -8,7 +8,8 @@
|
|||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"db:migrate": "prisma migrate dev",
|
"db:migrate": "prisma migrate dev",
|
||||||
"db:generate": "prisma generate",
|
"db:generate": "prisma generate",
|
||||||
"db:studio": "prisma studio"
|
"db:studio": "prisma studio",
|
||||||
|
"db:seed": "ts-node src/db/seed.ts"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Added the required column `programId` to the `histories` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "histories" ADD COLUMN "completedAt" TIMESTAMP(3),
|
||||||
|
ADD COLUMN "programId" TEXT NOT NULL;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "histories" ADD CONSTRAINT "histories_programId_fkey" FOREIGN KEY ("programId") REFERENCES "programs"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -88,6 +88,7 @@ model Program {
|
|||||||
|
|
||||||
exercises ProgramExercise[]
|
exercises ProgramExercise[]
|
||||||
groups GroupProgram[]
|
groups GroupProgram[]
|
||||||
|
histories History[]
|
||||||
|
|
||||||
@@map("programs")
|
@@map("programs")
|
||||||
}
|
}
|
||||||
@@ -113,10 +114,13 @@ model ProgramExercise {
|
|||||||
model History {
|
model History {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
date DateTime @default(now())
|
date DateTime @default(now())
|
||||||
|
completedAt DateTime?
|
||||||
notes String?
|
notes String?
|
||||||
|
|
||||||
userId String
|
userId String
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
programId String
|
||||||
|
program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
entries HistoryEntry[]
|
entries HistoryEntry[]
|
||||||
|
|
||||||
|
|||||||
48
backend/src/controllers/exercise.controller.ts
Normal file
48
backend/src/controllers/exercise.controller.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import type { Response } from "express";
|
||||||
|
import { prisma } from "../index";
|
||||||
|
import type { AppRequest } from "../types/context";
|
||||||
|
import { createExerciseSchema, updateExerciseSchema } from "../validators/exercise.validators";
|
||||||
|
|
||||||
|
export async function getAll(_req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const exercises = await prisma.exercise.findMany({
|
||||||
|
orderBy: { name: "asc" },
|
||||||
|
});
|
||||||
|
res.json(exercises);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOne(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const id = req.params.id as string;
|
||||||
|
const exercise = await prisma.exercise.findUnique({ where: { id } });
|
||||||
|
if (!exercise) {
|
||||||
|
res.status(404).json({ message: "Exercice introuvable." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json(exercise);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function create(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const parsed = createExerciseSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
res.status(400).json({ message: parsed.error.issues[0].message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const exercise = await prisma.exercise.create({ data: parsed.data });
|
||||||
|
res.status(201).json(exercise);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function update(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const id = req.params.id as string;
|
||||||
|
const parsed = updateExerciseSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
res.status(400).json({ message: parsed.error.issues[0].message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const exercise = await prisma.exercise.update({ where: { id }, data: parsed.data });
|
||||||
|
res.json(exercise);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function remove(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const id = req.params.id as string;
|
||||||
|
await prisma.exercise.delete({ where: { id } });
|
||||||
|
res.status(204).send();
|
||||||
|
}
|
||||||
116
backend/src/controllers/friend.controller.ts
Normal file
116
backend/src/controllers/friend.controller.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import type { Response } from "express";
|
||||||
|
import { prisma } from "../index";
|
||||||
|
import type { AppRequest } from "../types/context";
|
||||||
|
import { friendRequestSchema, friendResponseSchema } from "../validators/friend.validators";
|
||||||
|
import { checkAndGrantRewards } from "../services/reward.service";
|
||||||
|
|
||||||
|
// Envoyer une demande d'ami
|
||||||
|
export async function sendRequest(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const parsed = friendRequestSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
res.status(400).json({ message: parsed.error.issues[0].message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { receiverId } = parsed.data;
|
||||||
|
const senderId = req.user!.id;
|
||||||
|
|
||||||
|
if (senderId === receiverId) {
|
||||||
|
res.status(400).json({ message: "Impossible de s'ajouter soi-même." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const receiver = await prisma.user.findUnique({ where: { id: receiverId } });
|
||||||
|
if (!receiver) {
|
||||||
|
res.status(404).json({ message: "Utilisateur introuvable." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await prisma.friendRequest.findFirst({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ senderId, receiverId },
|
||||||
|
{ senderId: receiverId, receiverId: senderId },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (existing) {
|
||||||
|
res.status(409).json({ message: "Demande déjà existante." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = await prisma.friendRequest.create({
|
||||||
|
data: { senderId, receiverId },
|
||||||
|
include: {
|
||||||
|
receiver: { select: { id: true, username: true, avatar: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
res.status(201).json(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Répondre à une demande (accepter/refuser)
|
||||||
|
export async function respondToRequest(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const id = req.params.id as string;
|
||||||
|
const parsed = friendResponseSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
res.status(400).json({ message: parsed.error.issues[0].message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = await prisma.friendRequest.findFirst({
|
||||||
|
where: { id, receiverId: req.user!.id, status: "PENDING" },
|
||||||
|
});
|
||||||
|
if (!request) {
|
||||||
|
res.status(404).json({ message: "Demande introuvable." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await prisma.friendRequest.update({
|
||||||
|
where: { id },
|
||||||
|
data: { status: parsed.data.status },
|
||||||
|
include: {
|
||||||
|
sender: { select: { id: true, username: true, avatar: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Déclenche les récompenses pour les deux parties si accepté
|
||||||
|
if (parsed.data.status === "ACCEPTED") {
|
||||||
|
await Promise.all([
|
||||||
|
checkAndGrantRewards(req.user!.id),
|
||||||
|
checkAndGrantRewards(updated.sender.id),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Liste des amis (demandes acceptées)
|
||||||
|
export async function getFriends(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const userId = req.user!.id;
|
||||||
|
const requests = await prisma.friendRequest.findMany({
|
||||||
|
where: {
|
||||||
|
status: "ACCEPTED",
|
||||||
|
OR: [{ senderId: userId }, { receiverId: userId }],
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
sender: { select: { id: true, username: true, avatar: true } },
|
||||||
|
receiver: { select: { id: true, username: true, avatar: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const friends = requests.map((r) =>
|
||||||
|
r.senderId === userId ? r.receiver : r.sender
|
||||||
|
);
|
||||||
|
res.json(friends);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Demandes reçues en attente
|
||||||
|
export async function getPendingRequests(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const requests = await prisma.friendRequest.findMany({
|
||||||
|
where: { receiverId: req.user!.id, status: "PENDING" },
|
||||||
|
include: {
|
||||||
|
sender: { select: { id: true, username: true, avatar: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
res.json(requests);
|
||||||
|
}
|
||||||
132
backend/src/controllers/group.controller.ts
Normal file
132
backend/src/controllers/group.controller.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import type { Response } from "express";
|
||||||
|
import { prisma } from "../index";
|
||||||
|
import type { AppRequest } from "../types/context";
|
||||||
|
import { addGroupProgramSchema, createGroupSchema, inviteMemberSchema } from "../validators/group.validators";
|
||||||
|
|
||||||
|
// Créer un groupe
|
||||||
|
export async function create(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const parsed = createGroupSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
res.status(400).json({ message: parsed.error.issues[0].message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = await prisma.group.create({
|
||||||
|
data: {
|
||||||
|
...parsed.data,
|
||||||
|
members: {
|
||||||
|
create: { userId: req.user!.id, role: "ADMIN" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: { members: { include: { user: { select: { id: true, username: true, avatar: true } } } } },
|
||||||
|
});
|
||||||
|
res.status(201).json(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mes groupes
|
||||||
|
export async function getMyGroups(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const groups = await prisma.group.findMany({
|
||||||
|
where: { members: { some: { userId: req.user!.id } } },
|
||||||
|
include: {
|
||||||
|
members: { include: { user: { select: { id: true, username: true, avatar: true } } } },
|
||||||
|
programs: { include: { program: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
res.json(groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Détail d'un groupe
|
||||||
|
export async function getOne(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const id = req.params.id as string;
|
||||||
|
const group = await prisma.group.findFirst({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
OR: [{ isPublic: true }, { members: { some: { userId: req.user!.id } } }],
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
members: { include: { user: { select: { id: true, username: true, avatar: true } } } },
|
||||||
|
programs: { include: { program: { include: { exercises: { include: { exercise: true } } } } } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!group) {
|
||||||
|
res.status(404).json({ message: "Groupe introuvable." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inviter un membre
|
||||||
|
export async function inviteMember(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const groupId = req.params.id as string;
|
||||||
|
const parsed = inviteMemberSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
res.status(400).json({ message: parsed.error.issues[0].message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que l'appelant est ADMIN ou COACH du groupe
|
||||||
|
const membership = await prisma.groupMember.findUnique({
|
||||||
|
where: { groupId_userId: { groupId, userId: req.user!.id } },
|
||||||
|
});
|
||||||
|
if (!membership || membership.role === "MEMBER") {
|
||||||
|
res.status(403).json({ message: "Accès refusé." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const already = await prisma.groupMember.findUnique({
|
||||||
|
where: { groupId_userId: { groupId, userId: parsed.data.userId } },
|
||||||
|
});
|
||||||
|
if (already) {
|
||||||
|
res.status(409).json({ message: "Déjà membre du groupe." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const member = await prisma.groupMember.create({
|
||||||
|
data: { groupId, userId: parsed.data.userId, role: "MEMBER" },
|
||||||
|
include: { user: { select: { id: true, username: true, avatar: true } } },
|
||||||
|
});
|
||||||
|
res.status(201).json(member);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quitter un groupe
|
||||||
|
export async function leaveGroup(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const groupId = req.params.id as string;
|
||||||
|
const userId = req.user!.id;
|
||||||
|
|
||||||
|
const membership = await prisma.groupMember.findUnique({
|
||||||
|
where: { groupId_userId: { groupId, userId } },
|
||||||
|
});
|
||||||
|
if (!membership) {
|
||||||
|
res.status(404).json({ message: "Tu n'es pas membre de ce groupe." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.groupMember.delete({
|
||||||
|
where: { groupId_userId: { groupId, userId } },
|
||||||
|
});
|
||||||
|
res.status(204).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter un programme au groupe
|
||||||
|
export async function addProgram(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const groupId = req.params.id as string;
|
||||||
|
const parsed = addGroupProgramSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
res.status(400).json({ message: parsed.error.issues[0].message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const membership = await prisma.groupMember.findUnique({
|
||||||
|
where: { groupId_userId: { groupId, userId: req.user!.id } },
|
||||||
|
});
|
||||||
|
if (!membership || membership.role === "MEMBER") {
|
||||||
|
res.status(403).json({ message: "Accès refusé." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = await prisma.groupProgram.create({
|
||||||
|
data: { groupId, programId: parsed.data.programId },
|
||||||
|
include: { program: true },
|
||||||
|
});
|
||||||
|
res.status(201).json(entry);
|
||||||
|
}
|
||||||
125
backend/src/controllers/history.controller.ts
Normal file
125
backend/src/controllers/history.controller.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import type { Response } from "express";
|
||||||
|
import { prisma } from "../index";
|
||||||
|
import type { AppRequest } from "../types/context";
|
||||||
|
import {
|
||||||
|
addEntrySchema,
|
||||||
|
completeHistorySchema,
|
||||||
|
startHistorySchema,
|
||||||
|
} from "../validators/history.validators";
|
||||||
|
import { checkAndGrantRewards } from "../services/reward.service";
|
||||||
|
|
||||||
|
// Démarre une session d'entraînement
|
||||||
|
export async function start(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const parsed = startHistorySchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
res.status(400).json({ message: parsed.error.issues[0].message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const program = await prisma.program.findFirst({
|
||||||
|
where: {
|
||||||
|
id: parsed.data.programId,
|
||||||
|
OR: [{ isPublic: true }, { authorId: req.user!.id }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!program) {
|
||||||
|
res.status(404).json({ message: "Programme introuvable." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const history = await prisma.history.create({
|
||||||
|
data: {
|
||||||
|
userId: req.user!.id,
|
||||||
|
programId: parsed.data.programId,
|
||||||
|
},
|
||||||
|
include: { entries: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json(history);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajoute un exercice complété à la session
|
||||||
|
export async function addEntry(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const historyId = req.params.id as string;
|
||||||
|
const parsed = addEntrySchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
res.status(400).json({ message: parsed.error.issues[0].message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const history = await prisma.history.findFirst({
|
||||||
|
where: { id: historyId, userId: req.user!.id, completedAt: null },
|
||||||
|
});
|
||||||
|
if (!history) {
|
||||||
|
res.status(404).json({ message: "Session introuvable ou déjà terminée." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = await prisma.historyEntry.create({
|
||||||
|
data: { historyId, ...parsed.data },
|
||||||
|
include: { exercise: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Termine la session
|
||||||
|
export async function complete(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const historyId = req.params.id as string;
|
||||||
|
const parsed = completeHistorySchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
res.status(400).json({ message: parsed.error.issues[0].message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const history = await prisma.history.findFirst({
|
||||||
|
where: { id: historyId, userId: req.user!.id, completedAt: null },
|
||||||
|
include: { entries: true },
|
||||||
|
});
|
||||||
|
if (!history) {
|
||||||
|
res.status(404).json({ message: "Session introuvable ou déjà terminée." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const completed = await prisma.history.update({
|
||||||
|
where: { id: historyId },
|
||||||
|
data: {
|
||||||
|
completedAt: new Date(),
|
||||||
|
notes: parsed.data.notes,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
entries: { include: { exercise: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const newRewards = await checkAndGrantRewards(req.user!.id);
|
||||||
|
res.json({ ...completed, newRewards });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Historique de l'utilisateur connecté
|
||||||
|
export async function getMyHistory(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const histories = await prisma.history.findMany({
|
||||||
|
where: { userId: req.user!.id },
|
||||||
|
include: {
|
||||||
|
entries: { include: { exercise: true } },
|
||||||
|
},
|
||||||
|
orderBy: { date: "desc" },
|
||||||
|
});
|
||||||
|
res.json(histories);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Détail d'une session
|
||||||
|
export async function getOne(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const id = req.params.id as string;
|
||||||
|
const history = await prisma.history.findFirst({
|
||||||
|
where: { id, userId: req.user!.id },
|
||||||
|
include: {
|
||||||
|
entries: { include: { exercise: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!history) {
|
||||||
|
res.status(404).json({ message: "Session introuvable." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json(history);
|
||||||
|
}
|
||||||
123
backend/src/controllers/program.controller.ts
Normal file
123
backend/src/controllers/program.controller.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import type { Response } from "express";
|
||||||
|
import { prisma } from "../index";
|
||||||
|
import type { AppRequest } from "../types/context";
|
||||||
|
import { createProgramSchema, updateProgramSchema } from "../validators/program.validators";
|
||||||
|
|
||||||
|
export async function getAll(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const userId = req.user?.id;
|
||||||
|
const programs = await prisma.program.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ isPublic: true },
|
||||||
|
{ authorId: userId },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
author: { select: { id: true, username: true, avatar: true } },
|
||||||
|
exercises: {
|
||||||
|
include: { exercise: true },
|
||||||
|
orderBy: { order: "asc" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
});
|
||||||
|
res.json(programs);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOne(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const id = req.params.id as string;
|
||||||
|
const userId = req.user?.id;
|
||||||
|
const program = await prisma.program.findFirst({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
OR: [{ isPublic: true }, { authorId: userId }],
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
author: { select: { id: true, username: true, avatar: true } },
|
||||||
|
exercises: {
|
||||||
|
include: { exercise: true },
|
||||||
|
orderBy: { order: "asc" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!program) {
|
||||||
|
res.status(404).json({ message: "Programme introuvable." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json(program);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function create(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const parsed = createProgramSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
res.status(400).json({ message: parsed.error.issues[0].message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { exercises, ...programData } = parsed.data;
|
||||||
|
|
||||||
|
const program = await prisma.program.create({
|
||||||
|
data: {
|
||||||
|
...programData,
|
||||||
|
authorId: req.user!.id,
|
||||||
|
exercises: {
|
||||||
|
create: exercises.map(({ exerciseId, sets, reps, durationSec, order }) => ({
|
||||||
|
exerciseId,
|
||||||
|
sets,
|
||||||
|
reps,
|
||||||
|
durationSec,
|
||||||
|
order,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
exercises: {
|
||||||
|
include: { exercise: true },
|
||||||
|
orderBy: { order: "asc" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
res.status(201).json(program);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function update(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const id = req.params.id as string;
|
||||||
|
const parsed = updateProgramSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
res.status(400).json({ message: parsed.error.issues[0].message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const program = await prisma.program.findFirst({
|
||||||
|
where: { id, authorId: req.user!.id },
|
||||||
|
});
|
||||||
|
if (!program) {
|
||||||
|
res.status(404).json({ message: "Programme introuvable." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await prisma.program.update({
|
||||||
|
where: { id },
|
||||||
|
data: parsed.data,
|
||||||
|
include: {
|
||||||
|
exercises: {
|
||||||
|
include: { exercise: true },
|
||||||
|
orderBy: { order: "asc" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
res.json(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function remove(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const id = req.params.id as string;
|
||||||
|
const program = await prisma.program.findFirst({
|
||||||
|
where: { id, authorId: req.user!.id },
|
||||||
|
});
|
||||||
|
if (!program) {
|
||||||
|
res.status(404).json({ message: "Programme introuvable." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await prisma.program.delete({ where: { id } });
|
||||||
|
res.status(204).send();
|
||||||
|
}
|
||||||
19
backend/src/controllers/reward.controller.ts
Normal file
19
backend/src/controllers/reward.controller.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { Response } from "express";
|
||||||
|
import { prisma } from "../index";
|
||||||
|
import type { AppRequest } from "../types/context";
|
||||||
|
|
||||||
|
// Liste toutes les récompenses disponibles
|
||||||
|
export async function getAll(_req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const rewards = await prisma.reward.findMany({ orderBy: { name: "asc" } });
|
||||||
|
res.json(rewards);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récompenses débloquées par l'utilisateur connecté
|
||||||
|
export async function getMyRewards(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const rewards = await prisma.userReward.findMany({
|
||||||
|
where: { userId: req.user!.id },
|
||||||
|
include: { reward: true },
|
||||||
|
orderBy: { earnedAt: "desc" },
|
||||||
|
});
|
||||||
|
res.json(rewards);
|
||||||
|
}
|
||||||
120
backend/src/db/seed.ts
Normal file
120
backend/src/db/seed.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import "dotenv/config";
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
import { PrismaPg } from "@prisma/adapter-pg";
|
||||||
|
import argon2 from "argon2";
|
||||||
|
|
||||||
|
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
||||||
|
const prisma = new PrismaClient({ adapter });
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log("🌱 Seed en cours...");
|
||||||
|
|
||||||
|
// ── Admin ──────────────────────────────────────────────────────────────────
|
||||||
|
const adminPassword = await argon2.hash("admin1234");
|
||||||
|
const admin = await prisma.user.upsert({
|
||||||
|
where: { email: "admin@pulseform.app" },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
username: "admin",
|
||||||
|
email: "admin@pulseform.app",
|
||||||
|
password: adminPassword,
|
||||||
|
role: "ADMIN",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`✅ Admin : ${admin.email}`);
|
||||||
|
|
||||||
|
// ── Exercices ──────────────────────────────────────────────────────────────
|
||||||
|
const exercisesData = [
|
||||||
|
{ name: "Squat", description: "Flexion des jambes, dos droit.", difficulty: "BEGINNER" as const, muscleGroups: ["Legs", "Glutes"] },
|
||||||
|
{ name: "Squat sauté", description: "Squat avec impulsion vers le haut.", difficulty: "INTERMEDIATE" as const, muscleGroups: ["Legs", "Glutes"] },
|
||||||
|
{ name: "Fente avant", description: "Pas en avant, genou à 90°.", difficulty: "BEGINNER" as const, muscleGroups: ["Legs", "Glutes"] },
|
||||||
|
{ name: "Pompes", description: "Bras à largeur d'épaules, corps droit.", difficulty: "BEGINNER" as const, muscleGroups: ["Chest", "Arms"] },
|
||||||
|
{ name: "Pompes diamant", description: "Mains proches, triceps sollicités.", difficulty: "INTERMEDIATE" as const, muscleGroups: ["Arms", "Chest"] },
|
||||||
|
{ name: "Dips", description: "Sur une chaise ou barre parallèle.", difficulty: "INTERMEDIATE" as const, muscleGroups: ["Arms", "Chest"] },
|
||||||
|
{ name: "Traction", description: "Barre fixe, prise pronation.", difficulty: "ADVANCED" as const, muscleGroups: ["Back", "Arms"] },
|
||||||
|
{ name: "Rowing incliné", description: "Haltère, dos à 45°.", difficulty: "INTERMEDIATE" as const, muscleGroups: ["Back"] },
|
||||||
|
{ name: "Planche", description: "Corps droit, abdos contractés.", difficulty: "BEGINNER" as const, muscleGroups: ["Abdominals"] },
|
||||||
|
{ name: "Crunch", description: "Soulever les épaules, pas le dos.", difficulty: "BEGINNER" as const, muscleGroups: ["Abdominals"] },
|
||||||
|
{ name: "Mountain climber", description: "Alterner genoux vers la poitrine.", difficulty: "INTERMEDIATE" as const, muscleGroups: ["Abdominals", "Legs"] },
|
||||||
|
{ name: "Burpee", description: "Pompe + saut, exercice full body.", difficulty: "ADVANCED" as const, muscleGroups: ["Legs", "Chest", "Arms"] },
|
||||||
|
];
|
||||||
|
|
||||||
|
await prisma.exercise.createMany({ data: exercisesData, skipDuplicates: true });
|
||||||
|
const exercises = await prisma.exercise.findMany();
|
||||||
|
console.log(`✅ ${exercises.length} exercices`);
|
||||||
|
|
||||||
|
// ── Programme débutant ─────────────────────────────────────────────────────
|
||||||
|
const squat = exercises.find((e) => e.name === "Squat")!;
|
||||||
|
const pompes = exercises.find((e) => e.name === "Pompes")!;
|
||||||
|
const planche = exercises.find((e) => e.name === "Planche")!;
|
||||||
|
const crunch = exercises.find((e) => e.name === "Crunch")!;
|
||||||
|
const fente = exercises.find((e) => e.name === "Fente avant")!;
|
||||||
|
|
||||||
|
await prisma.program.upsert({
|
||||||
|
where: { id: "00000000-0000-0000-0000-000000000001" },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
id: "00000000-0000-0000-0000-000000000001",
|
||||||
|
name: "Full Body Débutant",
|
||||||
|
description: "Programme complet pour commencer sans équipement.",
|
||||||
|
isPublic: true,
|
||||||
|
authorId: admin.id,
|
||||||
|
exercises: {
|
||||||
|
create: [
|
||||||
|
{ exerciseId: squat.id!, sets: 3, reps: 12, order: 0 },
|
||||||
|
{ exerciseId: pompes.id!, sets: 3, reps: 10, order: 1 },
|
||||||
|
{ exerciseId: fente.id!, sets: 3, reps: 10, order: 2 },
|
||||||
|
{ exerciseId: planche.id!, sets: 3, durationSec: 30, order: 3 },
|
||||||
|
{ exerciseId: crunch.id!, sets: 3, reps: 15, order: 4 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log("✅ Programme : Full Body Débutant");
|
||||||
|
|
||||||
|
// ── Programme intermédiaire ────────────────────────────────────────────────
|
||||||
|
const burpee = exercises.find((e) => e.name === "Burpee")!;
|
||||||
|
const mountain = exercises.find((e) => e.name === "Mountain climber")!;
|
||||||
|
const squatSaute = exercises.find((e) => e.name === "Squat sauté")!;
|
||||||
|
|
||||||
|
await prisma.program.upsert({
|
||||||
|
where: { id: "00000000-0000-0000-0000-000000000002" },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
id: "00000000-0000-0000-0000-000000000002",
|
||||||
|
name: "Cardio Intermédiaire",
|
||||||
|
description: "Circuit cardio sans équipement, intensité modérée.",
|
||||||
|
isPublic: true,
|
||||||
|
authorId: admin.id,
|
||||||
|
exercises: {
|
||||||
|
create: [
|
||||||
|
{ exerciseId: burpee.id!, sets: 4, reps: 8, order: 0 },
|
||||||
|
{ exerciseId: squatSaute.id!, sets: 4, reps: 12, order: 1 },
|
||||||
|
{ exerciseId: mountain.id!, sets: 4, durationSec: 30, order: 2 },
|
||||||
|
{ exerciseId: pompes.id!, sets: 4, reps: 12, order: 3 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log("✅ Programme : Cardio Intermédiaire");
|
||||||
|
|
||||||
|
// ── Récompenses ───────────────────────────────────────────────────────────
|
||||||
|
const rewardsData = [
|
||||||
|
{ name: "Premier pas", description: "Terminer son premier programme.", condition: "first_program" },
|
||||||
|
{ name: "Régulier", description: "Terminer 5 programmes.", condition: "five_programs" },
|
||||||
|
{ name: "Acharné", description: "Terminer 10 programmes.", condition: "ten_programs" },
|
||||||
|
{ name: "Premier ami", description: "Avoir son premier ami.", condition: "first_friend" },
|
||||||
|
{ name: "Populaire", description: "Avoir 5 amis.", condition: "five_friends" },
|
||||||
|
{ name: "Esprit d'équipe", description: "Rejoindre un groupe.", condition: "first_group" },
|
||||||
|
{ name: "Créateur", description: "Créer son premier programme.", condition: "first_program_created" },
|
||||||
|
];
|
||||||
|
|
||||||
|
await prisma.reward.createMany({ data: rewardsData, skipDuplicates: true });
|
||||||
|
console.log(`✅ ${rewardsData.length} récompenses`);
|
||||||
|
|
||||||
|
console.log("🎉 Seed terminé.");
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((e) => { console.error(e); process.exit(1); })
|
||||||
|
.finally(() => prisma.$disconnect());
|
||||||
@@ -6,6 +6,12 @@ import { PrismaClient } from "@prisma/client";
|
|||||||
import { PrismaPg } from "@prisma/adapter-pg";
|
import { PrismaPg } from "@prisma/adapter-pg";
|
||||||
import authRoutes from "./routes/auth.routes";
|
import authRoutes from "./routes/auth.routes";
|
||||||
import userRoutes from "./routes/user.routes";
|
import userRoutes from "./routes/user.routes";
|
||||||
|
import exerciseRoutes from "./routes/exercise.routes";
|
||||||
|
import programRoutes from "./routes/program.routes";
|
||||||
|
import historyRoutes from "./routes/history.routes";
|
||||||
|
import friendRoutes from "./routes/friend.routes";
|
||||||
|
import groupRoutes from "./routes/group.routes";
|
||||||
|
import rewardRoutes from "./routes/reward.routes";
|
||||||
|
|
||||||
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -32,11 +38,12 @@ app.use("/uploads", express.static("uploads"));
|
|||||||
|
|
||||||
app.use("/api/auth", authRoutes);
|
app.use("/api/auth", authRoutes);
|
||||||
app.use("/api/users", userRoutes);
|
app.use("/api/users", userRoutes);
|
||||||
// app.use("/api/exercises", exerciseRoutes);
|
app.use("/api/exercises", exerciseRoutes);
|
||||||
// app.use("/api/programs", programRoutes);
|
app.use("/api/programs", programRoutes);
|
||||||
// app.use("/api/groups", groupRoutes);
|
app.use("/api/history", historyRoutes);
|
||||||
// app.use("/api/friends", friendRoutes);
|
app.use("/api/friends", friendRoutes);
|
||||||
// app.use("/api/history", historyRoutes);
|
app.use("/api/groups", groupRoutes);
|
||||||
|
app.use("/api/rewards", rewardRoutes);
|
||||||
|
|
||||||
app.get("/health", (_req, res) => {
|
app.get("/health", (_req, res) => {
|
||||||
res.json({ status: "ok" });
|
res.json({ status: "ok" });
|
||||||
|
|||||||
13
backend/src/routes/exercise.routes.ts
Normal file
13
backend/src/routes/exercise.routes.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import { getAll, getOne, create, update, remove } from "../controllers/exercise.controller";
|
||||||
|
import { requireAuth, requireRole } from "../middlewares/auth";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get("/", requireAuth, getAll);
|
||||||
|
router.get("/:id", requireAuth, getOne);
|
||||||
|
router.post("/", requireAuth, requireRole("ADMIN", "COACH"), create);
|
||||||
|
router.patch("/:id", requireAuth, requireRole("ADMIN", "COACH"), update);
|
||||||
|
router.delete("/:id", requireAuth, requireRole("ADMIN"), remove);
|
||||||
|
|
||||||
|
export default router;
|
||||||
12
backend/src/routes/friend.routes.ts
Normal file
12
backend/src/routes/friend.routes.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import { sendRequest, respondToRequest, getFriends, getPendingRequests } from "../controllers/friend.controller";
|
||||||
|
import { requireAuth } from "../middlewares/auth";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get("/", requireAuth, getFriends);
|
||||||
|
router.get("/pending", requireAuth, getPendingRequests);
|
||||||
|
router.post("/", requireAuth, sendRequest);
|
||||||
|
router.patch("/:id", requireAuth, respondToRequest);
|
||||||
|
|
||||||
|
export default router;
|
||||||
14
backend/src/routes/group.routes.ts
Normal file
14
backend/src/routes/group.routes.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import { create, getMyGroups, getOne, inviteMember, leaveGroup, addProgram } from "../controllers/group.controller";
|
||||||
|
import { requireAuth } from "../middlewares/auth";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get("/", requireAuth, getMyGroups);
|
||||||
|
router.get("/:id", requireAuth, getOne);
|
||||||
|
router.post("/", requireAuth, create);
|
||||||
|
router.post("/:id/members", requireAuth, inviteMember);
|
||||||
|
router.post("/:id/programs", requireAuth, addProgram);
|
||||||
|
router.delete("/:id/leave", requireAuth, leaveGroup);
|
||||||
|
|
||||||
|
export default router;
|
||||||
13
backend/src/routes/history.routes.ts
Normal file
13
backend/src/routes/history.routes.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import { start, addEntry, complete, getMyHistory, getOne } from "../controllers/history.controller";
|
||||||
|
import { requireAuth } from "../middlewares/auth";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get("/", requireAuth, getMyHistory);
|
||||||
|
router.get("/:id", requireAuth, getOne);
|
||||||
|
router.post("/", requireAuth, start);
|
||||||
|
router.post("/:id/entries", requireAuth, addEntry);
|
||||||
|
router.patch("/:id/complete", requireAuth, complete);
|
||||||
|
|
||||||
|
export default router;
|
||||||
13
backend/src/routes/program.routes.ts
Normal file
13
backend/src/routes/program.routes.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import { getAll, getOne, create, update, remove } from "../controllers/program.controller";
|
||||||
|
import { requireAuth } from "../middlewares/auth";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get("/", requireAuth, getAll);
|
||||||
|
router.get("/:id", requireAuth, getOne);
|
||||||
|
router.post("/", requireAuth, create);
|
||||||
|
router.patch("/:id", requireAuth, update);
|
||||||
|
router.delete("/:id", requireAuth, remove);
|
||||||
|
|
||||||
|
export default router;
|
||||||
10
backend/src/routes/reward.routes.ts
Normal file
10
backend/src/routes/reward.routes.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import { getAll, getMyRewards } from "../controllers/reward.controller";
|
||||||
|
import { requireAuth } from "../middlewares/auth";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get("/", requireAuth, getAll);
|
||||||
|
router.get("/me", requireAuth, getMyRewards);
|
||||||
|
|
||||||
|
export default router;
|
||||||
76
backend/src/services/reward.service.ts
Normal file
76
backend/src/services/reward.service.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { prisma } from "../index";
|
||||||
|
|
||||||
|
// Conditions disponibles — à étendre librement
|
||||||
|
const CONDITIONS: Record<string, (userId: string) => Promise<boolean>> = {
|
||||||
|
first_program: async (userId) => {
|
||||||
|
const count = await prisma.history.count({
|
||||||
|
where: { userId, completedAt: { not: null } },
|
||||||
|
});
|
||||||
|
return count >= 1;
|
||||||
|
},
|
||||||
|
five_programs: async (userId) => {
|
||||||
|
const count = await prisma.history.count({
|
||||||
|
where: { userId, completedAt: { not: null } },
|
||||||
|
});
|
||||||
|
return count >= 5;
|
||||||
|
},
|
||||||
|
ten_programs: async (userId) => {
|
||||||
|
const count = await prisma.history.count({
|
||||||
|
where: { userId, completedAt: { not: null } },
|
||||||
|
});
|
||||||
|
return count >= 10;
|
||||||
|
},
|
||||||
|
first_friend: async (userId) => {
|
||||||
|
const count = await prisma.friendRequest.count({
|
||||||
|
where: {
|
||||||
|
status: "ACCEPTED",
|
||||||
|
OR: [{ senderId: userId }, { receiverId: userId }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return count >= 1;
|
||||||
|
},
|
||||||
|
five_friends: async (userId) => {
|
||||||
|
const count = await prisma.friendRequest.count({
|
||||||
|
where: {
|
||||||
|
status: "ACCEPTED",
|
||||||
|
OR: [{ senderId: userId }, { receiverId: userId }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return count >= 5;
|
||||||
|
},
|
||||||
|
first_group: async (userId) => {
|
||||||
|
const count = await prisma.groupMember.count({ where: { userId } });
|
||||||
|
return count >= 1;
|
||||||
|
},
|
||||||
|
first_program_created: async (userId) => {
|
||||||
|
const count = await prisma.program.count({ where: { authorId: userId } });
|
||||||
|
return count >= 1;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Vérifie et attribue toutes les récompenses débloquées pour un utilisateur
|
||||||
|
export async function checkAndGrantRewards(userId: string): Promise<string[]> {
|
||||||
|
const allRewards = await prisma.reward.findMany();
|
||||||
|
const earned = await prisma.userReward.findMany({
|
||||||
|
where: { userId },
|
||||||
|
select: { rewardId: true },
|
||||||
|
});
|
||||||
|
const earnedIds = new Set(earned.map((r) => r.rewardId));
|
||||||
|
|
||||||
|
const newlyEarned: string[] = [];
|
||||||
|
|
||||||
|
for (const reward of allRewards) {
|
||||||
|
if (earnedIds.has(reward.id!)) continue;
|
||||||
|
|
||||||
|
const checker = CONDITIONS[reward.condition];
|
||||||
|
if (!checker) continue;
|
||||||
|
|
||||||
|
const unlocked = await checker(userId);
|
||||||
|
if (unlocked) {
|
||||||
|
await prisma.userReward.create({ data: { userId, rewardId: reward.id! } });
|
||||||
|
newlyEarned.push(reward.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newlyEarned;
|
||||||
|
}
|
||||||
14
backend/src/validators/exercise.validators.ts
Normal file
14
backend/src/validators/exercise.validators.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const createExerciseSchema = z.object({
|
||||||
|
name: z.string().min(2).max(50),
|
||||||
|
description: z.string().max(300).optional(),
|
||||||
|
difficulty: z.enum(["BEGINNER", "INTERMEDIATE", "ADVANCED"]).default("BEGINNER"),
|
||||||
|
muscleGroups: z.array(z.string()).min(1),
|
||||||
|
modelPath: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateExerciseSchema = createExerciseSchema.partial();
|
||||||
|
|
||||||
|
export type CreateExerciseInput = z.infer<typeof createExerciseSchema>;
|
||||||
|
export type UpdateExerciseInput = z.infer<typeof updateExerciseSchema>;
|
||||||
9
backend/src/validators/friend.validators.ts
Normal file
9
backend/src/validators/friend.validators.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const friendRequestSchema = z.object({
|
||||||
|
receiverId: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const friendResponseSchema = z.object({
|
||||||
|
status: z.enum(["ACCEPTED", "REJECTED"]),
|
||||||
|
});
|
||||||
15
backend/src/validators/group.validators.ts
Normal file
15
backend/src/validators/group.validators.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const createGroupSchema = z.object({
|
||||||
|
name: z.string().min(2).max(50),
|
||||||
|
description: z.string().max(300).optional(),
|
||||||
|
isPublic: z.boolean().default(false),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const inviteMemberSchema = z.object({
|
||||||
|
userId: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const addGroupProgramSchema = z.object({
|
||||||
|
programId: z.string().min(1),
|
||||||
|
});
|
||||||
21
backend/src/validators/history.validators.ts
Normal file
21
backend/src/validators/history.validators.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const startHistorySchema = z.object({
|
||||||
|
programId: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const addEntrySchema = z.object({
|
||||||
|
exerciseId: z.string().min(1),
|
||||||
|
sets: z.number().int().min(1),
|
||||||
|
reps: z.number().int().min(1).optional(),
|
||||||
|
weightKg: z.number().min(0).optional(),
|
||||||
|
durationSec: z.number().int().min(1).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const completeHistorySchema = z.object({
|
||||||
|
notes: z.string().max(500).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type StartHistoryInput = z.infer<typeof startHistorySchema>;
|
||||||
|
export type AddEntryInput = z.infer<typeof addEntrySchema>;
|
||||||
|
export type CompleteHistoryInput = z.infer<typeof completeHistorySchema>;
|
||||||
25
backend/src/validators/program.validators.ts
Normal file
25
backend/src/validators/program.validators.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const programExerciseSchema = z.object({
|
||||||
|
exerciseId: z.string().min(1),
|
||||||
|
sets: z.number().int().min(1),
|
||||||
|
reps: z.number().int().min(1).optional(),
|
||||||
|
durationSec: z.number().int().min(1).optional(),
|
||||||
|
order: z.number().int().min(0).default(0),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createProgramSchema = z.object({
|
||||||
|
name: z.string().min(2).max(50),
|
||||||
|
description: z.string().max(300).optional(),
|
||||||
|
isPublic: z.boolean().default(false),
|
||||||
|
exercises: z.array(programExerciseSchema).min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateProgramSchema = z.object({
|
||||||
|
name: z.string().min(2).max(50).optional(),
|
||||||
|
description: z.string().max(300).optional(),
|
||||||
|
isPublic: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreateProgramInput = z.infer<typeof createProgramSchema>;
|
||||||
|
export type UpdateProgramInput = z.infer<typeof updateProgramSchema>;
|
||||||
Reference in New Issue
Block a user