Skip to content
Open
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 @@ -5,6 +5,7 @@ node_modules
.env.development.local
.env.test.local
.env.production.local
backend/.env

build

Expand Down
13 changes: 13 additions & 0 deletions backend/middlewares/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import jwt from "jsonwebtoken";

export default function auth(req, res, next) {
const h = req.headers.authorization || "";
const token = h.startsWith("Bearer ") ? h.slice(7) : null;
if (!token) return res.status(401).json({ error: "No token" });
try {
req.user = jwt.verify(token, process.env.JWT_SECRET); // { id, iat, exp }
next();
} catch {
res.status(401).json({ error: "Invalid token" });
}
}
16 changes: 16 additions & 0 deletions backend/models/Item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import mongoose from "mongoose";

const itemSchema = new mongoose.Schema(
{
userId: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true, index: true },
childId: { type: mongoose.Schema.Types.ObjectId, ref: "Kid", required: true },
type: { type: String, enum: ["jacket", "pants", "boots", "hat", "top", "gloves", "other"], default: "other" },
size: { type: String, required: true },
season: { type: String, enum: ["winter", "spring", "summer", "autumn", "all"], default: "all" },
status: { type: String, enum: ["current", "needed", "stored", "to-sell"], default: "current" },
notes: String
},
{ timestamps: true }
);

export default mongoose.model("Item", itemSchema);
16 changes: 16 additions & 0 deletions backend/models/Kid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import mongoose from "mongoose";

const kidSchema = new mongoose.Schema({
name: { type: String, required: true },
birthdate: { type: Date, required: true },

// optional: male/female/other
sex: { type: String, enum: ["boy", "girl", "other"], default: "other" },

// optional: current height in cm
height: { type: Number, min: 40, max: 200 },

owner: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
}, { timestamps: true });

export default mongoose.model("Kid", kidSchema);
11 changes: 11 additions & 0 deletions backend/models/User.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import mongoose from "mongoose";

const userSchema = new mongoose.Schema(
{
email: { type: String, required: true, unique: true, trim: true },
passwordHash: { type: String, required: true }
},
{ timestamps: true }
);

export default mongoose.model("User", userSchema);
18 changes: 14 additions & 4 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,19 @@
"@babel/core": "^7.17.9",
"@babel/node": "^7.16.8",
"@babel/preset-env": "^7.16.11",
"bcrypt": "^6.0.0",
"cors": "^2.8.5",
"express": "^4.17.3",
"mongoose": "^8.4.0",
"nodemon": "^3.0.1"
"dotenv": "^17.2.1",
"express": "^4.21.2",
"jsonwebtoken": "^9.0.2",
"mongodb": "^6.18.0",
"mongoose": "^8.17.2",
"morgan": "^1.10.1",
"react-hook-form": "^7.62.0",
"react-router-dom": "^7.8.1",
"zustand": "^5.0.8"
},
"devDependencies": {
"nodemon": "^3.1.10"
}
}
}
57 changes: 57 additions & 0 deletions backend/routes/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import express from "express";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import mongoose from "mongoose";
import User from "../models/User.js";

const router = express.Router();
const sign = (id) => jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: "7d" });

// POST /auth/register
router.post("/register", async (req, res) => {
try {
const { email, password } = req.body;

// basic validation
if (!email || !password) return res.status(400).json({ error: "Email and password required" });
if (password.length < 6) return res.status(400).json({ error: "Password must be ≥ 6 chars" });

// unique email
const exists = await User.findOne({ email: email.toLowerCase().trim() });
if (exists) return res.status(409).json({ error: "Email already registered" });

// hash & save
const passwordHash = await bcrypt.hash(password, 10);
const user = await User.create({ email: email.toLowerCase().trim(), passwordHash });

// return a token immediately (so the FE can log in)
return res.status(201).json({ token: sign(user._id) });
} catch (e) {
// handle duplicate key race condition
if (e.code === 11000) return res.status(409).json({ error: "Email already registered" });
console.error(e);
return res.status(500).json({ error: "Register failed" });
}
});

// POST /auth/login
router.post("/login", async (req, res) => {
try {
const { email, password } = req.body;
if (!email || !password) return res.status(400).json({ error: "Email and password required" });

const user = await User.findOne({ email: email.toLowerCase().trim() });
if (!user) return res.status(401).json({ error: "Invalid credentials" });

const ok = await bcrypt.compare(password, user.passwordHash);
if (!ok) return res.status(401).json({ error: "Invalid credentials" });

return res.json({ token: sign(user._id) });
} catch (e) {
console.error(e);
return res.status(500).json({ error: "Login failed" });
}
});


export default router;
38 changes: 38 additions & 0 deletions backend/routes/items.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import express from "express";
import Item from "../models/Item.js";
import auth from "../middlewares/auth.js";

const router = express.Router();

// list items for user
router.get("/", auth, async (req, res) => {
const items = await Item.find({ userId: req.user.id }).lean();
res.json(items);
});

// create
router.post("/", auth, async (req, res) => {
const { childId, type, size, season, status, notes } = req.body;
const item = await Item.create({ userId: req.user.id, childId, type, size, season, status, notes });
res.status(201).json(item);
});

// update
router.patch("/:id", auth, async (req, res) => {
const item = await Item.findOneAndUpdate(
{ _id: req.params.id, userId: req.user.id },
req.body,
{ new: true }
);
if (!item) return res.status(404).json({ error: "Not found" });
res.json(item);
});

// delete
router.delete("/:id", auth, async (req, res) => {
const ok = await Item.deleteOne({ _id: req.params.id, userId: req.user.id });
if (!ok.deletedCount) return res.status(404).json({ error: "Not found" });
res.json({ ok: true });
});

export default router;
28 changes: 28 additions & 0 deletions backend/routes/kids.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import express from "express";
import auth from "../middlewares/auth.js";
import Kid from "../models/Kid.js";

const router = express.Router();

router.get("/", auth, async (req, res) => {
try {
const kids = await Kid.find({ owner: req.user.id }).lean();
res.json(kids);
} catch (e) {
console.error(e);
res.status(500).json({ error: "Failed to fetch kids" });
}
});

router.post("/", auth, async (req, res) => {
try {
const { name, birthdate } = req.body;
const kid = await Kid.create({ name, birthdate, owner: req.user.id });
res.status(201).json(kid);
} catch (e) {
console.error(e);
res.status(500).json({ error: "Failed to create kid" });
}
});

export default router;
58 changes: 49 additions & 9 deletions backend/server.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,62 @@
import dotenv from "dotenv";
import express from "express";
import cors from "cors";
import mongoose from "mongoose";
import morgan from "morgan";
import authRouter from "./routes/auth.js";
import "./models/User.js";
import kidsRouter from "./routes/kids.js";
import itemsRouter from "./routes/items.js";
import auth from "./middlewares/auth.js";

const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/final-project";
mongoose.connect(mongoUrl);
mongoose.Promise = Promise;

const port = process.env.PORT || 8080;

dotenv.config(); // 1) Load backend/.env

const app = express();
const PORT = process.env.PORT || 8081;
const MONGO_URL = process.env.MONGO_URL || "mongodb://localhost/final-project";
//local dev site and hosted frontend
const allowed = [process.env.FRONTEND_URL, "http://localhost:5173"].filter(Boolean);
app.use(cors({ origin: allowed }));

app.use(cors());
// 2) Middleware
//app.use(cors());
app.use(express.json());
app.use(morgan("dev")); // handy request logs during dev

app.use("/auth", authRouter);

app.use("/kids", kidsRouter);

app.use("/items", itemsRouter);


app.get("/", (req, res) => {

// 3) Mongo connect (with clear logs)
mongoose
.connect(MONGO_URL)
.then(() => console.log("✅ MongoDB connected"))
.catch((err) => console.error("❌ MongoDB connection error:", err.message));

// Helper: human-readable DB status
const dbStatus = () => {
const states = ["disconnected", "connected", "connecting", "disconnecting"];
return states[mongoose.connection.readyState] || "unknown";
};

// 4) Routes
app.get("/", (_req, res) => {
res.send("Hello Technigo!");
});

// Start the server
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
app.get("/health", (_req, res) => {
res.json({ ok: true, db: dbStatus() });
});

app.get("/me", auth, (req, res) => res.json({ userId: req.user.id }));

// 5) Start the server
app.listen(PORT, () => {
console.log(`🚀 Server running on http://localhost:${PORT}`);
});
27 changes: 16 additions & 11 deletions frontend/index.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Technigo React Vite Boiler Plate</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description"
content="LitenLeap keeps you ahead of kids' clothing needs with size prediction and seasonal planning." />
<title>LitenLeap</title>
</head>

<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>

</html>
6 changes: 5 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-hook-form": "^7.62.0",
"react-router-dom": "^7.8.1",
"recharts": "^3.1.2",
"zustand": "^5.0.8"
},
"devDependencies": {
"@types/react": "^18.2.15",
Expand Down
1 change: 1 addition & 0 deletions frontend/public/_redirects
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/* /index.html 200
34 changes: 30 additions & 4 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,34 @@
export const App = () => {
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { useAuth } from "./store/auth";
import Login from "./pages/Login";
import Register from "./pages/Register";
import Dashboard from "./pages/Dashboard";
import Kids from "./pages/Kids";
import Items from "./pages/Items";
import Nav from "./components/Nav";


const Protected = ({ children }) => {
const token = useAuth((s) => s.token);
return token ? children : <Navigate to="/login" replace />;
};

const App = () => {
return (
<>
<h1>Welcome to Final Project!</h1>
</>
<BrowserRouter>
<Nav /> {/* 👈 always visible */}
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/" element={<Protected><Dashboard /></Protected>} />
<Route path="*" element={<Navigate to="/" />} />
<Route path="/kids" element={<Protected><Kids /></Protected>} />
<Route path="/items" element={<Protected><Items /></Protected>} />
<Route path="*" element={<Navigate to="/" />} />

</Routes>
</BrowserRouter>
);
};

export default App;
Loading