feat: Initial commit of backend services and AGENTS.md
This commit is contained in:
67
AGENTS.md
Normal file
67
AGENTS.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# Adopt-a-Street Agents
|
||||||
|
|
||||||
|
This document outlines the various agents (user roles and automated systems) within the Adopt-a-Street application, their capabilities, and how they interact with the platform.
|
||||||
|
|
||||||
|
## User Agents
|
||||||
|
|
||||||
|
### 1. Regular User
|
||||||
|
|
||||||
|
Regular users are the primary actors in the Adopt-a-Street ecosystem. They are community members who volunteer to maintain and improve their local streets.
|
||||||
|
|
||||||
|
**Capabilities:**
|
||||||
|
|
||||||
|
* **Authentication:** Register for a new account, log in, and log out.
|
||||||
|
* **Street Adoption:** View available streets on a map and adopt one or more streets to care for.
|
||||||
|
* **Task Management:** View and complete maintenance tasks associated with their adopted streets (e.g., trash pickup, graffiti removal).
|
||||||
|
* **Social Feed:** Share updates and photos of their work, view posts from other users, and engage with the community.
|
||||||
|
* **Events:** Participate in community cleanup events and other local initiatives.
|
||||||
|
* **Rewards:** Earn points and badges for completing tasks and participating in events, which can be redeemed for rewards.
|
||||||
|
* **Profile Management:** View their profile, including adopted streets, completed tasks, points, and badges.
|
||||||
|
|
||||||
|
### 2. Premium User
|
||||||
|
|
||||||
|
Premium users have all the capabilities of regular users, with potential access to additional features and benefits.
|
||||||
|
|
||||||
|
**Capabilities:**
|
||||||
|
|
||||||
|
* All capabilities of a Regular User.
|
||||||
|
* **Premium Features:** Access to exclusive rewards, advanced analytics, or other premium features (as defined by the application).
|
||||||
|
|
||||||
|
## Automated Agents
|
||||||
|
|
||||||
|
### 1. AI Agent
|
||||||
|
|
||||||
|
The AI agent provides intelligent features and support to users, helping to streamline the street adoption process and enhance the user experience.
|
||||||
|
|
||||||
|
**Functionality:**
|
||||||
|
|
||||||
|
* **Task Suggestions:** Recommends maintenance tasks based on street conditions, user activity, and other data.
|
||||||
|
* **Community Insights:** Provides analytics and insights into community engagement and environmental impact.
|
||||||
|
* **Gamification:** Manages the points, badges, and rewards system to encourage user participation.
|
||||||
|
|
||||||
|
## System Architecture
|
||||||
|
|
||||||
|
The Adopt-a-Street application is a full-stack web application with a React frontend and a Node.js/Express backend.
|
||||||
|
|
||||||
|
### Frontend Components
|
||||||
|
|
||||||
|
* **`MapView`:** Displays an interactive map of streets, allowing users to view available streets and adopt them.
|
||||||
|
* **`TaskList`:** Lists the maintenance tasks for a user's adopted streets.
|
||||||
|
* **`SocialFeed`:** A social media-style feed for users to share updates and connect with others.
|
||||||
|
* **`Profile`:** Displays user information, including adopted streets, points, and badges.
|
||||||
|
* **`Events`:** Shows upcoming community events and allows users to RSVP.
|
||||||
|
* **`Rewards`:** A marketplace where users can redeem points for rewards.
|
||||||
|
* **`Login` and `Register`:** User authentication components.
|
||||||
|
* **`AuthContext`:** Manages user authentication state throughout the application.
|
||||||
|
|
||||||
|
### Backend API Endpoints
|
||||||
|
|
||||||
|
* `/api/auth`: User registration and login.
|
||||||
|
* `/api/streets`: Get street data and adopt streets.
|
||||||
|
* `/api/tasks`: Manage maintenance tasks.
|
||||||
|
* `/api/posts`: Create and view posts in the social feed.
|
||||||
|
* `/api/events`: Manage community events.
|
||||||
|
* `/api/rewards`: Manage rewards and user points.
|
||||||
|
* `/api/reports`: User-submitted reports on street conditions.
|
||||||
|
* `/api/ai`: AI-powered suggestions and insights.
|
||||||
|
* `/api/payments`: Handle premium subscriptions and other payments.
|
||||||
2
backend/.gitignore
vendored
Normal file
2
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
.env
|
||||||
18
backend/eslint.config.js
Normal file
18
backend/eslint.config.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
const globals = require("globals");
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "commonjs",
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"no-unused-vars": "warn",
|
||||||
|
"no-undef": "warn",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
20
backend/middleware/auth.js
Normal file
20
backend/middleware/auth.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
const jwt = require("jsonwebtoken");
|
||||||
|
|
||||||
|
module.exports = function (req, res, next) {
|
||||||
|
// Get token from header
|
||||||
|
const token = req.header("x-auth-token");
|
||||||
|
|
||||||
|
// Check if not token
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({ msg: "No token, authorization denied" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify token
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||||
|
req.user = decoded.user;
|
||||||
|
next();
|
||||||
|
} catch (err) {
|
||||||
|
res.status(401).json({ msg: "Token is not valid" });
|
||||||
|
}
|
||||||
|
};
|
||||||
33
backend/models/Event.js
Normal file
33
backend/models/Event.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const mongoose = require("mongoose");
|
||||||
|
|
||||||
|
const EventSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
date: {
|
||||||
|
type: Date,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
location: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
participants: [
|
||||||
|
{
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: "User",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = mongoose.model("Event", EventSchema);
|
||||||
29
backend/models/Post.js
Normal file
29
backend/models/Post.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
const mongoose = require("mongoose");
|
||||||
|
|
||||||
|
const PostSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
user: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: "User",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
imageUrl: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
likes: [
|
||||||
|
{
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: "User",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = mongoose.model("Post", PostSchema);
|
||||||
30
backend/models/Report.js
Normal file
30
backend/models/Report.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
const mongoose = require("mongoose");
|
||||||
|
|
||||||
|
const ReportSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
street: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: "Street",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: "User",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
issue: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
enum: ["open", "resolved"],
|
||||||
|
default: "open",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = mongoose.model("Report", ReportSchema);
|
||||||
27
backend/models/Reward.js
Normal file
27
backend/models/Reward.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
const mongoose = require("mongoose");
|
||||||
|
|
||||||
|
const RewardSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
cost: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isPremium: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = mongoose.model("Reward", RewardSchema);
|
||||||
37
backend/models/Street.js
Normal file
37
backend/models/Street.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
const mongoose = require("mongoose");
|
||||||
|
|
||||||
|
const StreetSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
location: {
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
enum: ["Point"],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
coordinates: {
|
||||||
|
type: [Number],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
adoptedBy: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: "User",
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
enum: ["available", "adopted"],
|
||||||
|
default: "available",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
StreetSchema.index({ location: "2dsphere" });
|
||||||
|
|
||||||
|
module.exports = mongoose.model("Street", StreetSchema);
|
||||||
29
backend/models/Task.js
Normal file
29
backend/models/Task.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
const mongoose = require("mongoose");
|
||||||
|
|
||||||
|
const TaskSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
street: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: "Street",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
completedBy: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: "User",
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
enum: ["pending", "completed"],
|
||||||
|
default: "pending",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = mongoose.model("Task", TaskSchema);
|
||||||
45
backend/models/User.js
Normal file
45
backend/models/User.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
const mongoose = require("mongoose");
|
||||||
|
|
||||||
|
const UserSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isPremium: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
points: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
adoptedStreets: [
|
||||||
|
{
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: "Street",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
completedTasks: [
|
||||||
|
{
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: "Task",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
badges: [String],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = mongoose.model("User", UserSchema);
|
||||||
2891
backend/package-lock.json
generated
Normal file
2891
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
backend/package.json
Normal file
28
backend/package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "adopt-a-street",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.8.3",
|
||||||
|
"bcryptjs": "^3.0.2",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
|
"express": "^4.21.2",
|
||||||
|
"globals": "^16.4.0",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"mongoose": "^8.12.1",
|
||||||
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"socket.io": "^4.8.1",
|
||||||
|
"stripe": "^17.7.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^9.38.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
23
backend/routes/ai.js
Normal file
23
backend/routes/ai.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const auth = require("../middleware/auth");
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Get AI task suggestions
|
||||||
|
router.get("/task-suggestions", auth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
// In a real application, you would use a more sophisticated AI model to generate task suggestions.
|
||||||
|
// For this example, we'll just return some mock data.
|
||||||
|
const suggestions = [
|
||||||
|
{ street: "Main St", description: "Clean up litter" },
|
||||||
|
{ street: "Elm St", description: "Remove graffiti" },
|
||||||
|
{ street: "Oak Ave", description: "Mow the lawn" },
|
||||||
|
];
|
||||||
|
res.json(suggestions);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
res.status(500).send("Server error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
98
backend/routes/auth.js
Normal file
98
backend/routes/auth.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const bcrypt = require("bcryptjs");
|
||||||
|
const jwt = require("jsonwebtoken");
|
||||||
|
const User = require("../models/User");
|
||||||
|
const auth = require("../middleware/auth");
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Get user
|
||||||
|
router.get("/", auth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const user = await User.findById(req.user.id).select("-password");
|
||||||
|
res.json(user);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
res.status(500).send("Server error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register
|
||||||
|
router.post("/register", async (req, res) => {
|
||||||
|
const { name, email, password } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let user = await User.findOne({ email });
|
||||||
|
if (user) {
|
||||||
|
return res.status(400).json({ msg: "User already exists" });
|
||||||
|
}
|
||||||
|
|
||||||
|
user = new User({
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
const salt = await bcrypt.genSalt(10);
|
||||||
|
user.password = await bcrypt.hash(password, salt);
|
||||||
|
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
jwt.sign(
|
||||||
|
payload,
|
||||||
|
process.env.JWT_SECRET,
|
||||||
|
{ expiresIn: 3600 },
|
||||||
|
(err, token) => {
|
||||||
|
if (err) throw err;
|
||||||
|
res.json({ token });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
res.status(500).send("Server error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Login
|
||||||
|
router.post("/login", async (req, res) => {
|
||||||
|
const { email, password } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let user = await User.findOne({ email });
|
||||||
|
if (!user) {
|
||||||
|
return res.status(400).json({ msg: "Invalid credentials" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMatch = await bcrypt.compare(password, user.password);
|
||||||
|
if (!isMatch) {
|
||||||
|
return res.status(400).json({ msg: "Invalid credentials" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
jwt.sign(
|
||||||
|
payload,
|
||||||
|
process.env.JWT_SECRET,
|
||||||
|
{ expiresIn: 3600 },
|
||||||
|
(err, token) => {
|
||||||
|
if (err) throw err;
|
||||||
|
res.json({ token });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
res.status(500).send("Server error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
66
backend/routes/events.js
Normal file
66
backend/routes/events.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const Event = require("../models/Event");
|
||||||
|
const auth = require("../middleware/auth");
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Get all events
|
||||||
|
router.get("/", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const events = await Event.find();
|
||||||
|
res.json(events);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
res.status(500).send("Server error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create an event
|
||||||
|
router.post("/", auth, async (req, res) => {
|
||||||
|
const { title, description, date, location } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newEvent = new Event({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
date,
|
||||||
|
location,
|
||||||
|
});
|
||||||
|
|
||||||
|
const event = await newEvent.save();
|
||||||
|
res.json(event);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
res.status(500).send("Server error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// RSVP to an event
|
||||||
|
router.put("/rsvp/:id", auth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const event = await Event.findById(req.params.id);
|
||||||
|
if (!event) {
|
||||||
|
return res.status(404).json({ msg: "Event not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user has already RSVPed
|
||||||
|
if (
|
||||||
|
event.participants.filter(
|
||||||
|
(participant) => participant.toString() === req.user.id,
|
||||||
|
).length > 0
|
||||||
|
) {
|
||||||
|
return res.status(400).json({ msg: "Already RSVPed" });
|
||||||
|
}
|
||||||
|
|
||||||
|
event.participants.unshift(req.user.id);
|
||||||
|
|
||||||
|
await event.save();
|
||||||
|
|
||||||
|
res.json(event.participants);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
res.status(500).send("Server error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
27
backend/routes/payments.js
Normal file
27
backend/routes/payments.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const auth = require("../middleware/auth");
|
||||||
|
const User = require("../models/User");
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Handle premium subscription
|
||||||
|
router.post("/subscribe", auth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
// In a real application, you would integrate with a payment gateway like Stripe.
|
||||||
|
// For this example, we'll just mock a successful payment.
|
||||||
|
const user = await User.findById(req.user.id);
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ msg: "User not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
user.isPremium = true;
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
res.json({ msg: "Subscription successful" });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
res.status(500).send("Server error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
63
backend/routes/posts.js
Normal file
63
backend/routes/posts.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const Post = require("../models/Post");
|
||||||
|
const auth = require("../middleware/auth");
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Get all posts
|
||||||
|
router.get("/", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const posts = await Post.find().populate("user", ["name"]);
|
||||||
|
res.json(posts);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
res.status(500).send("Server error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a post
|
||||||
|
router.post("/", auth, async (req, res) => {
|
||||||
|
const { content, imageUrl } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newPost = new Post({
|
||||||
|
user: req.user.id,
|
||||||
|
content,
|
||||||
|
imageUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await newPost.save();
|
||||||
|
res.json(post);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
res.status(500).send("Server error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Like a post
|
||||||
|
router.put("/like/:id", auth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const post = await Post.findById(req.params.id);
|
||||||
|
if (!post) {
|
||||||
|
return res.status(404).json({ msg: "Post not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the post has already been liked by this user
|
||||||
|
if (
|
||||||
|
post.likes.filter((like) => like.toString() === req.user.id).length > 0
|
||||||
|
) {
|
||||||
|
return res.status(400).json({ msg: "Post already liked" });
|
||||||
|
}
|
||||||
|
|
||||||
|
post.likes.unshift(req.user.id);
|
||||||
|
|
||||||
|
await post.save();
|
||||||
|
|
||||||
|
res.json(post.likes);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
res.status(500).send("Server error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
58
backend/routes/reports.js
Normal file
58
backend/routes/reports.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const Report = require("../models/Report");
|
||||||
|
const auth = require("../middleware/auth");
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Get all reports
|
||||||
|
router.get("/", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const reports = await Report.find()
|
||||||
|
.populate("street", ["name"])
|
||||||
|
.populate("user", ["name"]);
|
||||||
|
res.json(reports);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
res.status(500).send("Server error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a report
|
||||||
|
router.post("/", auth, async (req, res) => {
|
||||||
|
const { street, issue } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newReport = new Report({
|
||||||
|
street,
|
||||||
|
user: req.user.id,
|
||||||
|
issue,
|
||||||
|
});
|
||||||
|
|
||||||
|
const report = await newReport.save();
|
||||||
|
res.json(report);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
res.status(500).send("Server error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Resolve a report
|
||||||
|
router.put("/:id", auth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const report = await Report.findById(req.params.id);
|
||||||
|
if (!report) {
|
||||||
|
return res.status(404).json({ msg: "Report not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
report.status = "resolved";
|
||||||
|
|
||||||
|
await report.save();
|
||||||
|
|
||||||
|
res.json(report);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
res.status(500).send("Server error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
70
backend/routes/rewards.js
Normal file
70
backend/routes/rewards.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const Reward = require("../models/Reward");
|
||||||
|
const User = require("../models/User");
|
||||||
|
const auth = require("../middleware/auth");
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Get all rewards
|
||||||
|
router.get("/", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const rewards = await Reward.find();
|
||||||
|
res.json(rewards);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
res.status(500).send("Server error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a reward
|
||||||
|
router.post("/", auth, async (req, res) => {
|
||||||
|
const { name, description, cost, isPremium } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newReward = new Reward({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
cost,
|
||||||
|
isPremium,
|
||||||
|
});
|
||||||
|
|
||||||
|
const reward = await newReward.save();
|
||||||
|
res.json(reward);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
res.status(500).send("Server error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Redeem a reward
|
||||||
|
router.post("/redeem/:id", auth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const reward = await Reward.findById(req.params.id);
|
||||||
|
if (!reward) {
|
||||||
|
return res.status(404).json({ msg: "Reward not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.findById(req.user.id);
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ msg: "User not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.points < reward.cost) {
|
||||||
|
return res.status(400).json({ msg: "Not enough points" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reward.isPremium && !user.isPremium) {
|
||||||
|
return res.status(403).json({ msg: "Premium reward not available" });
|
||||||
|
}
|
||||||
|
|
||||||
|
user.points -= reward.cost;
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
res.json({ msg: "Reward redeemed successfully" });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
res.status(500).send("Server error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
74
backend/routes/streets.js
Normal file
74
backend/routes/streets.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const Street = require("../models/Street");
|
||||||
|
const auth = require("../middleware/auth");
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Get all streets
|
||||||
|
router.get("/", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const streets = await Street.find();
|
||||||
|
res.json(streets);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
res.status(500).send("Server error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get single street
|
||||||
|
router.get("/:id", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const street = await Street.findById(req.params.id);
|
||||||
|
if (!street) {
|
||||||
|
return res.status(404).json({ msg: "Street not found" });
|
||||||
|
}
|
||||||
|
res.json(street);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
res.status(500).send("Server error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a street
|
||||||
|
router.post("/", auth, async (req, res) => {
|
||||||
|
const { name, location } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newStreet = new Street({
|
||||||
|
name,
|
||||||
|
location,
|
||||||
|
});
|
||||||
|
|
||||||
|
const street = await newStreet.save();
|
||||||
|
res.json(street);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
res.status(500).send("Server error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Adopt a street
|
||||||
|
router.put("/adopt/:id", auth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const street = await Street.findById(req.params.id);
|
||||||
|
if (!street) {
|
||||||
|
return res.status(404).json({ msg: "Street not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (street.status === "adopted") {
|
||||||
|
return res.status(400).json({ msg: "Street already adopted" });
|
||||||
|
}
|
||||||
|
|
||||||
|
street.adoptedBy = req.user.id;
|
||||||
|
street.status = "adopted";
|
||||||
|
|
||||||
|
await street.save();
|
||||||
|
|
||||||
|
res.json(street);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
res.status(500).send("Server error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
56
backend/routes/tasks.js
Normal file
56
backend/routes/tasks.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const Task = require("../models/Task");
|
||||||
|
const auth = require("../middleware/auth");
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Get all tasks for user
|
||||||
|
router.get("/", auth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const tasks = await Task.find({ completedBy: req.user.id });
|
||||||
|
res.json(tasks);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
res.status(500).send("Server error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a task
|
||||||
|
router.post("/", auth, async (req, res) => {
|
||||||
|
const { street, description } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newTask = new Task({
|
||||||
|
street,
|
||||||
|
description,
|
||||||
|
});
|
||||||
|
|
||||||
|
const task = await newTask.save();
|
||||||
|
res.json(task);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
res.status(500).send("Server error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Complete a task
|
||||||
|
router.put("/:id", auth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const task = await Task.findById(req.params.id);
|
||||||
|
if (!task) {
|
||||||
|
return res.status(404).json({ msg: "Task not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
task.completedBy = req.user.id;
|
||||||
|
task.status = "completed";
|
||||||
|
|
||||||
|
await task.save();
|
||||||
|
|
||||||
|
res.json(task);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
res.status(500).send("Server error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
21
backend/routes/users.js
Normal file
21
backend/routes/users.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const User = require('../models/User');
|
||||||
|
const auth = require('../middleware/auth');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Get user by ID
|
||||||
|
router.get('/:id', auth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const user = await User.findById(req.params.id).populate('adoptedStreets');
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ msg: 'User not found' });
|
||||||
|
}
|
||||||
|
res.json(user);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.message);
|
||||||
|
res.status(500).send('Server error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
68
backend/server.js
Normal file
68
backend/server.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
require("dotenv").config();
|
||||||
|
const express = require("express");
|
||||||
|
const mongoose = require("mongoose");
|
||||||
|
const cors = require("cors");
|
||||||
|
const http = require("http");
|
||||||
|
const socketio = require("socket.io");
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const server = http.createServer(app);
|
||||||
|
const io = socketio(server);
|
||||||
|
const port = process.env.PORT || 5000;
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// MongoDB Connection
|
||||||
|
mongoose
|
||||||
|
.connect(process.env.MONGO_URI, {
|
||||||
|
useNewUrlParser: true,
|
||||||
|
useUnifiedTopology: true,
|
||||||
|
})
|
||||||
|
.then(() => console.log("MongoDB connected"))
|
||||||
|
.catch((err) => console.log("MongoDB connection error:", err));
|
||||||
|
|
||||||
|
// Socket.IO Setup
|
||||||
|
io.on("connection", (socket) => {
|
||||||
|
console.log("New client connected");
|
||||||
|
socket.on("joinEvent", (eventId) => {
|
||||||
|
socket.join(eventId);
|
||||||
|
});
|
||||||
|
socket.on("eventUpdate", (data) => {
|
||||||
|
io.to(data.eventId).emit("update", data.message);
|
||||||
|
});
|
||||||
|
socket.on("disconnect", () => {
|
||||||
|
console.log("Client disconnected");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
const authRoutes = require("./routes/auth");
|
||||||
|
const streetRoutes = require("./routes/streets");
|
||||||
|
const taskRoutes = require("./routes/tasks");
|
||||||
|
const postRoutes = require("./routes/posts");
|
||||||
|
const eventRoutes = require("./routes/events");
|
||||||
|
const rewardRoutes = require("./routes/rewards");
|
||||||
|
const reportRoutes = require("./routes/reports");
|
||||||
|
const aiRoutes = require("./routes/ai");
|
||||||
|
const paymentRoutes = require("./routes/payments");
|
||||||
|
const userRoutes = require("./routes/users");
|
||||||
|
|
||||||
|
app.use("/api/auth", authRoutes);
|
||||||
|
app.use("/api/streets", streetRoutes);
|
||||||
|
app.use("/api/tasks", taskRoutes);
|
||||||
|
app.use("/api/posts", postRoutes);
|
||||||
|
app.use("/api/events", eventRoutes);
|
||||||
|
app.use("/api/rewards", rewardRoutes);
|
||||||
|
app.use("/api/reports", reportRoutes);
|
||||||
|
app.use("/api/ai", aiRoutes);
|
||||||
|
app.use("/api/payments", paymentRoutes);
|
||||||
|
app.use("/api/users", userRoutes);
|
||||||
|
|
||||||
|
app.get("/", (req, res) => {
|
||||||
|
res.send("Street Adoption App Backend");
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(port, () => {
|
||||||
|
console.log(`Server running on port ${port}`);
|
||||||
|
});
|
||||||
1
frontend
Submodule
1
frontend
Submodule
Submodule frontend added at 2b98fcfd52
Reference in New Issue
Block a user