mirror of
https://github.com/farcasclaudiu/learn-build-apps-copilot-agent.git
synced 2026-06-22 07:01:37 +03:00
feat: implement backend API with models and seed data for OctoFit application
This commit is contained in:
+93
@@ -0,0 +1,93 @@
|
|||||||
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const express_1 = __importDefault(require("express"));
|
||||||
|
const mongoose_1 = __importDefault(require("mongoose"));
|
||||||
|
const Activity_1 = require("./models/Activity");
|
||||||
|
const Leaderboard_1 = require("./models/Leaderboard");
|
||||||
|
const Team_1 = require("./models/Team");
|
||||||
|
const User_1 = require("./models/User");
|
||||||
|
const Workout_1 = require("./models/Workout");
|
||||||
|
const app = (0, express_1.default)();
|
||||||
|
const port = Number(process.env.PORT) || 8000;
|
||||||
|
const mongoUri = process.env.MONGO_URI || "mongodb://127.0.0.1:27017/octofit_db";
|
||||||
|
const codespaceName = process.env.CODESPACE_NAME;
|
||||||
|
const baseUrl = codespaceName
|
||||||
|
? `https://${codespaceName}-8000.app.github.dev`
|
||||||
|
: `http://localhost:${port}`;
|
||||||
|
app.use(express_1.default.json());
|
||||||
|
app.get("/health", (_req, res) => {
|
||||||
|
res.status(200).json({ status: "ok", baseUrl });
|
||||||
|
});
|
||||||
|
app.get("/api/users/", async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const items = await User_1.UserModel.find().sort({ createdAt: -1 }).lean();
|
||||||
|
res.status(200).json({ resource: "users", count: items.length, items });
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
res.status(500).json({ message: "Failed to fetch users", error });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
app.get("/api/teams/", async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const items = await Team_1.TeamModel.find()
|
||||||
|
.populate("captain", "username email")
|
||||||
|
.populate("members", "username email")
|
||||||
|
.sort({ createdAt: -1 })
|
||||||
|
.lean();
|
||||||
|
res.status(200).json({ resource: "teams", count: items.length, items });
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
res.status(500).json({ message: "Failed to fetch teams", error });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
app.get("/api/activities/", async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const items = await Activity_1.ActivityModel.find()
|
||||||
|
.populate("user", "username email")
|
||||||
|
.populate("team", "name city")
|
||||||
|
.sort({ completedAt: -1 })
|
||||||
|
.lean();
|
||||||
|
res.status(200).json({ resource: "activities", count: items.length, items });
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
res.status(500).json({ message: "Failed to fetch activities", error });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
app.get("/api/leaderboard/", async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const items = await Leaderboard_1.LeaderboardModel.find()
|
||||||
|
.populate("entries.user", "username email")
|
||||||
|
.populate("entries.team", "name city")
|
||||||
|
.sort({ generatedAt: -1 })
|
||||||
|
.lean();
|
||||||
|
res.status(200).json({ resource: "leaderboard", count: items.length, items });
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
res.status(500).json({ message: "Failed to fetch leaderboard", error });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
app.get("/api/workouts/", async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const items = await Workout_1.WorkoutModel.find().sort({ createdAt: -1 }).lean();
|
||||||
|
res.status(200).json({ resource: "workouts", count: items.length, items });
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
res.status(500).json({ message: "Failed to fetch workouts", error });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const start = async () => {
|
||||||
|
try {
|
||||||
|
await mongoose_1.default.connect(mongoUri);
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`OctoFit backend listening on ${baseUrl}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Failed to start backend:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
void start();
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.ActivityModel = void 0;
|
||||||
|
const mongoose_1 = require("mongoose");
|
||||||
|
const activitySchema = new mongoose_1.Schema({
|
||||||
|
user: { type: mongoose_1.Types.ObjectId, ref: "User", required: true },
|
||||||
|
team: { type: mongoose_1.Types.ObjectId, ref: "Team", required: true },
|
||||||
|
type: { type: String, required: true, trim: true },
|
||||||
|
durationMinutes: { type: Number, required: true, min: 1 },
|
||||||
|
caloriesBurned: { type: Number, required: true, min: 1 },
|
||||||
|
completedAt: { type: Date, required: true },
|
||||||
|
}, {
|
||||||
|
timestamps: true,
|
||||||
|
});
|
||||||
|
exports.ActivityModel = (0, mongoose_1.model)("Activity", activitySchema);
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.LeaderboardModel = void 0;
|
||||||
|
const mongoose_1 = require("mongoose");
|
||||||
|
const leaderboardEntrySchema = new mongoose_1.Schema({
|
||||||
|
user: { type: mongoose_1.Types.ObjectId, ref: "User", required: true },
|
||||||
|
team: { type: mongoose_1.Types.ObjectId, ref: "Team", required: true },
|
||||||
|
points: { type: Number, required: true, min: 0 },
|
||||||
|
rank: { type: Number, required: true, min: 1 },
|
||||||
|
}, { _id: false });
|
||||||
|
const leaderboardSchema = new mongoose_1.Schema({
|
||||||
|
period: { type: String, required: true, trim: true },
|
||||||
|
generatedAt: { type: Date, required: true },
|
||||||
|
entries: { type: [leaderboardEntrySchema], required: true },
|
||||||
|
}, {
|
||||||
|
timestamps: true,
|
||||||
|
});
|
||||||
|
exports.LeaderboardModel = (0, mongoose_1.model)("Leaderboard", leaderboardSchema);
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.TeamModel = void 0;
|
||||||
|
const mongoose_1 = require("mongoose");
|
||||||
|
const teamSchema = new mongoose_1.Schema({
|
||||||
|
name: { type: String, required: true, trim: true },
|
||||||
|
city: { type: String, required: true, trim: true },
|
||||||
|
captain: { type: mongoose_1.Types.ObjectId, ref: "User", required: true },
|
||||||
|
members: [{ type: mongoose_1.Types.ObjectId, ref: "User", required: true }],
|
||||||
|
}, {
|
||||||
|
timestamps: true,
|
||||||
|
});
|
||||||
|
exports.TeamModel = (0, mongoose_1.model)("Team", teamSchema);
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.UserModel = void 0;
|
||||||
|
const mongoose_1 = require("mongoose");
|
||||||
|
const userSchema = new mongoose_1.Schema({
|
||||||
|
username: { type: String, required: true, trim: true },
|
||||||
|
email: { type: String, required: true, unique: true, lowercase: true, trim: true },
|
||||||
|
age: { type: Number, required: true, min: 13 },
|
||||||
|
heightCm: { type: Number, required: true, min: 100 },
|
||||||
|
weightKg: { type: Number, required: true, min: 30 },
|
||||||
|
fitnessGoal: { type: String, required: true, trim: true },
|
||||||
|
}, {
|
||||||
|
timestamps: true,
|
||||||
|
});
|
||||||
|
exports.UserModel = (0, mongoose_1.model)("User", userSchema);
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.WorkoutModel = void 0;
|
||||||
|
const mongoose_1 = require("mongoose");
|
||||||
|
const workoutSchema = new mongoose_1.Schema({
|
||||||
|
title: { type: String, required: true, trim: true },
|
||||||
|
category: { type: String, required: true, trim: true },
|
||||||
|
difficulty: { type: String, required: true, trim: true },
|
||||||
|
durationMinutes: { type: Number, required: true, min: 1 },
|
||||||
|
equipment: { type: [String], required: true },
|
||||||
|
targetMuscles: { type: [String], required: true },
|
||||||
|
}, {
|
||||||
|
timestamps: true,
|
||||||
|
});
|
||||||
|
exports.WorkoutModel = (0, mongoose_1.model)("Workout", workoutSchema);
|
||||||
+161
@@ -0,0 +1,161 @@
|
|||||||
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const mongoose_1 = __importDefault(require("mongoose"));
|
||||||
|
const Activity_1 = require("../models/Activity");
|
||||||
|
const Leaderboard_1 = require("../models/Leaderboard");
|
||||||
|
const Team_1 = require("../models/Team");
|
||||||
|
const User_1 = require("../models/User");
|
||||||
|
const Workout_1 = require("../models/Workout");
|
||||||
|
const mongoUri = process.env.MONGO_URI || "mongodb://127.0.0.1:27017/octofit_db";
|
||||||
|
const seed = async () => {
|
||||||
|
try {
|
||||||
|
await mongoose_1.default.connect(mongoUri);
|
||||||
|
console.log("Seed the octofit_db database with test data");
|
||||||
|
await Promise.all([
|
||||||
|
Activity_1.ActivityModel.deleteMany({}),
|
||||||
|
Leaderboard_1.LeaderboardModel.deleteMany({}),
|
||||||
|
Team_1.TeamModel.deleteMany({}),
|
||||||
|
User_1.UserModel.deleteMany({}),
|
||||||
|
Workout_1.WorkoutModel.deleteMany({}),
|
||||||
|
]);
|
||||||
|
const users = await User_1.UserModel.insertMany([
|
||||||
|
{
|
||||||
|
username: "alex_runner",
|
||||||
|
email: "alex.runner@example.com",
|
||||||
|
age: 29,
|
||||||
|
heightCm: 178,
|
||||||
|
weightKg: 74,
|
||||||
|
fitnessGoal: "Half marathon in under 1h40",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "maya_lifter",
|
||||||
|
email: "maya.lifter@example.com",
|
||||||
|
age: 33,
|
||||||
|
heightCm: 165,
|
||||||
|
weightKg: 62,
|
||||||
|
fitnessGoal: "Increase deadlift max to 140kg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "liam_hiker",
|
||||||
|
email: "liam.hiker@example.com",
|
||||||
|
age: 26,
|
||||||
|
heightCm: 182,
|
||||||
|
weightKg: 79,
|
||||||
|
fitnessGoal: "Improve endurance for alpine trekking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "sofia_cycle",
|
||||||
|
email: "sofia.cycle@example.com",
|
||||||
|
age: 31,
|
||||||
|
heightCm: 170,
|
||||||
|
weightKg: 66,
|
||||||
|
fitnessGoal: "Ride a 100km gran fondo",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const teams = await Team_1.TeamModel.insertMany([
|
||||||
|
{
|
||||||
|
name: "Pulse Pacers",
|
||||||
|
city: "Cluj-Napoca",
|
||||||
|
captain: users[0]._id,
|
||||||
|
members: [users[0]._id, users[2]._id],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Iron Orbit",
|
||||||
|
city: "Bucharest",
|
||||||
|
captain: users[1]._id,
|
||||||
|
members: [users[1]._id, users[3]._id],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
await Workout_1.WorkoutModel.insertMany([
|
||||||
|
{
|
||||||
|
title: "Tempo Run 8K",
|
||||||
|
category: "Cardio",
|
||||||
|
difficulty: "Intermediate",
|
||||||
|
durationMinutes: 50,
|
||||||
|
equipment: ["Running shoes", "Sports watch"],
|
||||||
|
targetMuscles: ["Quadriceps", "Hamstrings", "Calves", "Core"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Barbell Strength Circuit",
|
||||||
|
category: "Strength",
|
||||||
|
difficulty: "Advanced",
|
||||||
|
durationMinutes: 60,
|
||||||
|
equipment: ["Barbell", "Weight plates", "Bench"],
|
||||||
|
targetMuscles: ["Glutes", "Back", "Chest", "Shoulders"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Mobility and Recovery Flow",
|
||||||
|
category: "Mobility",
|
||||||
|
difficulty: "Beginner",
|
||||||
|
durationMinutes: 30,
|
||||||
|
equipment: ["Yoga mat", "Resistance band"],
|
||||||
|
targetMuscles: ["Hips", "Lower back", "Shoulders"],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
await Activity_1.ActivityModel.insertMany([
|
||||||
|
{
|
||||||
|
user: users[0]._id,
|
||||||
|
team: teams[0]._id,
|
||||||
|
type: "Interval Run",
|
||||||
|
durationMinutes: 42,
|
||||||
|
caloriesBurned: 520,
|
||||||
|
completedAt: new Date("2026-06-18T06:40:00.000Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: users[1]._id,
|
||||||
|
team: teams[1]._id,
|
||||||
|
type: "Heavy Strength Session",
|
||||||
|
durationMinutes: 64,
|
||||||
|
caloriesBurned: 610,
|
||||||
|
completedAt: new Date("2026-06-18T18:20:00.000Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: users[2]._id,
|
||||||
|
team: teams[0]._id,
|
||||||
|
type: "Hill Hike",
|
||||||
|
durationMinutes: 95,
|
||||||
|
caloriesBurned: 780,
|
||||||
|
completedAt: new Date("2026-06-19T07:10:00.000Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: users[3]._id,
|
||||||
|
team: teams[1]._id,
|
||||||
|
type: "Road Cycling",
|
||||||
|
durationMinutes: 88,
|
||||||
|
caloriesBurned: 845,
|
||||||
|
completedAt: new Date("2026-06-19T16:05:00.000Z"),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
await Leaderboard_1.LeaderboardModel.insertMany([
|
||||||
|
{
|
||||||
|
period: "weekly-2026-W25",
|
||||||
|
generatedAt: new Date("2026-06-20T00:00:00.000Z"),
|
||||||
|
entries: [
|
||||||
|
{ user: users[3]._id, team: teams[1]._id, points: 1280, rank: 1 },
|
||||||
|
{ user: users[2]._id, team: teams[0]._id, points: 1225, rank: 2 },
|
||||||
|
{ user: users[1]._id, team: teams[1]._id, points: 1160, rank: 3 },
|
||||||
|
{ user: users[0]._id, team: teams[0]._id, points: 1095, rank: 4 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const counts = {
|
||||||
|
users: await User_1.UserModel.countDocuments(),
|
||||||
|
teams: await Team_1.TeamModel.countDocuments(),
|
||||||
|
activities: await Activity_1.ActivityModel.countDocuments(),
|
||||||
|
leaderboard: await Leaderboard_1.LeaderboardModel.countDocuments(),
|
||||||
|
workouts: await Workout_1.WorkoutModel.countDocuments(),
|
||||||
|
};
|
||||||
|
console.log("Seed complete", counts);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Seed failed", error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
await mongoose_1.default.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
void seed();
|
||||||
+5
-1
@@ -1,9 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "backend",
|
"name": "octofit-tracker-backend",
|
||||||
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
|
"name": "octofit-tracker-backend",
|
||||||
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"mongoose": "^9.7.1"
|
"mongoose": "^9.7.1"
|
||||||
@@ -11,6 +14,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
"@types/node": "^26.0.0",
|
"@types/node": "^26.0.0",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
"ts-node-dev": "^2.0.0",
|
"ts-node-dev": "^2.0.0",
|
||||||
"typescript": "^6.0.3"
|
"typescript": "^6.0.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
|
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
|
||||||
"build": "tsc -p tsconfig.json",
|
"build": "tsc -p tsconfig.json",
|
||||||
"start": "node dist/index.js"
|
"start": "node dist/index.js",
|
||||||
|
"seed": "ts-node src/scripts/seed.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
@@ -15,6 +16,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
"@types/node": "^26.0.0",
|
"@types/node": "^26.0.0",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
"ts-node-dev": "^2.0.0",
|
"ts-node-dev": "^2.0.0",
|
||||||
"typescript": "^6.0.3"
|
"typescript": "^6.0.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,87 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import mongoose from "mongoose";
|
import mongoose from "mongoose";
|
||||||
|
import { ActivityModel } from "./models/Activity";
|
||||||
|
import { LeaderboardModel } from "./models/Leaderboard";
|
||||||
|
import { TeamModel } from "./models/Team";
|
||||||
|
import { UserModel } from "./models/User";
|
||||||
|
import { WorkoutModel } from "./models/Workout";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = Number(process.env.PORT) || 8000;
|
const port = Number(process.env.PORT) || 8000;
|
||||||
const mongoUri = process.env.MONGO_URI || "mongodb://127.0.0.1:27017/octofit_tracker";
|
const mongoUri = process.env.MONGO_URI || "mongodb://127.0.0.1:27017/octofit_db";
|
||||||
|
const codespaceName = process.env.CODESPACE_NAME;
|
||||||
|
const baseUrl = codespaceName
|
||||||
|
? `https://${codespaceName}-8000.app.github.dev`
|
||||||
|
: `http://localhost:${port}`;
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
app.get("/health", (_req, res) => {
|
app.get("/health", (_req, res) => {
|
||||||
res.status(200).json({ status: "ok" });
|
res.status(200).json({ status: "ok", baseUrl });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/users/", async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const items = await UserModel.find().sort({ createdAt: -1 }).lean();
|
||||||
|
res.status(200).json({ resource: "users", count: items.length, items });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ message: "Failed to fetch users", error });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/teams/", async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const items = await TeamModel.find()
|
||||||
|
.populate("captain", "username email")
|
||||||
|
.populate("members", "username email")
|
||||||
|
.sort({ createdAt: -1 })
|
||||||
|
.lean();
|
||||||
|
res.status(200).json({ resource: "teams", count: items.length, items });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ message: "Failed to fetch teams", error });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/activities/", async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const items = await ActivityModel.find()
|
||||||
|
.populate("user", "username email")
|
||||||
|
.populate("team", "name city")
|
||||||
|
.sort({ completedAt: -1 })
|
||||||
|
.lean();
|
||||||
|
res.status(200).json({ resource: "activities", count: items.length, items });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ message: "Failed to fetch activities", error });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/leaderboard/", async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const items = await LeaderboardModel.find()
|
||||||
|
.populate("entries.user", "username email")
|
||||||
|
.populate("entries.team", "name city")
|
||||||
|
.sort({ generatedAt: -1 })
|
||||||
|
.lean();
|
||||||
|
res.status(200).json({ resource: "leaderboard", count: items.length, items });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ message: "Failed to fetch leaderboard", error });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/workouts/", async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const items = await WorkoutModel.find().sort({ createdAt: -1 }).lean();
|
||||||
|
res.status(200).json({ resource: "workouts", count: items.length, items });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ message: "Failed to fetch workouts", error });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const start = async () => {
|
const start = async () => {
|
||||||
try {
|
try {
|
||||||
await mongoose.connect(mongoUri);
|
await mongoose.connect(mongoUri);
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`OctoFit backend listening on http://localhost:${port}`);
|
console.log(`OctoFit backend listening on ${baseUrl}`);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to start backend:", error);
|
console.error("Failed to start backend:", error);
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Schema, model, type InferSchemaType, Types } from "mongoose";
|
||||||
|
|
||||||
|
const activitySchema = new Schema(
|
||||||
|
{
|
||||||
|
user: { type: Types.ObjectId, ref: "User", required: true },
|
||||||
|
team: { type: Types.ObjectId, ref: "Team", required: true },
|
||||||
|
type: { type: String, required: true, trim: true },
|
||||||
|
durationMinutes: { type: Number, required: true, min: 1 },
|
||||||
|
caloriesBurned: { type: Number, required: true, min: 1 },
|
||||||
|
completedAt: { type: Date, required: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ActivityDocument = InferSchemaType<typeof activitySchema>;
|
||||||
|
export const ActivityModel = model("Activity", activitySchema);
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Schema, model, type InferSchemaType, Types } from "mongoose";
|
||||||
|
|
||||||
|
const leaderboardEntrySchema = new Schema(
|
||||||
|
{
|
||||||
|
user: { type: Types.ObjectId, ref: "User", required: true },
|
||||||
|
team: { type: Types.ObjectId, ref: "Team", required: true },
|
||||||
|
points: { type: Number, required: true, min: 0 },
|
||||||
|
rank: { type: Number, required: true, min: 1 },
|
||||||
|
},
|
||||||
|
{ _id: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
const leaderboardSchema = new Schema(
|
||||||
|
{
|
||||||
|
period: { type: String, required: true, trim: true },
|
||||||
|
generatedAt: { type: Date, required: true },
|
||||||
|
entries: { type: [leaderboardEntrySchema], required: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export type LeaderboardDocument = InferSchemaType<typeof leaderboardSchema>;
|
||||||
|
export const LeaderboardModel = model("Leaderboard", leaderboardSchema);
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Schema, model, type InferSchemaType, Types } from "mongoose";
|
||||||
|
|
||||||
|
const teamSchema = new Schema(
|
||||||
|
{
|
||||||
|
name: { type: String, required: true, trim: true },
|
||||||
|
city: { type: String, required: true, trim: true },
|
||||||
|
captain: { type: Types.ObjectId, ref: "User", required: true },
|
||||||
|
members: [{ type: Types.ObjectId, ref: "User", required: true }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export type TeamDocument = InferSchemaType<typeof teamSchema>;
|
||||||
|
export const TeamModel = model("Team", teamSchema);
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Schema, model, type InferSchemaType } from "mongoose";
|
||||||
|
|
||||||
|
const userSchema = new Schema(
|
||||||
|
{
|
||||||
|
username: { type: String, required: true, trim: true },
|
||||||
|
email: { type: String, required: true, unique: true, lowercase: true, trim: true },
|
||||||
|
age: { type: Number, required: true, min: 13 },
|
||||||
|
heightCm: { type: Number, required: true, min: 100 },
|
||||||
|
weightKg: { type: Number, required: true, min: 30 },
|
||||||
|
fitnessGoal: { type: String, required: true, trim: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export type UserDocument = InferSchemaType<typeof userSchema>;
|
||||||
|
export const UserModel = model("User", userSchema);
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Schema, model, type InferSchemaType } from "mongoose";
|
||||||
|
|
||||||
|
const workoutSchema = new Schema(
|
||||||
|
{
|
||||||
|
title: { type: String, required: true, trim: true },
|
||||||
|
category: { type: String, required: true, trim: true },
|
||||||
|
difficulty: { type: String, required: true, trim: true },
|
||||||
|
durationMinutes: { type: Number, required: true, min: 1 },
|
||||||
|
equipment: { type: [String], required: true },
|
||||||
|
targetMuscles: { type: [String], required: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export type WorkoutDocument = InferSchemaType<typeof workoutSchema>;
|
||||||
|
export const WorkoutModel = model("Workout", workoutSchema);
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import mongoose from "mongoose";
|
||||||
|
import { ActivityModel } from "../models/Activity";
|
||||||
|
import { LeaderboardModel } from "../models/Leaderboard";
|
||||||
|
import { TeamModel } from "../models/Team";
|
||||||
|
import { UserModel } from "../models/User";
|
||||||
|
import { WorkoutModel } from "../models/Workout";
|
||||||
|
|
||||||
|
const mongoUri = process.env.MONGO_URI || "mongodb://127.0.0.1:27017/octofit_db";
|
||||||
|
|
||||||
|
const seed = async () => {
|
||||||
|
try {
|
||||||
|
await mongoose.connect(mongoUri);
|
||||||
|
|
||||||
|
console.log("Seed the octofit_db database with test data");
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
ActivityModel.deleteMany({}),
|
||||||
|
LeaderboardModel.deleteMany({}),
|
||||||
|
TeamModel.deleteMany({}),
|
||||||
|
UserModel.deleteMany({}),
|
||||||
|
WorkoutModel.deleteMany({}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const users = await UserModel.insertMany([
|
||||||
|
{
|
||||||
|
username: "alex_runner",
|
||||||
|
email: "alex.runner@example.com",
|
||||||
|
age: 29,
|
||||||
|
heightCm: 178,
|
||||||
|
weightKg: 74,
|
||||||
|
fitnessGoal: "Half marathon in under 1h40",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "maya_lifter",
|
||||||
|
email: "maya.lifter@example.com",
|
||||||
|
age: 33,
|
||||||
|
heightCm: 165,
|
||||||
|
weightKg: 62,
|
||||||
|
fitnessGoal: "Increase deadlift max to 140kg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "liam_hiker",
|
||||||
|
email: "liam.hiker@example.com",
|
||||||
|
age: 26,
|
||||||
|
heightCm: 182,
|
||||||
|
weightKg: 79,
|
||||||
|
fitnessGoal: "Improve endurance for alpine trekking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "sofia_cycle",
|
||||||
|
email: "sofia.cycle@example.com",
|
||||||
|
age: 31,
|
||||||
|
heightCm: 170,
|
||||||
|
weightKg: 66,
|
||||||
|
fitnessGoal: "Ride a 100km gran fondo",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const teams = await TeamModel.insertMany([
|
||||||
|
{
|
||||||
|
name: "Pulse Pacers",
|
||||||
|
city: "Cluj-Napoca",
|
||||||
|
captain: users[0]._id,
|
||||||
|
members: [users[0]._id, users[2]._id],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Iron Orbit",
|
||||||
|
city: "Bucharest",
|
||||||
|
captain: users[1]._id,
|
||||||
|
members: [users[1]._id, users[3]._id],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await WorkoutModel.insertMany([
|
||||||
|
{
|
||||||
|
title: "Tempo Run 8K",
|
||||||
|
category: "Cardio",
|
||||||
|
difficulty: "Intermediate",
|
||||||
|
durationMinutes: 50,
|
||||||
|
equipment: ["Running shoes", "Sports watch"],
|
||||||
|
targetMuscles: ["Quadriceps", "Hamstrings", "Calves", "Core"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Barbell Strength Circuit",
|
||||||
|
category: "Strength",
|
||||||
|
difficulty: "Advanced",
|
||||||
|
durationMinutes: 60,
|
||||||
|
equipment: ["Barbell", "Weight plates", "Bench"],
|
||||||
|
targetMuscles: ["Glutes", "Back", "Chest", "Shoulders"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Mobility and Recovery Flow",
|
||||||
|
category: "Mobility",
|
||||||
|
difficulty: "Beginner",
|
||||||
|
durationMinutes: 30,
|
||||||
|
equipment: ["Yoga mat", "Resistance band"],
|
||||||
|
targetMuscles: ["Hips", "Lower back", "Shoulders"],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await ActivityModel.insertMany([
|
||||||
|
{
|
||||||
|
user: users[0]._id,
|
||||||
|
team: teams[0]._id,
|
||||||
|
type: "Interval Run",
|
||||||
|
durationMinutes: 42,
|
||||||
|
caloriesBurned: 520,
|
||||||
|
completedAt: new Date("2026-06-18T06:40:00.000Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: users[1]._id,
|
||||||
|
team: teams[1]._id,
|
||||||
|
type: "Heavy Strength Session",
|
||||||
|
durationMinutes: 64,
|
||||||
|
caloriesBurned: 610,
|
||||||
|
completedAt: new Date("2026-06-18T18:20:00.000Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: users[2]._id,
|
||||||
|
team: teams[0]._id,
|
||||||
|
type: "Hill Hike",
|
||||||
|
durationMinutes: 95,
|
||||||
|
caloriesBurned: 780,
|
||||||
|
completedAt: new Date("2026-06-19T07:10:00.000Z"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: users[3]._id,
|
||||||
|
team: teams[1]._id,
|
||||||
|
type: "Road Cycling",
|
||||||
|
durationMinutes: 88,
|
||||||
|
caloriesBurned: 845,
|
||||||
|
completedAt: new Date("2026-06-19T16:05:00.000Z"),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await LeaderboardModel.insertMany([
|
||||||
|
{
|
||||||
|
period: "weekly-2026-W25",
|
||||||
|
generatedAt: new Date("2026-06-20T00:00:00.000Z"),
|
||||||
|
entries: [
|
||||||
|
{ user: users[3]._id, team: teams[1]._id, points: 1280, rank: 1 },
|
||||||
|
{ user: users[2]._id, team: teams[0]._id, points: 1225, rank: 2 },
|
||||||
|
{ user: users[1]._id, team: teams[1]._id, points: 1160, rank: 3 },
|
||||||
|
{ user: users[0]._id, team: teams[0]._id, points: 1095, rank: 4 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const counts = {
|
||||||
|
users: await UserModel.countDocuments(),
|
||||||
|
teams: await TeamModel.countDocuments(),
|
||||||
|
activities: await ActivityModel.countDocuments(),
|
||||||
|
leaderboard: await LeaderboardModel.countDocuments(),
|
||||||
|
workouts: await WorkoutModel.countDocuments(),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("Seed complete", counts);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Seed failed", error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
} finally {
|
||||||
|
await mongoose.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void seed();
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"module": "Node16",
|
"module": "Node16",
|
||||||
"moduleResolution": "Node16",
|
"moduleResolution": "Node16",
|
||||||
|
"types": ["node"],
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user