Add microservices architecture with separate frontend/backend
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- Created backend/Dockerfile for Express API server - Created frontend/Dockerfile with Nginx for static files - Added nginx.conf to proxy /api/* to backend - Created docker-compose.microservices.yml for multi-container setup - Added .dockerignore files for both frontend and backend - Updated .woodpecker.yaml to fix registry URL and use separate Dockerfiles - Added CORS support to backend for microservices mode - Updated README with dual-mode deployment instructions - Frontend copies to frontend/public/ for Nginx serving - CI/CD pipeline now builds separate images for frontend and backend
This commit is contained in:
parent
5a7585f755
commit
15b8334f2a
@ -29,7 +29,7 @@ steps:
|
|||||||
path:
|
path:
|
||||||
include: [ backend/** ]
|
include: [ backend/** ]
|
||||||
settings:
|
settings:
|
||||||
registry: harbor-core.dev-tools.svc.cluster.local
|
registry: harbor.dvirlabs.com
|
||||||
repo: my-apps/${CI_REPO_NAME}-backend
|
repo: my-apps/${CI_REPO_NAME}-backend
|
||||||
dockerfile: backend/Dockerfile
|
dockerfile: backend/Dockerfile
|
||||||
context: backend
|
context: backend
|
||||||
|
|||||||
100
README.md
100
README.md
@ -44,17 +44,43 @@ oramap/
|
|||||||
├── backend/
|
├── backend/
|
||||||
│ ├── server.js # Express server
|
│ ├── server.js # Express server
|
||||||
│ ├── package.json # Backend dependencies
|
│ ├── package.json # Backend dependencies
|
||||||
|
│ ├── Dockerfile # Backend container image
|
||||||
│ └── data/
|
│ └── data/
|
||||||
│ └── families.json # Family location data
|
│ └── families.json # Family location data
|
||||||
|
├── frontend/
|
||||||
|
│ ├── Dockerfile # Frontend Nginx container
|
||||||
|
│ ├── nginx.conf # Nginx configuration
|
||||||
|
│ └── public/
|
||||||
|
│ ├── index.html # Frontend HTML
|
||||||
|
│ ├── script.js # Frontend JavaScript
|
||||||
|
│ └── style.css # Styles
|
||||||
├── public/
|
├── public/
|
||||||
│ ├── index.html # Frontend HTML
|
│ ├── index.html # Shared frontend files
|
||||||
│ ├── script.js # Frontend JavaScript
|
│ ├── script.js
|
||||||
│ └── style.css # Styles
|
│ └── style.css
|
||||||
├── Dockerfile # Multi-stage Docker build
|
├── Dockerfile # Monolith Docker build
|
||||||
├── docker-compose.yml # Docker Compose configuration
|
├── docker-compose.yml # Monolith deployment
|
||||||
└── .dockerignore # Docker ignore rules
|
├── docker-compose.microservices.yml # Microservices deployment
|
||||||
|
├── .woodpecker.yaml # CI/CD pipeline config
|
||||||
|
└── .dockerignore # Docker ignore rules
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 🏗️ Architecture
|
||||||
|
|
||||||
|
The application supports two deployment modes:
|
||||||
|
|
||||||
|
### 1. **Monolith Mode** (Simple)
|
||||||
|
- Single container with Express serving both API and static files
|
||||||
|
- Best for: Development, simple deployments
|
||||||
|
- Use: `docker-compose up`
|
||||||
|
|
||||||
|
### 2. **Microservices Mode** (Production)
|
||||||
|
- **Frontend**: Nginx serving static files (Port 80)
|
||||||
|
- **Backend**: Express API server (Internal Port 3000)
|
||||||
|
- Nginx proxies `/api/*` requests to backend
|
||||||
|
- Best for: Production, scalability, CI/CD pipelines
|
||||||
|
- Use: `docker-compose -f docker-compose.microservices.yml up`
|
||||||
|
|
||||||
## 🚀 Getting Started
|
## 🚀 Getting Started
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
@ -79,40 +105,76 @@ Navigate to `http://localhost:3000`
|
|||||||
|
|
||||||
## 🐳 Docker Deployment
|
## 🐳 Docker Deployment
|
||||||
|
|
||||||
### Using Docker Compose (Recommended)
|
### Monolith Mode (Recommended for Development)
|
||||||
|
|
||||||
1. **Build and start the application:**
|
1. **Build and start:**
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **View logs:**
|
2. **Access:** http://localhost:3000
|
||||||
```bash
|
|
||||||
docker-compose logs -f
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Stop the application:**
|
3. **Stop:**
|
||||||
```bash
|
```bash
|
||||||
docker-compose down
|
docker-compose down
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using Docker directly
|
### Microservices Mode (Recommended for Production)
|
||||||
|
|
||||||
1. **Build the image:**
|
1. **Build and start:**
|
||||||
```bash
|
```bash
|
||||||
docker build -t oramap:latest .
|
docker-compose -f docker-compose.microservices.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Run the container:**
|
2. **Access:** http://localhost (Port 80)
|
||||||
|
|
||||||
|
3. **View logs:**
|
||||||
```bash
|
```bash
|
||||||
|
docker logs oramap-frontend
|
||||||
|
docker logs oramap-backend
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Stop:**
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.microservices.yml down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Docker directly
|
||||||
|
|
||||||
|
**Monolith:**
|
||||||
|
```bash
|
||||||
|
docker build -t oramap:latest .
|
||||||
docker run -d -p 3000:3000 --name oramap-app oramap:latest
|
docker run -d -p 3000:3000 --name oramap-app oramap:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Check health:**
|
**Microservices:**
|
||||||
```bash
|
```bash
|
||||||
docker ps
|
# Build images
|
||||||
|
docker build -t oramap-backend -f backend/Dockerfile backend/
|
||||||
|
docker build -t oramap-frontend -f frontend/Dockerfile frontend/
|
||||||
|
|
||||||
|
# Run with network
|
||||||
|
docker network create oramap-network
|
||||||
|
docker run -d --name oramap-backend --network oramap-network oramap-backend
|
||||||
|
docker run -d --name oramap-frontend --network oramap-network -p 80:80 oramap-frontend
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 🔄 CI/CD Pipeline
|
||||||
|
|
||||||
|
The project includes a Woodpecker CI configuration (`.woodpecker.yaml`) that automatically:
|
||||||
|
|
||||||
|
1. **Builds** separate frontend and backend Docker images on push
|
||||||
|
2. **Tags** images with branch name and commit SHA
|
||||||
|
3. **Pushes** to Harbor registry (`harbor.dvirlabs.com`)
|
||||||
|
4. **Updates** Kubernetes manifests with new image tags
|
||||||
|
5. **Triggers** on changes to `frontend/**` or `backend/**` paths
|
||||||
|
|
||||||
|
### Pipeline Steps:
|
||||||
|
- `build-frontend`: Build and push frontend Nginx image
|
||||||
|
- `build-backend`: Build and push backend API image
|
||||||
|
- `update-values-frontend`: Update frontend image tag in values.yaml
|
||||||
|
- `update-values-backend`: Update backend image tag in values.yaml
|
||||||
|
|
||||||
## 💻 Development
|
## 💻 Development
|
||||||
|
|
||||||
### Running in Development Mode
|
### Running in Development Mode
|
||||||
|
|||||||
28
backend/.dockerignore
Normal file
28
backend/.dockerignore
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
*.md
|
||||||
|
|
||||||
|
# Data not needed in image (using volume or copy)
|
||||||
|
# Keep data/ folder as it's needed
|
||||||
30
backend/Dockerfile
Normal file
30
backend/Dockerfile
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Backend Dockerfile for Ora Map
|
||||||
|
FROM node:20-alpine AS base
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM base AS production
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install production dependencies
|
||||||
|
RUN npm install --production
|
||||||
|
|
||||||
|
# Copy application files
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=3000
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD node -e "require('http').get('http://localhost:3000/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
CMD ["node", "server.js"]
|
||||||
35
backend/server-microservice.js
Normal file
35
backend/server-microservice.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const cors = require('cors');
|
||||||
|
const path = require('path');
|
||||||
|
const app = express();
|
||||||
|
const families = require('./data/families.json');
|
||||||
|
|
||||||
|
// Enable CORS for frontend
|
||||||
|
app.use(cors());
|
||||||
|
|
||||||
|
// Serve static files from the public directory (for standalone mode)
|
||||||
|
if (process.env.SERVE_STATIC === 'true') {
|
||||||
|
app.use(express.static(path.join(__dirname, '../public')));
|
||||||
|
}
|
||||||
|
|
||||||
|
// API endpoint for family search
|
||||||
|
app.get('/api/search', (req, res) => {
|
||||||
|
const query = req.query.family?.toLowerCase();
|
||||||
|
if (!query) {
|
||||||
|
return res.json([]);
|
||||||
|
}
|
||||||
|
const matches = families.filter(fam => fam.family.toLowerCase().includes(query));
|
||||||
|
res.json(matches);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Health check endpoint
|
||||||
|
app.get('/api/health', (req, res) => {
|
||||||
|
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||||
|
});
|
||||||
|
|
||||||
|
const port = process.env.PORT || 3000;
|
||||||
|
app.listen(port, '0.0.0.0', () => {
|
||||||
|
console.log(`🗺️ Ora Map Backend API running at http://localhost:${port}`);
|
||||||
|
console.log(`📍 Search endpoint: http://localhost:${port}/api/search`);
|
||||||
|
console.log(`💚 Health endpoint: http://localhost:${port}/api/health`);
|
||||||
|
});
|
||||||
48
docker-compose.microservices.yml
Normal file
48
docker-compose.microservices.yml
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Backend API Service
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: oramap-backend:latest
|
||||||
|
container_name: oramap-backend
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- PORT=3000
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
start_period: 5s
|
||||||
|
networks:
|
||||||
|
- oramap-network
|
||||||
|
|
||||||
|
# Frontend Nginx Service
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: oramap-frontend:latest
|
||||||
|
container_name: oramap-frontend
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
depends_on:
|
||||||
|
backend:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
start_period: 5s
|
||||||
|
networks:
|
||||||
|
- oramap-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
oramap-network:
|
||||||
|
driver: bridge
|
||||||
31
frontend/.dockerignore
Normal file
31
frontend/.dockerignore
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Frontend .dockerignore
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Source files (not needed for production)
|
||||||
|
src/
|
||||||
|
|
||||||
|
# Build files
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
README.md
|
||||||
|
*.md
|
||||||
18
frontend/Dockerfile
Normal file
18
frontend/Dockerfile
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Frontend Dockerfile for Ora Map (Static files with Nginx)
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Copy static files to nginx html directory
|
||||||
|
COPY public/ /usr/share/nginx/html/
|
||||||
|
|
||||||
|
# Copy custom nginx configuration
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1
|
||||||
|
|
||||||
|
# Start nginx
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
34
frontend/nginx.conf
Normal file
34
frontend/nginx.conf
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Gzip compression
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||||
|
|
||||||
|
# Main location
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# API proxy to backend
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://backend:3000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache static assets
|
||||||
|
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
}
|
||||||
26
frontend/public/index.html
Normal file
26
frontend/public/index.html
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>🗺️ Ora</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>🗺️ Ora</h1>
|
||||||
|
<div class="search-bar">
|
||||||
|
<input type="text" id="searchInput" placeholder="Enter family name..." />
|
||||||
|
<button onclick="searchFamily()">Search</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div id="map"></div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/fuse.js/dist/fuse.min.js"></script>
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
frontend/public/logo-shay.png
Normal file
BIN
frontend/public/logo-shay.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 975 KiB |
BIN
frontend/public/logo.png
Normal file
BIN
frontend/public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
124
frontend/public/script.js
Normal file
124
frontend/public/script.js
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
let map = L.map('map').setView([15.5527, 48.5164], 6);
|
||||||
|
|
||||||
|
// Define the two tile layers
|
||||||
|
const voyager = L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', {
|
||||||
|
attribution: '© OpenStreetMap contributors © CARTO',
|
||||||
|
subdomains: 'abcd',
|
||||||
|
maxZoom: 19
|
||||||
|
});
|
||||||
|
|
||||||
|
const openStreetMap = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: 'Map data © OpenStreetMap contributors',
|
||||||
|
maxZoom: 19
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add the default layer (Voyager)
|
||||||
|
voyager.addTo(map);
|
||||||
|
|
||||||
|
// Group the layers for switching
|
||||||
|
const baseMaps = {
|
||||||
|
"English Map (CartoDB Voyager)": voyager,
|
||||||
|
"Original Map (OpenStreetMap)": openStreetMap
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the layer control to the map
|
||||||
|
L.control.layers(baseMaps).addTo(map);
|
||||||
|
|
||||||
|
async function searchFamily() {
|
||||||
|
const familyName = document.getElementById('searchInput').value.trim();
|
||||||
|
if (!familyName) {
|
||||||
|
alert('Please enter a family name.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/search?family=${encodeURIComponent(familyName)}`);
|
||||||
|
const familyResult = await response.json();
|
||||||
|
|
||||||
|
if (!familyResult.length) {
|
||||||
|
alert('No matching family found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearMarkers();
|
||||||
|
|
||||||
|
familyResult.forEach(record => {
|
||||||
|
L.marker([record.lat, record.lng]).addTo(map)
|
||||||
|
.bindPopup(`<strong>${record.family}</strong><br>City: ${record.city}`)
|
||||||
|
.openPopup();
|
||||||
|
});
|
||||||
|
|
||||||
|
const first = familyResult[0];
|
||||||
|
map.setView([first.lat, first.lng], 7);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Search error:', error);
|
||||||
|
alert('Something went wrong while searching. Please try again later.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearMarkers() {
|
||||||
|
map.eachLayer(layer => {
|
||||||
|
if (layer instanceof L.Marker) {
|
||||||
|
map.removeLayer(layer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const familyNames = [
|
||||||
|
"Kafe (קאפח)", "Shiheb (שחב-שבח)", "Eraki (עראקי)", "Salumi (סלומי-שלומי)",
|
||||||
|
"Afgin (עפג'ין)", "Uzeyri (עזירי-עוזרי)"
|
||||||
|
// Add more families here if you like
|
||||||
|
];
|
||||||
|
|
||||||
|
// Initialize Fuse.js
|
||||||
|
const fuse = new Fuse(familyNames, {
|
||||||
|
includeScore: true,
|
||||||
|
threshold: 0.4 // Lower = stricter matching
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchInput = document.getElementById('searchInput');
|
||||||
|
const suggestionsBox = document.createElement('div');
|
||||||
|
suggestionsBox.classList.add('suggestions');
|
||||||
|
searchInput.parentNode.style.position = 'relative'; // Make parent relative
|
||||||
|
searchInput.parentNode.appendChild(suggestionsBox);
|
||||||
|
|
||||||
|
searchInput.addEventListener('input', () => {
|
||||||
|
const value = searchInput.value.trim();
|
||||||
|
suggestionsBox.innerHTML = '';
|
||||||
|
|
||||||
|
if (!value) return;
|
||||||
|
|
||||||
|
const results = fuse.search(value);
|
||||||
|
|
||||||
|
results.forEach(result => {
|
||||||
|
const option = document.createElement('div');
|
||||||
|
option.textContent = result.item;
|
||||||
|
option.onclick = () => {
|
||||||
|
searchInput.value = result.item;
|
||||||
|
suggestionsBox.innerHTML = '';
|
||||||
|
};
|
||||||
|
suggestionsBox.appendChild(option);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
L.control.logo = function (opts) {
|
||||||
|
return new L.Control.Logo(opts);
|
||||||
|
};
|
||||||
|
|
||||||
|
L.Control.Logo = L.Control.extend({
|
||||||
|
onAdd: function () {
|
||||||
|
const div = L.DomUtil.create('div', 'custom-logo');
|
||||||
|
div.innerHTML = `
|
||||||
|
<img src="logo.png" alt="Logo" style="height: 40px; vertical-align: middle;">
|
||||||
|
<span style="margin-left: 8px; font-weight: bold; color: #333;">Shevach</span>
|
||||||
|
`;
|
||||||
|
return div;
|
||||||
|
},
|
||||||
|
|
||||||
|
onRemove: function () {
|
||||||
|
// Nothing to clean up
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
L.control.logo({ position: 'bottomleft' }).addTo(map);
|
||||||
98
frontend/public/style.css
Normal file
98
frontend/public/style.css
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
/* Reset some basic styles */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: #f4f4f4;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header styles */
|
||||||
|
header {
|
||||||
|
background-color: #2c3e50;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar input {
|
||||||
|
padding: 10px;
|
||||||
|
width: 250px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar button {
|
||||||
|
padding: 10px 15px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #3498db;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions {
|
||||||
|
background: #363251;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
position: absolute;
|
||||||
|
top: 110%; /* Just below the input */
|
||||||
|
left: 48%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
max-width: fit-content;
|
||||||
|
min-width: 200px; /* Optional: minimum size */
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions div {
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions div:hover {
|
||||||
|
background-color: #95a1a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-logo {
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 8px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Map container */
|
||||||
|
#map {
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 80vh;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main container */
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
45
frontend/src/components/Search.jsx
Normal file
45
frontend/src/components/Search.jsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
function Search() {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
const [results, setResults] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (query.trim() === '') {
|
||||||
|
setResults([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchSuggestions = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/search?q=${encodeURIComponent(query)}`);
|
||||||
|
const data = await res.json();
|
||||||
|
setResults(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch suggestions:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchSuggestions();
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="search-container">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search..."
|
||||||
|
value={query}
|
||||||
|
onChange={e => setQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
{results.length > 0 && (
|
||||||
|
<div className="autocomplete-box">
|
||||||
|
{results.map((res, i) => (
|
||||||
|
<div key={i} className="suggestion">{res}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Search;
|
||||||
Loading…
x
Reference in New Issue
Block a user