feat: initial sbsports deployment setup
Add Coolify/Woodpecker CI config, .gitignore, and deployment scripts.
This commit is contained in:
45
server/db.js
Normal file
45
server/db.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import Database from "better-sqlite3";
|
||||
import { join, dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { mkdirSync } from "fs";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const DATA_DIR = process.env.DATA_DIR || join(__dirname, "../data");
|
||||
mkdirSync(DATA_DIR, { recursive: true });
|
||||
|
||||
const db = new Database(join(DATA_DIR, "db.sqlite"));
|
||||
|
||||
// Enable WAL for better concurrent read performance
|
||||
db.pragma("journal_mode = WAL");
|
||||
db.pragma("foreign_keys = ON");
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS students (
|
||||
id TEXT PRIMARY KEY,
|
||||
firstName TEXT NOT NULL,
|
||||
lastName TEXT NOT NULL,
|
||||
age INTEGER,
|
||||
sex TEXT,
|
||||
weight REAL,
|
||||
height REAL,
|
||||
imc REAL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activities (
|
||||
id TEXT PRIMARY KEY,
|
||||
studentId TEXT NOT NULL REFERENCES students(id) ON DELETE CASCADE,
|
||||
type TEXT,
|
||||
durationInput REAL,
|
||||
durationUnit TEXT,
|
||||
duration REAL,
|
||||
displayDuration TEXT,
|
||||
intensity TEXT,
|
||||
lifestyle TEXT,
|
||||
anxiety INTEGER DEFAULT 0,
|
||||
grade TEXT,
|
||||
date TEXT,
|
||||
createdAt TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
`);
|
||||
|
||||
export default db;
|
||||
150
server/index.js
Normal file
150
server/index.js
Normal file
@@ -0,0 +1,150 @@
|
||||
import express from "express";
|
||||
import { join, dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import db from "./db.js";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// Serve Vue build in production
|
||||
const distPath = join(__dirname, "../dist");
|
||||
app.use(express.static(distPath));
|
||||
|
||||
// ── Students ──────────────────────────────────────────────────────────────
|
||||
|
||||
app.get("/api/students", (_req, res) => {
|
||||
res.json(
|
||||
db.prepare("SELECT * FROM students ORDER BY lastName, firstName").all(),
|
||||
);
|
||||
});
|
||||
|
||||
app.post("/api/students", (req, res) => {
|
||||
const { id, firstName, lastName, age, sex, weight, height, imc } = req.body;
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO students (id, firstName, lastName, age, sex, weight, height, imc)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
).run(id, firstName, lastName, age, sex, weight, height, imc);
|
||||
res.status(201).json({ id });
|
||||
});
|
||||
|
||||
app.put("/api/students/:id", (req, res) => {
|
||||
const { firstName, lastName, age, sex, weight, height, imc } = req.body;
|
||||
const info = db
|
||||
.prepare(
|
||||
`
|
||||
UPDATE students SET firstName=?, lastName=?, age=?, sex=?, weight=?, height=?, imc=?
|
||||
WHERE id=?
|
||||
`,
|
||||
)
|
||||
.run(firstName, lastName, age, sex, weight, height, imc, req.params.id);
|
||||
if (info.changes === 0) return res.status(404).json({ error: "Not found" });
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
app.delete("/api/students/:id", (req, res) => {
|
||||
// CASCADE deletes linked activities automatically (FK ON DELETE CASCADE)
|
||||
db.prepare("DELETE FROM students WHERE id=?").run(req.params.id);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// ── Activities ────────────────────────────────────────────────────────────
|
||||
|
||||
app.get("/api/activities", (_req, res) => {
|
||||
res.json(
|
||||
db.prepare("SELECT * FROM activities ORDER BY createdAt DESC").all(),
|
||||
);
|
||||
});
|
||||
|
||||
app.post("/api/activities", (req, res) => {
|
||||
const {
|
||||
id,
|
||||
studentId,
|
||||
type,
|
||||
durationInput,
|
||||
durationUnit,
|
||||
duration,
|
||||
displayDuration,
|
||||
intensity,
|
||||
lifestyle,
|
||||
anxiety,
|
||||
grade,
|
||||
date,
|
||||
} = req.body;
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO activities
|
||||
(id, studentId, type, durationInput, durationUnit, duration, displayDuration, intensity, lifestyle, anxiety, grade, date)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
).run(
|
||||
id,
|
||||
studentId,
|
||||
type,
|
||||
durationInput,
|
||||
durationUnit,
|
||||
duration,
|
||||
displayDuration,
|
||||
intensity,
|
||||
lifestyle,
|
||||
anxiety ?? 0,
|
||||
grade,
|
||||
date,
|
||||
);
|
||||
res.status(201).json({ id });
|
||||
});
|
||||
|
||||
app.put("/api/activities/:id", (req, res) => {
|
||||
const {
|
||||
type,
|
||||
durationInput,
|
||||
durationUnit,
|
||||
duration,
|
||||
displayDuration,
|
||||
intensity,
|
||||
lifestyle,
|
||||
anxiety,
|
||||
grade,
|
||||
date,
|
||||
} = req.body;
|
||||
const info = db
|
||||
.prepare(
|
||||
`
|
||||
UPDATE activities
|
||||
SET type=?, durationInput=?, durationUnit=?, duration=?, displayDuration=?,
|
||||
intensity=?, lifestyle=?, anxiety=?, grade=?, date=?
|
||||
WHERE id=?
|
||||
`,
|
||||
)
|
||||
.run(
|
||||
type,
|
||||
durationInput,
|
||||
durationUnit,
|
||||
duration,
|
||||
displayDuration,
|
||||
intensity,
|
||||
lifestyle,
|
||||
anxiety ?? 0,
|
||||
grade,
|
||||
date,
|
||||
req.params.id,
|
||||
);
|
||||
if (info.changes === 0) return res.status(404).json({ error: "Not found" });
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
app.delete("/api/activities/:id", (req, res) => {
|
||||
db.prepare("DELETE FROM activities WHERE id=?").run(req.params.id);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// ── SPA fallback ──────────────────────────────────────────────────────────
|
||||
app.get("*", (_req, res) => {
|
||||
res.sendFile(join(distPath, "index.html"));
|
||||
});
|
||||
|
||||
app.listen(PORT, () => console.log(`SB Sports API running on :${PORT}`));
|
||||
Reference in New Issue
Block a user