Initial commit
This commit is contained in:
14
backend/.dockerignore
Normal file
14
backend/.dockerignore
Normal file
@@ -0,0 +1,14 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
.npmrc
|
||||
.env
|
||||
.env.*
|
||||
.git
|
||||
.gitignore
|
||||
Dockerfile
|
||||
*.md
|
||||
coverage
|
||||
tests
|
||||
docs
|
||||
build
|
||||
dist
|
||||
13
backend/Dockerfile
Normal file
13
backend/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json ./
|
||||
|
||||
RUN npm install --omit=dev
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 4000
|
||||
|
||||
CMD ["npm", "start"]
|
||||
13
backend/config/api.js
Normal file
13
backend/config/api.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const weatherBaseUrl = "https://api.openweathermap.org/data/2.5";
|
||||
|
||||
const config = {
|
||||
weather: {
|
||||
baseUrl: weatherBaseUrl,
|
||||
apiKey: process.env.OPENWEATHER_API_KEY || "",
|
||||
city: process.env.WEATHER_CITY || "Seoul",
|
||||
units: process.env.WEATHER_UNITS || "metric",
|
||||
language: process.env.WEATHER_LANG || "en",
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
14
backend/config/db.js
Normal file
14
backend/config/db.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const connectDb = async () => {
|
||||
const mongoUri = process.env.MONGODB_URI;
|
||||
if (!mongoUri) {
|
||||
throw new Error("MONGODB_URI is not set");
|
||||
}
|
||||
|
||||
mongoose.set("strictQuery", true);
|
||||
await mongoose.connect(mongoUri);
|
||||
return mongoose.connection;
|
||||
};
|
||||
|
||||
module.exports = connectDb;
|
||||
13
backend/models/Announcement.js
Normal file
13
backend/models/Announcement.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const announcementSchema = new mongoose.Schema(
|
||||
{
|
||||
title: { type: String, required: true },
|
||||
content: { type: String },
|
||||
priority: { type: Number, default: 0 },
|
||||
active: { type: Boolean, default: true },
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
module.exports = mongoose.model("Announcement", announcementSchema);
|
||||
13
backend/models/BibleVerse.js
Normal file
13
backend/models/BibleVerse.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const bibleVerseSchema = new mongoose.Schema(
|
||||
{
|
||||
text: { type: String, required: true },
|
||||
reference: { type: String, required: true },
|
||||
date: { type: String, trim: true },
|
||||
active: { type: Boolean, default: true },
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
module.exports = mongoose.model("BibleVerse", bibleVerseSchema);
|
||||
13
backend/models/FamilyMember.js
Normal file
13
backend/models/FamilyMember.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const familyMemberSchema = new mongoose.Schema(
|
||||
{
|
||||
name: { type: String, required: true },
|
||||
emoji: { type: String, required: true },
|
||||
color: { type: String, required: true },
|
||||
order: { type: Number, default: 0 },
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
module.exports = mongoose.model("FamilyMember", familyMemberSchema);
|
||||
12
backend/models/Photo.js
Normal file
12
backend/models/Photo.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const photoSchema = new mongoose.Schema(
|
||||
{
|
||||
url: { type: String, required: true },
|
||||
caption: { type: String },
|
||||
active: { type: Boolean, default: true },
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
module.exports = mongoose.model("Photo", photoSchema);
|
||||
15
backend/models/Schedule.js
Normal file
15
backend/models/Schedule.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const scheduleSchema = new mongoose.Schema(
|
||||
{
|
||||
title: { type: String, required: true },
|
||||
description: { type: String },
|
||||
startDate: { type: Date, required: true },
|
||||
endDate: { type: Date, required: true },
|
||||
familyMemberId: { type: mongoose.Schema.Types.ObjectId, ref: "FamilyMember" },
|
||||
isAllDay: { type: Boolean, default: false },
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
module.exports = mongoose.model("Schedule", scheduleSchema);
|
||||
11
backend/models/Setting.js
Normal file
11
backend/models/Setting.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const settingSchema = new mongoose.Schema(
|
||||
{
|
||||
key: { type: String, required: true, unique: true },
|
||||
value: { type: mongoose.Schema.Types.Mixed },
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
module.exports = mongoose.model("Setting", settingSchema);
|
||||
13
backend/models/Todo.js
Normal file
13
backend/models/Todo.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const todoSchema = new mongoose.Schema(
|
||||
{
|
||||
familyMemberId: { type: mongoose.Schema.Types.ObjectId, ref: "FamilyMember" },
|
||||
title: { type: String, required: true },
|
||||
completed: { type: Boolean, default: false },
|
||||
dueDate: { type: Date },
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
module.exports = mongoose.model("Todo", todoSchema);
|
||||
1213
backend/package-lock.json
generated
Normal file
1213
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
backend/package.json
Normal file
20
backend/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "bini-google-tv-backend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"main": "server.js",
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "node server.js",
|
||||
"seed": "node scripts/seed.js",
|
||||
"demo": "node scripts/demo.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.9",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.19.2",
|
||||
"mongoose": "^8.9.0"
|
||||
}
|
||||
}
|
||||
69
backend/routes/announcements.js
Normal file
69
backend/routes/announcements.js
Normal file
@@ -0,0 +1,69 @@
|
||||
const express = require("express");
|
||||
const Announcement = require("../models/Announcement");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/", async (req, res) => {
|
||||
try {
|
||||
const filter = {};
|
||||
if (req.query.active === "true") {
|
||||
filter.active = true;
|
||||
}
|
||||
const announcements = await Announcement.find(filter).sort({
|
||||
priority: -1,
|
||||
createdAt: -1,
|
||||
});
|
||||
res.json(announcements);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to fetch announcements" });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
try {
|
||||
const announcement = await Announcement.create(req.body);
|
||||
res.status(201).json(announcement);
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Failed to create announcement" });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/:id", async (req, res) => {
|
||||
try {
|
||||
const announcement = await Announcement.findById(req.params.id);
|
||||
if (!announcement) {
|
||||
return res.status(404).json({ message: "Announcement not found" });
|
||||
}
|
||||
res.json(announcement);
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Failed to fetch announcement" });
|
||||
}
|
||||
});
|
||||
|
||||
router.put("/:id", async (req, res) => {
|
||||
try {
|
||||
const announcement = await Announcement.findByIdAndUpdate(req.params.id, req.body, {
|
||||
new: true,
|
||||
});
|
||||
if (!announcement) {
|
||||
return res.status(404).json({ message: "Announcement not found" });
|
||||
}
|
||||
res.json(announcement);
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Failed to update announcement" });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete("/:id", async (req, res) => {
|
||||
try {
|
||||
const announcement = await Announcement.findByIdAndDelete(req.params.id);
|
||||
if (!announcement) {
|
||||
return res.status(404).json({ message: "Announcement not found" });
|
||||
}
|
||||
res.json({ ok: true });
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Failed to delete announcement" });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
94
backend/routes/bible.js
Normal file
94
backend/routes/bible.js
Normal file
@@ -0,0 +1,94 @@
|
||||
const express = require("express");
|
||||
const BibleVerse = require("../models/BibleVerse");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const pickRandomVerse = async (filter) => {
|
||||
const results = await BibleVerse.aggregate([
|
||||
{ $match: filter },
|
||||
{ $sample: { size: 1 } },
|
||||
]);
|
||||
return results[0] || null;
|
||||
};
|
||||
|
||||
router.get("/today", async (req, res) => {
|
||||
try {
|
||||
const targetDate = req.query.date || new Date().toISOString().slice(0, 10);
|
||||
const datedVerse = await pickRandomVerse({
|
||||
active: true,
|
||||
date: targetDate,
|
||||
});
|
||||
if (datedVerse) {
|
||||
return res.json(datedVerse);
|
||||
}
|
||||
|
||||
const undatedVerse = await pickRandomVerse({
|
||||
active: true,
|
||||
$or: [{ date: { $exists: false } }, { date: null }, { date: "" }],
|
||||
});
|
||||
if (undatedVerse) {
|
||||
return res.json(undatedVerse);
|
||||
}
|
||||
|
||||
const anyVerse = await pickRandomVerse({ active: true });
|
||||
if (!anyVerse) {
|
||||
return res.status(404).json({ message: "No bible verses available" });
|
||||
}
|
||||
return res.json(anyVerse);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to fetch bible verse" });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/verses", async (req, res) => {
|
||||
try {
|
||||
const filter = {};
|
||||
if (req.query.active === "true") {
|
||||
filter.active = true;
|
||||
}
|
||||
const verses = await BibleVerse.find(filter).sort({
|
||||
date: -1,
|
||||
createdAt: -1,
|
||||
});
|
||||
res.json(verses);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to fetch bible verses" });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/verses", async (req, res) => {
|
||||
try {
|
||||
const verse = await BibleVerse.create(req.body);
|
||||
res.status(201).json(verse);
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Failed to create bible verse" });
|
||||
}
|
||||
});
|
||||
|
||||
router.put("/verses/:id", async (req, res) => {
|
||||
try {
|
||||
const verse = await BibleVerse.findByIdAndUpdate(req.params.id, req.body, {
|
||||
new: true,
|
||||
});
|
||||
if (!verse) {
|
||||
return res.status(404).json({ message: "Bible verse not found" });
|
||||
}
|
||||
res.json(verse);
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Failed to update bible verse" });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete("/verses/:id", async (req, res) => {
|
||||
try {
|
||||
const verse = await BibleVerse.findByIdAndDelete(req.params.id);
|
||||
if (!verse) {
|
||||
return res.status(404).json({ message: "Bible verse not found" });
|
||||
}
|
||||
res.json({ ok: true });
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Failed to delete bible verse" });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
62
backend/routes/family.js
Normal file
62
backend/routes/family.js
Normal file
@@ -0,0 +1,62 @@
|
||||
const express = require("express");
|
||||
const FamilyMember = require("../models/FamilyMember");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/", async (req, res) => {
|
||||
try {
|
||||
const members = await FamilyMember.find().sort({ order: 1, createdAt: 1 });
|
||||
res.json(members);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to fetch family members" });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
try {
|
||||
const member = await FamilyMember.create(req.body);
|
||||
res.status(201).json(member);
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Failed to create family member" });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/:id", async (req, res) => {
|
||||
try {
|
||||
const member = await FamilyMember.findById(req.params.id);
|
||||
if (!member) {
|
||||
return res.status(404).json({ message: "Family member not found" });
|
||||
}
|
||||
res.json(member);
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Failed to fetch family member" });
|
||||
}
|
||||
});
|
||||
|
||||
router.put("/:id", async (req, res) => {
|
||||
try {
|
||||
const member = await FamilyMember.findByIdAndUpdate(req.params.id, req.body, {
|
||||
new: true,
|
||||
});
|
||||
if (!member) {
|
||||
return res.status(404).json({ message: "Family member not found" });
|
||||
}
|
||||
res.json(member);
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Failed to update family member" });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete("/:id", async (req, res) => {
|
||||
try {
|
||||
const member = await FamilyMember.findByIdAndDelete(req.params.id);
|
||||
if (!member) {
|
||||
return res.status(404).json({ message: "Family member not found" });
|
||||
}
|
||||
res.json({ ok: true });
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Failed to delete family member" });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
40
backend/routes/photos.js
Normal file
40
backend/routes/photos.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const express = require("express");
|
||||
const Photo = require("../models/Photo");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/", async (req, res) => {
|
||||
try {
|
||||
const filter = {};
|
||||
if (req.query.active === "true") {
|
||||
filter.active = true;
|
||||
}
|
||||
const photos = await Photo.find(filter).sort({ createdAt: -1 });
|
||||
res.json(photos);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to fetch photos" });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
try {
|
||||
const photo = await Photo.create(req.body);
|
||||
res.status(201).json(photo);
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Failed to create photo" });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete("/:id", async (req, res) => {
|
||||
try {
|
||||
const photo = await Photo.findByIdAndDelete(req.params.id);
|
||||
if (!photo) {
|
||||
return res.status(404).json({ message: "Photo not found" });
|
||||
}
|
||||
res.json({ ok: true });
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Failed to delete photo" });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
115
backend/routes/schedules.js
Normal file
115
backend/routes/schedules.js
Normal file
@@ -0,0 +1,115 @@
|
||||
const express = require("express");
|
||||
const Schedule = require("../models/Schedule");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const startOfWeek = (date = new Date()) => {
|
||||
const copy = new Date(date);
|
||||
const day = copy.getDay();
|
||||
const diff = day === 0 ? -6 : 1 - day;
|
||||
copy.setDate(copy.getDate() + diff);
|
||||
copy.setHours(0, 0, 0, 0);
|
||||
return copy;
|
||||
};
|
||||
|
||||
const endOfWeek = (date = new Date()) => {
|
||||
const start = startOfWeek(date);
|
||||
const end = new Date(start);
|
||||
end.setDate(end.getDate() + 6);
|
||||
end.setHours(23, 59, 59, 999);
|
||||
return end;
|
||||
};
|
||||
|
||||
const startOfMonth = (date = new Date()) => {
|
||||
return new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0);
|
||||
};
|
||||
|
||||
const endOfMonth = (date = new Date()) => {
|
||||
return new Date(date.getFullYear(), date.getMonth() + 1, 0, 23, 59, 59, 999);
|
||||
};
|
||||
|
||||
router.get("/", async (req, res) => {
|
||||
try {
|
||||
const schedules = await Schedule.find().sort({ startDate: 1 });
|
||||
res.json(schedules);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to fetch schedules" });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/week", async (req, res) => {
|
||||
try {
|
||||
const start = startOfWeek();
|
||||
const end = endOfWeek();
|
||||
const schedules = await Schedule.find({
|
||||
startDate: { $lte: end },
|
||||
endDate: { $gte: start },
|
||||
}).sort({ startDate: 1 });
|
||||
res.json(schedules);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to fetch weekly schedules" });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/month", async (req, res) => {
|
||||
try {
|
||||
const start = startOfMonth();
|
||||
const end = endOfMonth();
|
||||
const schedules = await Schedule.find({
|
||||
startDate: { $lte: end },
|
||||
endDate: { $gte: start },
|
||||
}).sort({ startDate: 1 });
|
||||
res.json(schedules);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to fetch monthly schedules" });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
try {
|
||||
const schedule = await Schedule.create(req.body);
|
||||
res.status(201).json(schedule);
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Failed to create schedule" });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/:id", async (req, res) => {
|
||||
try {
|
||||
const schedule = await Schedule.findById(req.params.id);
|
||||
if (!schedule) {
|
||||
return res.status(404).json({ message: "Schedule not found" });
|
||||
}
|
||||
res.json(schedule);
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Failed to fetch schedule" });
|
||||
}
|
||||
});
|
||||
|
||||
router.put("/:id", async (req, res) => {
|
||||
try {
|
||||
const schedule = await Schedule.findByIdAndUpdate(req.params.id, req.body, {
|
||||
new: true,
|
||||
});
|
||||
if (!schedule) {
|
||||
return res.status(404).json({ message: "Schedule not found" });
|
||||
}
|
||||
res.json(schedule);
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Failed to update schedule" });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete("/:id", async (req, res) => {
|
||||
try {
|
||||
const schedule = await Schedule.findByIdAndDelete(req.params.id);
|
||||
if (!schedule) {
|
||||
return res.status(404).json({ message: "Schedule not found" });
|
||||
}
|
||||
res.json({ ok: true });
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Failed to delete schedule" });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
81
backend/routes/todos.js
Normal file
81
backend/routes/todos.js
Normal file
@@ -0,0 +1,81 @@
|
||||
const express = require("express");
|
||||
const Todo = require("../models/Todo");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const getDayRange = (date = new Date()) => {
|
||||
const start = new Date(date);
|
||||
start.setHours(0, 0, 0, 0);
|
||||
const end = new Date(date);
|
||||
end.setHours(23, 59, 59, 999);
|
||||
return { start, end };
|
||||
};
|
||||
|
||||
router.get("/", async (req, res) => {
|
||||
try {
|
||||
const todos = await Todo.find().sort({ dueDate: 1, createdAt: -1 });
|
||||
res.json(todos);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to fetch todos" });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/today", async (req, res) => {
|
||||
try {
|
||||
const { start, end } = getDayRange();
|
||||
const todos = await Todo.find({ dueDate: { $gte: start, $lte: end } }).sort({
|
||||
dueDate: 1,
|
||||
createdAt: -1,
|
||||
});
|
||||
res.json(todos);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to fetch today todos" });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
try {
|
||||
const todo = await Todo.create(req.body);
|
||||
res.status(201).json(todo);
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Failed to create todo" });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/:id", async (req, res) => {
|
||||
try {
|
||||
const todo = await Todo.findById(req.params.id);
|
||||
if (!todo) {
|
||||
return res.status(404).json({ message: "Todo not found" });
|
||||
}
|
||||
res.json(todo);
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Failed to fetch todo" });
|
||||
}
|
||||
});
|
||||
|
||||
router.put("/:id", async (req, res) => {
|
||||
try {
|
||||
const todo = await Todo.findByIdAndUpdate(req.params.id, req.body, { new: true });
|
||||
if (!todo) {
|
||||
return res.status(404).json({ message: "Todo not found" });
|
||||
}
|
||||
res.json(todo);
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Failed to update todo" });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete("/:id", async (req, res) => {
|
||||
try {
|
||||
const todo = await Todo.findByIdAndDelete(req.params.id);
|
||||
if (!todo) {
|
||||
return res.status(404).json({ message: "Todo not found" });
|
||||
}
|
||||
res.json({ ok: true });
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Failed to delete todo" });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
35
backend/routes/weather.js
Normal file
35
backend/routes/weather.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const express = require("express");
|
||||
const axios = require("axios");
|
||||
const config = require("../config/api");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/", async (req, res) => {
|
||||
try {
|
||||
const { apiKey, baseUrl, city, units, language } = config.weather;
|
||||
if (!apiKey) {
|
||||
return res.status(400).json({ message: "OPENWEATHER_API_KEY is not set" });
|
||||
}
|
||||
|
||||
const { q, lat, lon } = req.query;
|
||||
const params = {
|
||||
appid: apiKey,
|
||||
units,
|
||||
lang: language,
|
||||
};
|
||||
|
||||
if (lat && lon) {
|
||||
params.lat = lat;
|
||||
params.lon = lon;
|
||||
} else {
|
||||
params.q = q || city;
|
||||
}
|
||||
|
||||
const response = await axios.get(`${baseUrl}/weather`, { params });
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to fetch weather" });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
29
backend/scripts/demo.js
Normal file
29
backend/scripts/demo.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const path = require("path");
|
||||
const { spawn } = require("child_process");
|
||||
|
||||
const run = (command, args) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, { stdio: "inherit" });
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
reject(new Error(`Command failed: ${command} ${args.join(" ")}`));
|
||||
});
|
||||
});
|
||||
|
||||
const start = async () => {
|
||||
const seedPath = path.join(__dirname, "seed.js");
|
||||
const serverPath = path.join(__dirname, "..", "server.js");
|
||||
|
||||
await run("node", [seedPath]);
|
||||
|
||||
const server = spawn("node", [serverPath], { stdio: "inherit" });
|
||||
server.on("close", (code) => process.exit(code ?? 0));
|
||||
};
|
||||
|
||||
start().catch((error) => {
|
||||
console.error("Demo failed", error);
|
||||
process.exit(1);
|
||||
});
|
||||
132
backend/scripts/seed.js
Normal file
132
backend/scripts/seed.js
Normal file
@@ -0,0 +1,132 @@
|
||||
const dotenv = require("dotenv");
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const FamilyMember = require("../models/FamilyMember");
|
||||
const Todo = require("../models/Todo");
|
||||
const Schedule = require("../models/Schedule");
|
||||
const Announcement = require("../models/Announcement");
|
||||
const Photo = require("../models/Photo");
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const connect = async () => {
|
||||
const mongoUri = process.env.MONGODB_URI;
|
||||
if (!mongoUri) {
|
||||
throw new Error("MONGODB_URI is not set");
|
||||
}
|
||||
mongoose.set("strictQuery", true);
|
||||
await mongoose.connect(mongoUri);
|
||||
};
|
||||
|
||||
const seed = async () => {
|
||||
await Promise.all([
|
||||
FamilyMember.deleteMany({}),
|
||||
Todo.deleteMany({}),
|
||||
Schedule.deleteMany({}),
|
||||
Announcement.deleteMany({}),
|
||||
Photo.deleteMany({}),
|
||||
]);
|
||||
|
||||
const family = await FamilyMember.insertMany([
|
||||
{ name: "Dad", emoji: ":)", color: "#0F766E", order: 1 },
|
||||
{ name: "Mom", emoji: "<3", color: "#C2410C", order: 2 },
|
||||
{ name: "Son", emoji: ":D", color: "#1D4ED8", order: 3 },
|
||||
{ name: "Daughter", emoji: ":-)", color: "#7C3AED", order: 4 },
|
||||
]);
|
||||
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 9, 0);
|
||||
const tomorrow = new Date(today);
|
||||
tomorrow.setDate(today.getDate() + 1);
|
||||
|
||||
await Todo.insertMany([
|
||||
{
|
||||
familyMemberId: family[0]._id,
|
||||
title: "Grocery run",
|
||||
completed: false,
|
||||
dueDate: today,
|
||||
},
|
||||
{
|
||||
familyMemberId: family[1]._id,
|
||||
title: "Team meeting",
|
||||
completed: false,
|
||||
dueDate: today,
|
||||
},
|
||||
{
|
||||
familyMemberId: family[2]._id,
|
||||
title: "Math homework",
|
||||
completed: false,
|
||||
dueDate: today,
|
||||
},
|
||||
{
|
||||
familyMemberId: family[3]._id,
|
||||
title: "Piano lesson",
|
||||
completed: false,
|
||||
dueDate: tomorrow,
|
||||
},
|
||||
]);
|
||||
|
||||
await Schedule.insertMany([
|
||||
{
|
||||
title: "Family dinner",
|
||||
description: "Everyone at home",
|
||||
startDate: today,
|
||||
endDate: new Date(today.getTime() + 2 * 60 * 60 * 1000),
|
||||
familyMemberId: family[0]._id,
|
||||
isAllDay: false,
|
||||
},
|
||||
{
|
||||
title: "Soccer practice",
|
||||
description: "School field",
|
||||
startDate: new Date(today.getTime() + 4 * 60 * 60 * 1000),
|
||||
endDate: new Date(today.getTime() + 5 * 60 * 60 * 1000),
|
||||
familyMemberId: family[2]._id,
|
||||
isAllDay: false,
|
||||
},
|
||||
]);
|
||||
|
||||
await Announcement.insertMany([
|
||||
{
|
||||
title: "Weekend trip",
|
||||
content: "Pack light and be ready by 8 AM",
|
||||
priority: 2,
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
title: "Trash day",
|
||||
content: "Take out bins tonight",
|
||||
priority: 1,
|
||||
active: true,
|
||||
},
|
||||
]);
|
||||
|
||||
await Photo.insertMany([
|
||||
{
|
||||
url: "https://picsum.photos/1200/800?random=10",
|
||||
caption: "Summer vacation",
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
url: "https://picsum.photos/1200/800?random=11",
|
||||
caption: "Family hike",
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
url: "https://picsum.photos/1200/800?random=12",
|
||||
caption: "Birthday party",
|
||||
active: true,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
connect()
|
||||
.then(seed)
|
||||
.then(() => {
|
||||
console.log("Seed data inserted");
|
||||
return mongoose.disconnect();
|
||||
})
|
||||
.then(() => process.exit(0))
|
||||
.catch((error) => {
|
||||
console.error("Seed failed", error);
|
||||
process.exit(1);
|
||||
});
|
||||
43
backend/server.js
Normal file
43
backend/server.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const express = require("express");
|
||||
const cors = require("cors");
|
||||
const dotenv = require("dotenv");
|
||||
|
||||
const connectDb = require("./config/db");
|
||||
const familyRoutes = require("./routes/family");
|
||||
const todoRoutes = require("./routes/todos");
|
||||
const scheduleRoutes = require("./routes/schedules");
|
||||
const announcementRoutes = require("./routes/announcements");
|
||||
const weatherRoutes = require("./routes/weather");
|
||||
const bibleRoutes = require("./routes/bible");
|
||||
const photoRoutes = require("./routes/photos");
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
const port = process.env.PORT || 4000;
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: "2mb" }));
|
||||
|
||||
app.get("/health", (req, res) => {
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
app.use("/api/family", familyRoutes);
|
||||
app.use("/api/todos", todoRoutes);
|
||||
app.use("/api/schedules", scheduleRoutes);
|
||||
app.use("/api/announcements", announcementRoutes);
|
||||
app.use("/api/weather", weatherRoutes);
|
||||
app.use("/api/bible", bibleRoutes);
|
||||
app.use("/api/photos", photoRoutes);
|
||||
|
||||
connectDb()
|
||||
.then(() => {
|
||||
app.listen(port, () => {
|
||||
console.log(`Server listening on ${port}`);
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to start server", error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user