diff --git a/exercises/archi-separate-concerns/README.md b/exercises/archi-separate-concerns/README.md new file mode 100644 index 0000000..276a3bd --- /dev/null +++ b/exercises/archi-separate-concerns/README.md @@ -0,0 +1,15 @@ +Architecture Understanding & Separation of Concerns Exercise + +This archi-separate-component-front violates proper architecture principles by: +1. Mixing data fetching, business logic, and UI rendering all in one place +2. Performing multiple responsibilities within a single component +3. Lacking proper separation between API calls, data processing, and UI +4. Containing both data fetching and state updates for unrelated features + +Your task: Refactor the archi-separate-component-front by: +1. Separating API calls into a dedicated service +2. Breaking down the UI into smaller, focused components +3. Properly separating business logic from presentation +4. Creating a proper architecture with clear separation of concerns + +Be careful there could be a few mistakes in the code \ No newline at end of file diff --git a/exercises/archi-separate-concerns/api/controllers/orderController.js b/exercises/archi-separate-concerns/api/controllers/orderController.js new file mode 100644 index 0000000..3c4b8ff --- /dev/null +++ b/exercises/archi-separate-concerns/api/controllers/orderController.js @@ -0,0 +1,71 @@ +const express = require("express"); +const router = express.Router(); + +const dummyOrders = [ + { + id: "1", + customerName: "Alice", + customerEmail: "alice@example.com", + date: new Date(), + status: "processing", + total: 120, + }, + { + id: "2", + customerName: "Bob", + customerEmail: "bob@example.com", + date: new Date(), + status: "completed", + total: 80, + }, +]; + +// Get all orders +router.post("/list", async (req, res) => { + try { + return res.status(200).send({ ok: true, data: dummyOrders }); + } catch (error) { + res.status(500).send({ ok: false, message: "Internal error" }); + } +}); + +// Mark order as shipped +router.post("/mark-shipped", async (req, res) => { + try { + const { status } = req.body; + const orderId = req.params.id; + + const order = dummyOrders.find((o) => o.id === orderId); + if (!order) { + return res.status(404).send({ ok: false, message: "Order not found" }); + } + + if (status) { + order.status = status; + } + + return res.status(200).send({ ok: true, data: order }); + } catch (error) { + res + .status(500) + .send({ ok: false, message: "Failed to update order status" }); + } +}); + +// Send reminder email +router.post("/send-reminder", async (req, res) => { + try { + const { orderId } = req.body; + const order = dummyOrders.find((o) => o.id === orderId); + if (!order) + return res.status(404).send({ ok: false, message: "Order not found" }); + return res.status(200).send({ + ok: true, + data: { message: `Reminder sent for order #${orderId}` }, + }); + } catch (error) { + res.status(500).send({ ok: false, message: "Failed to send reminder" }); + } +}); + +module.exports = router; diff --git a/exercises/archi-separate-concerns/api/index.js b/exercises/archi-separate-concerns/api/index.js new file mode 100644 index 0000000..3ad834b --- /dev/null +++ b/exercises/archi-separate-concerns/api/index.js @@ -0,0 +1,15 @@ +// archi-separate-concern/back/index.js +const express = require("express"); +const cors = require("cors"); +const app = express(); +const port = 3001; +const ordersRoutes = require("./controllers/ordersController"); + +app.use(cors()); +app.use(express.json()); + +app.use("/api/orders", ordersRoutes); + +app.listen(port, () => + console.log(`API server running at http://localhost:${port}`) +); diff --git a/exercises/archi-separate-concerns.js b/exercises/archi-separate-concerns/app/scenes/archi-separate-concerns-front.js similarity index 59% rename from exercises/archi-separate-concerns.js rename to exercises/archi-separate-concerns/app/scenes/archi-separate-concerns-front.js index 34ffb73..b65aba8 100644 --- a/exercises/archi-separate-concerns.js +++ b/exercises/archi-separate-concerns/app/scenes/archi-separate-concerns-front.js @@ -12,127 +12,124 @@ // 3. Properly separating business logic from presentation // 4. Creating a proper architecture with clear separation of concerns -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect } from "react"; +import api from "../services/api"; const OrdersDashboard = () => { const [orders, setOrders] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [filter, setFilter] = useState('all'); - const [sortBy, setSortBy] = useState('date'); + const [filter, setFilter] = useState("all"); + const [sortBy, setSortBy] = useState("date"); const [totalRevenue, setTotalRevenue] = useState(0); const [customerEmails, setCustomerEmails] = useState([]); - + // Fetch orders useEffect(() => { - const fetchOrders = async () => { - try { - setLoading(true); - const response = await fetch('/api/orders'); - const data = await response.json(); - - if (!response.ok) { - throw new Error(data.message || 'Failed to fetch orders'); + try { + setLoading(true); + (async () => { + const { data, ok } = await api.post("/api/orders/list"); + if (ok) { + setOrders(data); + } else { + alert(data.message || "Erreur requête"); + setLoading(false); + return; + } + + let total = 0; + for (let i = 0; i < data.length; i++) { + total += data[i].total; + } + setTotalRevenue(total); + + const emails = []; + for (let i = 0; i < data.length; i++) { + if (!emails.includes(data[i].customerEmail)) { + emails.push(data[i].customerEmail); + } } - - setOrders(data); - - // Calculate total revenue - const revenue = data.reduce((sum, order) => sum + order.total, 0); - setTotalRevenue(revenue); - - // Extract customer emails - const emails = [...new Set(data.map(order => order.customerEmail))]; setCustomerEmails(emails); - - setLoading(false); - } catch (err) { - setError(err.message); + setLoading(false); - } - }; - - fetchOrders(); + })(); + } catch (e) { + setError("Erreur générale"); + setLoading(false); + } }, []); - + // Filter and sort orders - const filteredOrders = orders.filter(order => { - if (filter === 'all') return true; - if (filter === 'completed') return order.status === 'completed'; - if (filter === 'processing') return order.status === 'processing'; - if (filter === 'shipped') return order.status === 'shipped'; + const filteredOrders = orders.filter((order) => { + if (filter === "all") return true; + if (filter === "completed") return order.status === "completed"; + if (filter === "processing") return order.status === "processing"; + if (filter === "shipped") return order.status === "shipped"; return true; }); - + const sortedOrders = [...filteredOrders].sort((a, b) => { - if (sortBy === 'date') { + if (sortBy === "date") { return new Date(b.date) - new Date(a.date); - } else if (sortBy === 'total') { + } else if (sortBy === "total") { return b.total - a.total; } return 0; }); - + // Send reminder email const sendReminderEmail = async (orderId) => { try { - const response = await fetch(`/api/orders/${orderId}/send-reminder`, { - method: 'POST' + const { data, ok } = await api.post("/api/orders/send-reminder", { + orderId, }); - - const data = await response.json(); - - if (!response.ok) { - throw new Error(data.message || 'Failed to send reminder'); + if (ok) { + alert("Reminder email sent successfully"); + } else { + throw new Error(data.message || "Failed to send reminder"); } - - alert('Reminder email sent successfully'); } catch (err) { alert(`Error: ${err.message}`); } }; - + // Mark order as shipped const markAsShipped = async (orderId) => { try { - const response = await fetch(`/api/orders/${orderId}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ status: 'shipped' }) + const { data, ok } = await api.post("/api/orders/mark-shipped", { + orderId, }); - - const data = await response.json(); - - if (!response.ok) { - throw new Error(data.message || 'Failed to update order'); + if (ok) { + setOrders( + orders.map((order) => + order.id === orderId ? { ...order, status: "shipped" } : order + ) + ); + alert("Order marked as shipped"); + } else { + throw new Error(data.message || "Failed to update order"); } - - setOrders(orders.map(order => - order.id === orderId ? { ...order, status: 'shipped' } : order - )); - alert('Order marked as shipped'); } catch (err) { alert(`Error: ${err.message}`); } }; - + return (

Orders Dashboard

- +

Dashboard Summary

Total Orders: {orders.length}

Total Revenue: ${totalRevenue.toFixed(2)}

Unique Customers: {customerEmails.length}

- +
- - -
- + {loading ? (
@@ -163,41 +160,66 @@ const OrdersDashboard = () => { - - - - - - + + + + + + - {sortedOrders.map(order => ( + {sortedOrders.map((order) => ( - - + + - +
Order IDCustomerDateStatusTotalActions + Order ID + + Customer + + Date + + Status + + Total + + Actions +
#{order.id}{order.customerName}
{order.customerEmail}
+ #{order.id} + + {order.customerName} +
+ {order.customerEmail} +
{new Date(order.date).toLocaleDateString()} - {order.status} ${order.total.toFixed(2)} + ${order.total.toFixed(2)} + - - {order.status !== 'shipped' && ( -