Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ coverage/
.vscode/
.idea/
package-lock.json
apps/backend/data/
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ COPY apps/backend/package*.json ./apps/backend/
RUN cd apps/backend && npm install --omit=dev
COPY --from=backend /app/backend/dist ./apps/backend/dist
COPY --from=frontend /app/frontend/dist ./apps/frontend/dist
RUN mkdir -p /app/apps/backend/data && chown -R node:node /app/apps/backend/data
ENV NODE_ENV=production
ENV PORT=3001
EXPOSE 3001
Expand Down
2 changes: 2 additions & 0 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
"test": "vitest run"
},
"dependencies": {
"better-sqlite3": "^9.4.4",
"express": "^4.18.2"
},
"devDependencies": {
"@eslint/js": "^8.56.0",
"@types/better-sqlite3": "^7.6.6",
"@types/express": "^4.17.21",
"@types/node": "^20.10.6",
"eslint": "^8.56.0",
Expand Down
36 changes: 32 additions & 4 deletions apps/backend/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,35 @@
import { describe, it, expect } from "vitest";
import { describe, expect, it } from "vitest";

describe("Backend", () => {
it("should pass placeholder test", () => {
expect(true).toBe(true);
import { computeAverage, normalizeComment, validateFeedback } from "./roti-service.js";

describe("roti-service", () => {
it("rejects rating below 1", () => {
const result = validateFeedback({ rating: 0, comment: "ok" });
expect(result.ok).toBe(false);
});

it("rejects rating above 5", () => {
const result = validateFeedback({ rating: 6, comment: "ok" });
expect(result.ok).toBe(false);
});

it("requires comment for rating 3", () => {
const result = validateFeedback({ rating: 3, comment: "" });
expect(result.ok).toBe(false);
});

it("accepts rating 4 without comment", () => {
const result = validateFeedback({ rating: 4, comment: "" });
expect(result.ok).toBe(true);
});

it("normalizes blank comment to null", () => {
const normalized = normalizeComment(" ");
expect(normalized).toBe(null);
});

it("computes average to one decimal", () => {
const average = computeAverage([5, 4, 4]);
expect(average).toBe(4.3);
});
});
65 changes: 59 additions & 6 deletions apps/backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,73 @@
import express, { Request, Response } from "express";
import path from "path";
import { fileURLToPath } from "url";

import express, { Request, Response } from "express";

import { getDb } from "./roti-db.js";
import { computeAverage, normalizeComment, validateFeedback } from "./roti-service.js";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const app = express();
const PORT = process.env.PORT || 3001;
const db = getDb();

interface FeedbackRow {
id: number;
rating: number;
comment: string | null;
created_at: string;
}

app.use(express.json());

// API routes
app.get("/api/health", (_req: Request, res: Response) => {
res.json({ status: "ok", timestamp: new Date().toISOString() });
app.get("/api/roti", (_req: Request, res: Response) => {
try {
const rows = db
.prepare("SELECT id, rating, comment, created_at FROM feedbacks ORDER BY created_at DESC")
.all() as FeedbackRow[];

const feedbacks = rows.map((row) => ({
id: row.id,
rating: row.rating,
comment: row.comment ?? null,
createdAt: row.created_at,
}));

const average = computeAverage(feedbacks.map((feedback) => feedback.rating));

res.json({ average, feedbacks });
} catch (error) {
console.error("Failed to fetch feedbacks:", error);
res.status(500).json({ error: "Internal server error" });
}
});

app.get("/api/hello", (_req: Request, res: Response) => {
res.json({ message: "Hello from backend!", env: process.env.NODE_ENV || "development" });
app.post("/api/roti", (req: Request, res: Response) => {
const rating = Number(req.body?.rating);
const comment = normalizeComment(req.body?.comment);
const validation = validateFeedback({ rating, comment });

if (!validation.ok) {
res.status(400).json({ error: validation.error });
return;
}

try {
const createdAt = new Date().toISOString();
const result = db
.prepare("INSERT INTO feedbacks (rating, comment, created_at) VALUES (?, ?, ?)")
.run(rating, comment, createdAt);

res.status(201).json({
id: Number(result.lastInsertRowid),
rating,
comment,
createdAt,
});
} catch (error) {
console.error("Failed to save feedback:", error);
res.status(500).json({ error: "Internal server error" });
}
});

// Serve frontend static files
Expand Down
26 changes: 26 additions & 0 deletions apps/backend/src/roti-db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";

import Database from "better-sqlite3";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const dataDir = path.join(__dirname, "..", "data");

fs.mkdirSync(dataDir, { recursive: true });

const dbPath = path.join(dataDir, "roti.db");
const db = new Database(dbPath);

db.exec(`
CREATE TABLE IF NOT EXISTS feedbacks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5),
comment TEXT,
created_at TEXT NOT NULL
)
`);

export function getDb() {
return db;
}
41 changes: 41 additions & 0 deletions apps/backend/src/roti-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export interface FeedbackInput {
rating: number;
comment?: string | null;
}

export interface Feedback {
id: number;
rating: number;
comment: string | null;
createdAt: string;
}

export interface ValidationResult {
ok: boolean;
error?: string;
}

export function validateFeedback(input: FeedbackInput): ValidationResult {
if (!Number.isInteger(input.rating) || input.rating < 1 || input.rating > 5) {
return { ok: false, error: "La note doit etre entre 1 et 5." };
}

const trimmedComment = input.comment?.trim() ?? "";
if (input.rating <= 3 && trimmedComment.length === 0) {
return { ok: false, error: "Le commentaire est obligatoire pour une note de 1 a 3." };
}

return { ok: true };
}

export function normalizeComment(comment?: string | null) {
const trimmedComment = comment?.trim();
return trimmedComment && trimmedComment.length > 0 ? trimmedComment : null;
}

export function computeAverage(ratings: number[]) {
if (ratings.length === 0) return 0;
const total = ratings.reduce((sum, rating) => sum + rating, 0);
const average = total / ratings.length;
return Math.round(average * 10) / 10;
}
10 changes: 8 additions & 2 deletions apps/frontend/index.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
<!doctype html>
<html lang="en">
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Live Code</title>
<title>ROTI Live Feedback</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600&family=Space+Grotesk:wght@500;600;700&display=swap"
rel="stylesheet"
/>
</head>
<body>
<div id="root"></div>
Expand Down
3 changes: 3 additions & 0 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.17",
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"postcss": "^8.4.35",
"tailwindcss": "^3.4.3",
"typescript": "^5.2.2",
"typescript-eslint": "^7.0.0",
"vite": "^5.0.8"
Expand Down
6 changes: 6 additions & 0 deletions apps/frontend/postcss.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
49 changes: 0 additions & 49 deletions apps/frontend/src/App.css

This file was deleted.

Loading