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
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# find-addis

**Short description:** find-addis is a small React + Express + MongoDB application to discover and review restaurants. The repo contains a `client` (React + Vite) and a `server` (Express + MongoDB) folder.

---

## ⚙️ Structure

- `client/` — React frontend (Vite)
- `server/` — Express API server (MongoDB)

---

## 🔧 Quick start

Prerequisites

- Node 18+ / npm or yarn
- MongoDB (local or Atlas)

Install

```bash
# from repository root
npm install
# or install separately under each subfolder
cd client && npm install
cd ../server && npm install
```

Development

Run both services concurrently (recommended)

```bash
# from repository root (uses npm workspaces + concurrently)
npm install
npm run dev
```

Run individually (alternatives)

```bash
# run server in dev (nodemon)
cd server && npm run dev
# run client in dev (Vite)
cd client && npm run dev
```

Production (build & serve frontend + run server)

```bash
# build frontend
cd client && npm run build
# serve client/build with any static file server (Nginx, serve, etc.) or configure server to serve static files
# run server
cd server && npm start
```

---

## 📚 Contributing & Notes

- Follow the existing code style and ESLint in `client/` (use `npm run lint`).
- If you add new env vars, document them in `server/.env.example` (see `server/README.md`).

---

> For more details about usage, roadmap and endpoints, see `client/README.md` and `server/README.md`.
88 changes: 43 additions & 45 deletions client/README.md
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,74 +1,72 @@
# FindAddis
# Client (React + Vite) — find-addis (Project README)

A small React + Vite project with an Express + MongoDB backend. This README describes how to run the app locally.
**Purpose:** This file documents how to run and build the frontend for the find-addis project. The app consumes the `find-addis` API (default `http://localhost:3000/api`).

## Prerequisites
- Node.js (16+ recommended)
- npm
- MongoDB (local `mongod`) or a MongoDB Atlas connection string
---

## Run locally (Frontend + Backend)
## Functional Highlights

1) Frontend (run from project root):
- Browse restaurants, view details, filter by category/rating.
- Users: signup/login, add favorites, write reviews.
- Owners: signup/login, add restaurants, manage own restaurants.

```bash
cd "C:\Users\PC\Downloads\ReactProject-zip\ReactProject\FindAddis"
npm install
npm run dev
```
---

Vite will print a local URL (e.g. `http://localhost:5173` or another available port).
## How to run (dev)

2) Backend (in a separate terminal):
1. Install

```bash
cd backend
cd client
npm install
node server.js
```

The backend defaults to port `5000` and uses the `MONGO_URI` environment variable to connect to MongoDB.

Create `backend/.env` (already added in this repo) with values like:
2. Start dev server

```bash
npm run dev
```
MONGO_URI=mongodb://localhost:27017/findaddis
PORT=5000
```

3) Seed sample data (optional):
3. Open the app at the URL printed by Vite (usually `http://localhost:5173`).

---

## Build & Preview (production)

```bash
curl http://localhost:5000/api/seed
npm run build
npm run preview # or npm start
```

4) Verify:
- Restaurants API: `http://localhost:5000/api/restaurants`
- Frontend: visit the Vite URL printed by `npm run dev`.
---

## Environment configuration

- Recommended: add `VITE_API_URL` to `.env` to avoid hard-coded `http://localhost:3000` strings.

Example `.env`

## Notes
- Bootstrap is included and used sparingly for Navbar, Buttons, Forms and Grid.
- If MongoDB is not running locally, replace `MONGO_URI` in `backend/.env` with your Atlas connection string.
- Do not commit secrets. `backend/.env` in this repository should be replaced with safe values before sharing publicly.
```
VITE_API_URL="http://localhost:3000"
```

## Development tips
- To enable automatic backend reloads, install `nodemon` and run `npx nodemon server.js` from the `backend` folder.
- Linting: `npm run lint` (configured in the root `package.json`).
---

If you want, I can add a GitHub Actions workflow to run tests/lint on push.
# React + Vite
## Linting

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
```bash
npm run lint
```

Currently, two official plugins are available:
---

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
---

## React Compiler
## Configuration hygiene

The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
- Some files reference the API base URL directly (for example, `http://localhost:3000`). For production, centralize the base URL using `VITE_API_URL` and an HTTP client helper (see suggested `src/utils/api.js` above).
- Images are sent as base64; keep payloads within the server limit (default `5MB`) or use dedicated storage (e.g., S3 or Cloudinary) for large media.

## Expanding the ESLint configuration
---

If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
If you'd like, I can open a PR to replace the remaining direct API calls with the centralized helper and add `VITE_API_URL` to the repository's environment templates.
2 changes: 2 additions & 0 deletions client/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import SearchResultsPage from "./pages/SearchResultsPage";
import WriteReviewPage from "./pages/WriteReviewPage";
import Login from "./pages/Login";
import OwnerLoginPage from "./pages/OwnerLoginPage"
import OwnerDashboard from "./pages/OwnerDashboard";
import Signup from "./pages/Signup";
import OwnerSignUp from "./pages/OwnerSignup";
import UserProfilePage from "./pages/UserProfilePage";
Expand Down Expand Up @@ -46,6 +47,7 @@ function App() {
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
<Route path="/OwnerSignUp" element={<OwnerSignUp />} />
<Route path="/OwnerDashboard" element={<OwnerDashboard />} />
<Route path="/OwnerLoginPage" element={<OwnerLoginPage />} />
<Route path="/profile" element={<UserProfilePage />} />
<Route path="/favorites" element={<Favorites />} />
Expand Down
8 changes: 4 additions & 4 deletions client/src/components/home/FeaturedCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,11 @@ import TomocaImg from '../../assets/tomoca.png';
import Placeholder from "../../assets/addis-cafe.jpg";


function FeaturedCard({ restaurant }) {
function FeaturedCard({ restaurant, showDeleteButton }) {

const { toggleFavorite, isFavorite } = useContext(RestaurantsContext);
const { toggleFavorite, isFavorite, deleteRestaurant } = useContext(RestaurantsContext);
const fav = isFavorite ? isFavorite(restaurant._id) : false;

// const inputRef = useRef(null);
// const [uploading, setUploading] = useState(false);

// Default images for known restaurants
const defaults = {
Expand Down Expand Up @@ -80,6 +78,8 @@ function FeaturedCard({ restaurant }) {
<div className="featured-footer">
{restaurant.address && <div className="featured-address">{restaurant.address}</div>}
<Link to={`/restaurants/${restaurant._id}`} className="button button-small">View</Link>
{showDeleteButton && <button onClick={() => deleteRestaurant(restaurant._id)} className="button button-small">delete</button>}

</div>
</div>
</article>
Expand Down
3 changes: 1 addition & 2 deletions client/src/components/layout/Navbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ function Navbar({ onSearch }) {
<nav className="navbar navbar-expand-lg bg-white shadow-sm" style={{ position: "relative", zIndex: 1000 }}>
<div className="container">
<Link to="/" className="navbar-brand">Find Addis</Link>

<form className="d-flex mx-auto w-50" onSubmit={submitSearch}>
<input
className="form-control me-2"
Expand All @@ -59,7 +58,7 @@ function Navbar({ onSearch }) {

<div className="d-flex align-items-center">
<nav className="me-3">
<Link to="/favorites" className="nav-link d-inline">Favorites</Link>
<Link to="/favorites" className="nav-link d-inline">Favorites </Link>
<Link to="/restaurants" className="nav-link d-inline">Restaurants</Link>
</nav>

Expand Down
2 changes: 0 additions & 2 deletions client/src/components/layout/Sidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import React from "react";
import Dropdown from "../common/Dropdown";

function Sidebar({ filters, setFilters, restaurants }) {
console.log("Sidebar", restaurants)
const uniqueCategories = [...new Set(restaurants.map(r => r.category))]
console.log('uniqueCategories', uniqueCategories)
return (
<aside className="sidebar-container">
<div className="filter-block">
Expand Down
107 changes: 107 additions & 0 deletions client/src/components/owner/OwnerAddRestaurantForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import React from "react";
import { useState, useContext } from "react";
import InputField from "../common/InputField";
import Button from "../common/Button";
import AuthContext from "../../context/AuthContext";
import axios from "axios";
import RestaurantsContext from "../../context/RestaurantsContext";

function OwnerRestaurantRegistrationForm() {
const [form, setForm] = useState({ name: "", category: "", address: "", hours: "", description: "", menu: "" });
const [imageDataUrl, setImageDataUrl] = useState(""); // full data URL with prefix
const [imagePreview, setImagePreview] = useState(null);
const { token } = useContext(AuthContext);

function handleChange(e) {
setForm({ ...form, [e.target.name]: e.target.value });
}

const handleImageChange = (e) => {
const file = e.target.files && e.target.files[0];
if (!file) return;

const validTypes = ["image/jpeg", "image/png", "image/gif", "image/webp"];
if (!validTypes.includes(file.type)) {
alert("Please select a valid image (jpg, png, gif, webp).");
return;
}

const maxSize = 3 * 1024 * 1024; // 3MB
if (file.size > maxSize) {
alert("Image is too large. Max size is 3MB.");
return;
}

const reader = new FileReader();
reader.onloadend = () => {
// reader.result is a data URL: data:<type>;base64,<data>
const dataUrl = reader.result;
// store the full data URL (including the data:image/...;base64, prefix)
setImageDataUrl(dataUrl);
setImagePreview(dataUrl);
};
reader.readAsDataURL(file);
};

async function handleSubmit(e) {
e.preventDefault();
try {
const payload = {
name: form.name,
category: form.category,
address: form.address,
hours: form.hours,
description: form.description,
menu: form.menu ? form.menu.split(",").map(item => item.trim()) : [],
images: imageDataUrl ? [imageDataUrl] : [] // send as an array with full data URL prefix
};

const res = await axios.post("http://localhost:3000/api/restaurants",
payload,
{
headers: {
Authorization: `Bearer ${token}`,
}
});

if (res.status >= 200 && res.status < 300) {
alert("Restaurant created successfully!");
// reset form
setForm({ name: "", category: "", address: "", hours: "", description: "", menu: "", price: "" });
setImageDataUrl("");
setImagePreview(null);
} else {
alert(res.data?.message || "Error creating restaurant");
}
} catch (err) {
console.error(err);
alert(err.response?.data?.message || "Server error");
}
}

return (
<form className="signup-form" onSubmit={handleSubmit}>
<InputField name="name" label="Name" value={form.name} onChange={handleChange} placeholder="Restaurant name" />
<InputField name="category" label="Category" value={form.category} onChange={handleChange} placeholder="Ethiopian,Italian..." />
<InputField name="address" label="Address" value={form.address} onChange={handleChange} placeholder="Bole, Addis Ababa" />
<InputField name="hours" label="Hours" type="text" value={form.hours} onChange={handleChange} placeholder="Enter Working hours" />
<InputField name="description" label="Description" value={form.description} onChange={handleChange} placeholder="Enter what you serve" />
<InputField name="menu" label="Menu" type="text" value={form.menu} onChange={handleChange} placeholder="e.g. injera, tibs, coffee" />

<div className="form-group" style={{ marginBottom: 12 }}>
<label htmlFor="image">Profile Image</label>
<input id="image" name="image" type="file" accept="image/*" onChange={handleImageChange} />
{imagePreview && (
<div style={{ marginTop: 8 }}>
<img src={imagePreview} alt="Preview" style={{ width: 150, height: 'auto', display: 'block', borderRadius: 6 }} />
<button type="button" style={{ marginTop: 6 }} onClick={() => { setImagePreview(null); setImageDataUrl(""); }}>Remove</button>
</div>
)}
</div>

<Button type="submit">Add Restaurant</Button>
</form>
);
}

export default OwnerRestaurantRegistrationForm;
2 changes: 1 addition & 1 deletion client/src/components/owner/OwnerSignupForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function OwnerSignupForm({ onSignup }) {
async function handleSubmit(e) {
e.preventDefault();
try {
const res = await fetch("http://localhost:3000/api/RestaurantOwners", {
const res = await fetch("http://localhost:3000/api/owners", {
method: "POST",
headers: {
"Content-Type": "application/json"
Expand Down
Loading