Compare commits

..

2 Commits

Author SHA1 Message Date
e42c62820b Add README.md 2025-06-05 03:53:54 +03:00
f884de3b3a Convert vanila javascript to vite+react 2025-06-05 03:49:43 +03:00
61 changed files with 3465 additions and 2485 deletions

View File

@ -1,50 +0,0 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Git
.git/
.gitignore
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Docker
Dockerfile
docker-compose.yml
.dockerignore
# Kubernetes
oramap/
# Build artifacts
dist/
build/
# Other
old-project/
frontend/node_modules/
README copy.md
*.md
!backend/
!public/
run_ora_map.sh
nfsshare/
nul

19
.gitignore vendored
View File

@ -1,19 +0,0 @@
node_modules/
package-lock.json
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
nul
# Old files
old/
old-project/
oramap-chart/

View File

@ -1,121 +0,0 @@
steps:
build-frontend:
name: Build & Push Frontend
image: woodpeckerci/plugin-kaniko
when:
branch: [ master, develop ]
event: [ push, pull_request, tag ]
path:
include: [ frontend/** ]
settings:
registry: harbor.dvirlabs.com
repo: my-apps/${CI_REPO_NAME}-frontend
dockerfile: frontend/Dockerfile
context: frontend
tags:
- latest
- ${CI_COMMIT_TAG:-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}}
username:
from_secret: DOCKER_USERNAME
password:
from_secret: DOCKER_PASSWORD
build-backend:
name: Build & Push Backend
image: woodpeckerci/plugin-kaniko
when:
branch: [ master, develop ]
event: [ push, pull_request, tag ]
path:
include: [ backend/** ]
settings:
registry: harbor.dvirlabs.com
repo: my-apps/${CI_REPO_NAME}-backend
dockerfile: backend/Dockerfile
context: backend
tags:
- latest
- ${CI_COMMIT_TAG:-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}}
username:
from_secret: DOCKER_USERNAME
password:
from_secret: DOCKER_PASSWORD
update-values-frontend:
name: Update frontend tag in values.yaml
image: alpine:3.19
when:
branch: [ master, develop ]
event: [ push ]
path:
include: [ frontend/** ]
environment:
GIT_USERNAME:
from_secret: GIT_USERNAME
GIT_TOKEN:
from_secret: GIT_TOKEN
commands:
- apk add --no-cache git yq
- git config --global user.name "woodpecker-bot"
- git config --global user.email "ci@dvirlabs.com"
- git clone "https://$${GIT_USERNAME}:$${GIT_TOKEN}@git.dvirlabs.com/dvirlabs/my-apps.git"
- cd my-apps
- |
TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}"
echo "💡 Setting frontend tag to: $TAG"
yq -i ".frontend.image.tag = \"$TAG\"" manifests/${CI_REPO_NAME}/values.yaml
git add manifests/${CI_REPO_NAME}/values.yaml
git commit -m "frontend: update tag to $TAG" || echo "No changes"
git push origin HEAD
update-values-backend:
name: Update backend tag in values.yaml
image: alpine:3.19
when:
branch: [ master, develop ]
event: [ push ]
path:
include: [ backend/** ]
environment:
GIT_USERNAME:
from_secret: GIT_USERNAME
GIT_TOKEN:
from_secret: GIT_TOKEN
commands:
- apk add --no-cache git yq
- git config --global user.name "woodpecker-bot"
- git config --global user.email "ci@dvirlabs.com"
- |
if [ ! -d "my-apps" ]; then
git clone "https://$${GIT_USERNAME}:$${GIT_TOKEN}@git.dvirlabs.com/dvirlabs/my-apps.git"
fi
- cd my-apps
- |
TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}"
echo "💡 Setting backend tag to: $TAG"
yq -i ".backend.image.tag = \"$TAG\"" manifests/${CI_REPO_NAME}/values.yaml
git add manifests/${CI_REPO_NAME}/values.yaml
git commit -m "backend: update tag to $TAG" || echo "No changes"
git push origin HEAD
trigger-gitops-via-push:
when:
branch: [ master, develop ]
event: [ push ]
name: Trigger apps-gitops via Git push
image: alpine/git
environment:
GIT_USERNAME:
from_secret: GIT_USERNAME
GIT_TOKEN:
from_secret: GIT_TOKEN
commands: |
git config --global user.name "woodpecker-bot"
git config --global user.email "ci@dvirlabs.com"
git clone "https://$${GIT_USERNAME}:$${GIT_TOKEN}@git.dvirlabs.com/dvirlabs/apps-gitops.git"
cd apps-gitops
echo "# trigger at $(date) by $${CI_REPO_NAME}" >> .trigger
git add .trigger
git commit -m "ci: trigger apps-gitops build" || echo "no changes"
git push origin HEAD

View File

@ -1,35 +1,10 @@
# Multi-stage build for Ora Map Application
FROM node:20-alpine AS base
# Production stage
FROM base AS production
FROM node:24-slim
WORKDIR /app
# Copy backend package files
COPY backend/package*.json ./backend/
WORKDIR /app/backend
RUN npm install --production
COPY package*.json ./
RUN npm install
# Copy backend source and data
COPY backend/ ./
WORKDIR /app
COPY . .
# Copy public frontend files
COPY public/ ./public/
# Expose port
EXPOSE 3000
# Set environment variable
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
WORKDIR /app/backend
CMD ["node", "server.js"]

406
README.md
View File

@ -1,404 +1,6 @@
# 🗺️ Ora Map - Family Location Mapping Application
Start the Server:
Just start the main.py using vscode or pyhotn path-to-main.py
A full-stack web application for mapping and searching family locations in Yemen using interactive maps.
## 📋 Table of Contents
- [Features](#features)
- [Tech Stack](#tech-stack)
- [Project Structure](#project-structure)
- [Getting Started](#getting-started)
- [Docker Deployment](#docker-deployment)
- [Development](#development)
- [API Endpoints](#api-endpoints)
## ✨ Features
- 🔍 **Family Search**: Search for families by name with autocomplete suggestions
- 🗺️ **Interactive Map**: Leaflet-based map with multiple tile layer options
- 📍 **Location Markers**: View family locations with city information
- **Add Families**: Admin UI to add new family locations to the database
- 💾 **MongoDB Database**: Real database persistence with full CRUD operations
- 🎨 **Modern UI**: Clean and responsive design with modal forms
- 🐳 **Docker Ready**: Containerized for easy deployment
- ☸️ **Kubernetes Ready**: Helm chart for production deployment
- 💚 **Health Checks**: Built-in health monitoring
## 🛠️ Tech Stack
**Backend:**
- Node.js 20 Alpine
- Express.js 4.18
- MongoDB 7.0
- Mongoose ODM
- CORS enabled
**Frontend:**
- HTML5/CSS3
- Vanilla JavaScript (ES6+)
- Leaflet.js (interactive maps)
- Fuse.js (fuzzy search)
- Nginx (production server)
**DevOps:**
- Docker & Docker Compose
- Kubernetes & Helm Charts
- Woodpecker CI/CD
- Harbor Registry
## 📁 Project Structure with MongoDB
│ ├── package.json # Backend dependencies
│ ├── Dockerfile # Backend container image
│ ├── config/
│ │ └── database.js # MongoDB connection
│ ├── models/
│ │ └── Family.js # Mongoose schema
│ └── scripts/
│ └── seed.js # Database seeding script
├── frontend/
│ ├── Dockerfile # Frontend Nginx container
│ ├── nginx.conf # Nginx configuration
│ └── public/
│ ├── index.html # Frontend HTML with modal form
│ ├── script.js # Frontend JavaScript with CRUD
│ └── style.css # Styles including modal
├── oramap/ # Helm Chart
│ ├── Chart.yaml # Chart metadata (v0.3.0)
│ ├── values.yaml # Configuration values
│ ├── README.md # Helm documentation
│ └── templates/
│ ├── deployment.yaml # Backend & Frontend deployments
│ ├── service.yaml # Services
│ ├── ingress.yaml # Ingress rules
│ ├── configmap.yaml # Nginx config
│ ├── mongodb-statefulset.yaml # MongoDB StatefulSet
│ └── mongodb-service.yaml # MongoDB service
├── old/ # Legacy files
│ ├── server.js
│ ├── package.json
│ └── data/families.json
├── Dockerfile # Monolith Docker build
├── docker-compose.yml # Monolith deployment
├── docker-compose.microservices.yml # Microservices with MongoDB
│ ├── script.js
│ └── style.css
├── Dockerfile # Monolith Docker build
├── docker-compose.yml # Monolith deployment
├── MongoDB**: Database for family locations (Port 27017)
- **Backend**: Express API server with MongoDB integration (Internal Port 3000)
- **Frontend**: Nginx serving static files with API proxy (Port 80)
- Nginx proxies `/api/*` requests to backend
- Best for: Production, scalability, CI/CD pipelines
- Use: `docker-compose -f docker-compose.microservices.yml up`
### 3. **Kubernetes Mode** (Enterprise)
- Helm chart deployment with StatefulSet for MongoDB
- Persistent storage for database
- Horizontal scaling for backend/frontend
- Ingress with TLS support
- See: `oramap/README.md` for Helm chart documentation
## 🏗️ 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
### Prerequisites
- Node.js (v18 or higher)
- Docker & Docker Compose (optional, for containerized deployment)
### Local Development
1. **Start MongoDB (optional - uses local instance if not running):**
```bash
# Using Docker
docker run -d -p 27017:27017 --name mongodb mongo:7.0
# Or install MongoDB locally
```
2. **Install backend dependencies:**
```bash
cd backend
npm install
```
3. **Seed initial data:**
```bash
npm run seed
```
4. **Start the server:**
```bash
npm start
# Or for development with auto-reload:
Start the Client:
cd frontend
npm run dev
```
5. **Open your browser:**
Navigate to `http://localhost:3000`
## 🐳 Docker Deployment
### Monolith Mode (Recommended for Development)
1. **Build and start:**
```bash
docker-compose up -d
```
2. **Access:** http://localhost:3000
3. **Stop:**
```bash
docker-compose down
```
### Microservices Mode (Recommended for Production)
1. **Build and start all services (Backend + Frontend + MongoDB):**
```bash
docker-compose -f docker-compose.microservices.yml up -d
```
2. **Seed the database:**
```bash
docker exec -it oramap-backend npm run seed
```
3. **Access:** http://localhost (Port 80)
4. **View logs:**
```bash
docker logs oramap-frontend
docker logs oramap-backend
docker logs oramap-mongo
```
5. **Stop:**
```bash
docker-compose -f docker-compose.microservices.yml down
# To remove volumes:
docker-compose -f docker-compose.microservices.yml down -v
```
### Using Docker directly
**Monolith:**
```bash
docker build -t oramap:latest .
docker run -d -p 3000:3000 --name oramap-app oramap:latest
```
**Microservices:**
```bash
# 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
### Running in Development Mode
```bash
cd backend
npm run dev
```
### Database Management
**Seed initial data:**
```bash
npm run seed
```
**Force re-seed (clears existing data):**
```bash
npm run seed:force
```
### Adding New Families
Use the web UI "Add Family" button, or make API calls:
```bash
curl -X POST http://localhost:3000/api/families \
-H "Content-Type: application/json" \
-d '{
"family": "Family Name",
"city": "City Name",
"lat": 15.3545,
"lng": 44.2064
}'
```
## 📡 API Endpoints
### Get All Families
```
GET /api/families
```
Returns all families sorted by name.
**Example:**
```bash
curl http://localhost:3000/api/families
```
**Response:**
```json
[
{
"_id": "507f1f77bcf86cd799439011",
"family": "משפחת כפה",
"city": "צנעא",
"lat": 15.3545,
"lng": 44.2064,
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z"
}
]
```
### Search Families
```
GET /api/search?family={familyName}
```
Returns matching family records using MongoDB text search.
**Example:**
```bash
curl "http://localhost:3000/api/search?family=Kafe"
```
### Create Family
```
POST /api/families
Content-Type: application/json
```
Creates a new family location.
**Request Body:**
```json
{
"family": "משפחת דוד",
"city": "עדן",
"lat": 12.7855,
"lng": 45.0187
}
```
**Response:**
```json
{
"_id": "507f1f77bcf86cd799439011",
"family": "משפחת דוד",
"city": "עדן",
"lat": 12.7855,
"lng": 45.0187,
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z"
}
```
### Update Family
```
PUT /api/families/:id
Content-Type: application/json
```
Updates an existing family.
**Request Body:**
```json
{
"family": "משפחת כהן",
"city": "צנעא",
"lat": 15.3695,
"lng": 44.1910
}
```
### Delete Family
```
DELETE /api/families/:id
```
Deletes a family by ID.
**Example:**
```bash
curl -X DELETE http://localhost:3000/api/families/507f1f77bcf86cd799439011
```
### Health Check
```
GET /api/health
```
Returns server and database health status.
**Response:**
```json
{
"status": "ok",
"timestamp": "2024-01-01T00:00:00.000Z",
"database": "connected"
}
```
Returns server health status.
**Example Response:**
```json
{
"status": "ok",
"timestamp": "2026-03-24T10:30:00.000Z"
}
```
## 🗺️ Available Family Names
- Kafe (קאפח)
- Shiheb (שחב-שבח)
- Uzeyri (עזירי-עוזרי)
- Salumi (סלומי-שלומי)
- Afgin (עפג'ין)
- Eraki (עראקי)
## 📝 License
ISC
## 🤝 Contributing
Contributions, issues, and feature requests are welcome!

View File

@ -1,28 +0,0 @@
# 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

View File

@ -1,30 +0,0 @@
# 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"]

Binary file not shown.

View File

@ -1,20 +0,0 @@
const mongoose = require('mongoose');
const connectDB = async () => {
try {
const mongoURI = process.env.MONGODB_URI || 'mongodb://localhost:27017/oramap';
await mongoose.connect(mongoURI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log('✅ MongoDB connected successfully');
console.log(`📍 Database: ${mongoose.connection.name}`);
} catch (error) {
console.error('❌ MongoDB connection error:', error.message);
process.exit(1);
}
};
module.exports = connectDB;

33
backend/main.py Normal file
View File

@ -0,0 +1,33 @@
from fastapi import FastAPI, Query
from fastapi.middleware.cors import CORSMiddleware
import json
from pathlib import Path
app = FastAPI()
# CORS configuration
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Load data once
data_file = Path(__file__).resolve().parent.parent / "data" / "families.json"
with data_file.open("r", encoding="utf-8") as f:
families = json.load(f)
@app.get("/search")
def search_families(family: str = Query(..., min_length=1)):
query = family.lower()
matches = [f for f in families if query in f["family"].lower()]
return matches
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

View File

@ -1,33 +0,0 @@
const mongoose = require('mongoose');
const familySchema = new mongoose.Schema({
family: {
type: String,
required: true,
trim: true
},
city: {
type: String,
required: true,
trim: true
},
lat: {
type: Number,
required: true,
min: -90,
max: 90
},
lng: {
type: Number,
required: true,
min: -180,
max: 180
}
}, {
timestamps: true
});
// Index for faster searches
familySchema.index({ family: 'text', city: 'text' });
module.exports = mongoose.model('Family', familySchema);

View File

@ -1,20 +0,0 @@
{
"name": "oramap-backend",
"version": "1.0.0",
"description": "Ora Map Backend Server",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "node server.js",
"seed": "node scripts/seed.js",
"seed:force": "node scripts/seed.js --force"
},
"keywords": ["map", "family", "leaflet", "express"],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"mongoose": "^7.6.0"
}
}

View File

@ -1,59 +0,0 @@
const mongoose = require('mongoose');
const Family = require('../models/Family');
const initialData = [
{ "family": "Kafe (קאפח)", "city": "Sana'a (צנעא)", "lat": 15.3545, "lng": 44.2064 },
{ "family": "Shiheb (שחב-שבח)", "city": "Sana'a (צנעא)", "lat": 15.3545, "lng": 44.2064 },
{ "family": "Uzeyri (עזירי-עוזרי)", "city": "Sana'a (צנעא)", "lat": 15.3545, "lng": 44.2064 },
{ "family": "Uzeyri (עזירי-עוזרי)", "city": "Manakhah (מנאכה)", "lat": 15.3019, "lng": 43.5983 },
{ "family": "Uzeyri (עזירי-עוזרי)", "city": "Dhamar (ד'מאר)", "lat": 14.5424, "lng": 44.4056 },
{ "family": "Salumi (סלומי-שלומי)", "city": "Al Kafla (אל קפלה)", "lat": 16.0240, "lng": 43.9790 },
{ "family": "Afgin (עפג'ין)", "city": "Sa'dah (צעדה)", "lat": 16.9402, "lng": 43.7639 },
{ "family": "Eraki (עראקי)", "city": "Sana'a (צנעא)", "lat": 15.3545, "lng": 44.2064 }
];
async function seedDatabase() {
try {
const mongoURI = process.env.MONGODB_URI || 'mongodb://localhost:27017/oramap';
await mongoose.connect(mongoURI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log('✅ Connected to MongoDB');
// Check if data already exists
const existingCount = await Family.countDocuments();
if (existingCount > 0) {
console.log(` Database already has ${existingCount} families`);
const answer = process.argv.includes('--force');
if (answer) {
console.log('🗑️ Clearing existing data...');
await Family.deleteMany({});
} else {
console.log(' Skipping seed. Use --force flag to override existing data');
process.exit(0);
}
}
// Insert initial data
console.log('📝 Seeding database...');
const result = await Family.insertMany(initialData);
console.log(`✅ Successfully seeded ${result.length} families`);
console.log('\nAdded families:');
result.forEach(family => {
console.log(` - ${family.family} | ${family.city}`);
});
process.exit(0);
} catch (error) {
console.error('❌ Seed error:', error);
process.exit(1);
}
}
seedDatabase();

View File

@ -1,35 +0,0 @@
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`);
});

View File

@ -1,174 +0,0 @@
const express = require('express');
const cors = require('cors');
const path = require('path');
const connectDB = require('./config/database');
const Family = require('./models/Family');
const app = express();
// Enable CORS for frontend
app.use(cors());
// Body parser middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Request logging middleware
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
if (req.method === 'POST' || req.method === 'PUT') {
console.log(' Body:', JSON.stringify(req.body));
}
next();
});
// Connect to MongoDB
connectDB();
// 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', async (req, res) => {
try {
const query = req.query.family?.toLowerCase();
if (!query) {
return res.json([]);
}
// Escape special regex characters to allow literal search
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// Search by family name (case-insensitive)
const matches = await Family.find({
family: { $regex: escapedQuery, $options: 'i' }
}).select('-__v -createdAt -updatedAt');
res.json(matches);
} catch (error) {
console.error('Search error:', error);
res.status(500).json({ error: 'Search failed', message: error.message });
}
});
// Get all families
app.get('/api/families', async (req, res) => {
try {
const families = await Family.find()
.select('-__v -createdAt -updatedAt')
.sort({ family: 1 });
res.json(families);
} catch (error) {
console.error('Get families error:', error);
res.status(500).json({ error: 'Failed to fetch families', message: error.message });
}
});
// Create new family
app.post('/api/families', async (req, res) => {
try {
console.log('📝 POST /api/families - Creating new family');
const { family, city, lat, lng } = req.body;
console.log(` Data: ${family}, ${city}, (${lat}, ${lng})`);
// Validation
if (!family || !city || lat === undefined || lng === undefined) {
console.log(' ❌ Validation failed: Missing fields');
return res.status(400).json({
error: 'Missing required fields',
required: ['family', 'city', 'lat', 'lng'],
received: { family: !!family, city: !!city, lat: lat !== undefined, lng: lng !== undefined }
});
}
// Validate coordinates
if (lat < -90 || lat > 90 || lng < -180 || lng > 180) {
console.log(' ❌ Validation failed: Invalid coordinates');
return res.status(400).json({
error: 'Invalid coordinates',
message: 'Latitude must be between -90 and 90, Longitude between -180 and 180'
});
}
const newFamily = new Family({ family, city, lat, lng });
await newFamily.save();
console.log(` ✅ Family created successfully: ${newFamily._id}`);
res.status(201).json({
message: 'Family added successfully',
family: newFamily
});
} catch (error) {
console.error('❌ Create family error:', error);
res.status(500).json({ error: 'Failed to create family', message: error.message });
}
});
// Update family
app.put('/api/families/:id', async (req, res) => {
try {
const { id } = req.params;
const { family, city, lat, lng } = req.body;
const updatedFamily = await Family.findByIdAndUpdate(
id,
{ family, city, lat, lng },
{ new: true, runValidators: true }
);
if (!updatedFamily) {
return res.status(404).json({ error: 'Family not found' });
}
res.json({
message: 'Family updated successfully',
family: updatedFamily
});
} catch (error) {
console.error('Update family error:', error);
res.status(500).json({ error: 'Failed to update family', message: error.message });
}
});
// Delete family
app.delete('/api/families/:id', async (req, res) => {
try {
const { id } = req.params;
const deletedFamily = await Family.findByIdAndDelete(id);
if (!deletedFamily) {
return res.status(404).json({ error: 'Family not found' });
}
res.json({
message: 'Family deleted successfully',
family: deletedFamily
});
} catch (error) {
console.error('Delete family error:', error);
res.status(500).json({ error: 'Failed to delete family', message: error.message });
}
});
// Health check endpoint
app.get('/api/health', (req, res) => {
const mongoose = require('mongoose');
const dbStatus = mongoose.connection.readyState === 1 ? 'connected' : 'disconnected';
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
database: dbStatus
});
});
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`);
});

View File

@ -1,76 +0,0 @@
version: '3.8'
services:
# MongoDB Database Service
mongo:
image: mongo:7.0
container_name: oramap-mongo
environment:
- MONGO_INITDB_DATABASE=oramap
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db
restart: unless-stopped
healthcheck:
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
networks:
- oramap-network
# Backend API Service
backend:
build:
context: ./backend
dockerfile: Dockerfile
image: harbor.dvirlabs.com/my-apps/oramap-backend:latest
container_name: oramap-backend
environment:
- NODE_ENV=production
- PORT=3000
- MONGODB_URI=mongodb://mongo:27017/oramap
depends_on:
mongo:
condition: service_healthy
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: harbor.dvirlabs.com/my-apps/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
volumes:
mongo-data:
driver: local

View File

@ -1,27 +0,0 @@
version: '3.8'
services:
oramap:
build:
context: .
dockerfile: Dockerfile
image: oramap:latest
container_name: oramap-app
ports:
- "3000:3000"
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
networks:
oramap-network:
driver: bridge

View File

@ -1,31 +0,0 @@
# 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

24
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -1,18 +0,0 @@
# 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;"]

12
frontend/README.md Normal file
View File

@ -0,0 +1,12 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
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/) 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
## 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.

33
frontend/eslint.config.js Normal file
View File

@ -0,0 +1,33 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
export default [
{ ignores: ['dist'] },
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
]

13
frontend/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/oramap-logo.svg" />
<title>Ora</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

View File

@ -1,57 +0,0 @@
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 - no cache for HTML
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# API proxy to backend service
# For Kubernetes: set BACKEND_HOST env var or use service name
# For Docker Compose: backend service name is 'backend'
location /api/ {
# Docker's embedded DNS server
resolver 127.0.0.11 valid=30s;
# In Kubernetes, this will be: oramap-backend:3000
# In Docker Compose, this will be: backend:3000
# Default to backend:3000
set $backend_host backend;
if ($http_x_backend_host != "") {
set $backend_host $http_x_backend_host;
}
proxy_pass http://$backend_host: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;
}
# No cache for CSS and JS to allow quick updates
location ~* \.(css|js)$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# Cache images only
location ~* \.(jpg|jpeg|png|gif|ico|svg)$ {
expires 7d;
add_header Cache-Control "public, max-age=604800";
}
}

2601
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
frontend/package.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"leaflet": "^1.9.4",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"vite": "^6.3.5"
}
}

View File

@ -1,62 +0,0 @@
<!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>
<button onclick="toggleAddFamilyForm()" class="add-btn"> Add Family</button>
</div>
</header>
<!-- Add Family Form Modal -->
<div id="addFamilyModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Add New Family Location</h2>
<span class="close" onclick="toggleAddFamilyForm()">&times;</span>
</div>
<div class="modal-body">
<div id="formMessage" class="form-message"></div>
<form id="addFamilyForm" onsubmit="addFamily(event)">
<div class="form-group">
<label for="familyName">Family Name</label>
<input type="text" id="familyName" required placeholder="e.g., Cohen (כהן)" />
</div>
<div class="form-group">
<label for="cityName">City</label>
<input type="text" id="cityName" required placeholder="e.g., Jerusalem (ירושלים)" />
</div>
<div class="form-group">
<label for="latitude">Latitude</label>
<input type="number" id="latitude" required step="0.0001" min="-90" max="90" placeholder="e.g., 31.7683" />
</div>
<div class="form-group">
<label for="longitude">Longitude</label>
<input type="number" id="longitude" required step="0.0001" min="-180" max="180" placeholder="e.g., 35.2137" />
</div>
<div class="form-actions">
<button type="submit" class="btn-submit">Add Family</button>
<button type="button" class="btn-cancel" onclick="toggleAddFamilyForm()">Cancel</button>
</div>
</form>
</div>
</div>
</div>
<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>

View File

@ -0,0 +1,143 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 256 256" style="enable-background:new 0 0 256 256;" xml:space="preserve">
<style type="text/css">
.st0{fill:#4671C6;}
.st1{fill:#F9CFCF;}
.st2{fill:#F9A7A7;}
.st3{fill:#A4C9FF;}
.st4{fill:#3762CC;}
.st5{fill:#E0EBFC;}
.st6{fill:#6BDDDD;}
.st7{fill:#B9BEFC;}
.st8{fill:#FFEA92;}
.st9{fill:#EAA97D;}
.st10{fill:#FFEA94;}
.st11{fill:#FFE164;}
.st12{fill:#FFDC85;}
.st13{fill:#FFFFFF;}
.st14{fill:#383838;}
</style>
<g>
<g>
<path class="st3" d="M8,34.61v164.383c0,1.758,1.148,3.31,2.829,3.825L88,226.451V53.701L13.171,30.785
C10.6,29.998,8,31.921,8,34.61z"/>
</g>
<g>
<path class="st3" d="M242.829,224.868c2.571,0.787,5.171-1.136,5.171-3.825V56.66c0-1.758-1.148-3.31-2.829-3.825
C164.251,28.053,169.733,29.568,168,29.568v172.567C168.933,202.135,164.12,200.763,242.829,224.868z"/>
</g>
<g>
<path class="st3" d="M168,29.568c-1.732,0,4.024-1.599-80,24.133v172.75c84.284-25.812,79.068-24.317,80-24.317V29.568z"/>
</g>
<g>
<path class="st0" d="M32.871,49.368L32.871,49.368l-10.285-3.15C21.3,45.825,20,46.786,20,48.131v143.467
c0,0.879,0.574,1.655,1.414,1.912c64.095,19.629,51.098,15.61,66.586,20.525V65.701L32.871,49.368z"/>
</g>
<g>
<path class="st0" d="M223.129,205.65L223.129,205.65l10.285,3.13c1.286,0.391,2.586-0.564,2.586-1.9V64.329
c0-2.097,2.474-0.717-56-18.51l-12-3.784v147.386L223.129,205.65z"/>
</g>
<g>
<polygon class="st0" points="156,45.427 168,42.035 168,189.368 88,214.035 88,65.701 "/>
</g>
<g>
<path class="st6" d="M69.667,75.376c0-1.987-1.284-3.751-3.183-4.358l-8.957-2.859c-2.056-0.656-4.291,0.214-5.362,2.088
c-1.705,2.983-1.921,3.604-2.921,4.354c-0.228,0.171-12.492,9.378-12.724,9.526c-8.879,5.65-10.852,6.189-10.852,9.417
c0,8.441-0.271,9.132,0.928,10.714c6.196,8.179,6.74,9.775,9.679,9.775c2.709,0,5.495-0.441,7.012,2.446
c3.369,6.41,4.001,8.97,7.478,8.97c2.031,0,3.696,1.263,4.331,3.011l0.81,2.228c0.657,1.808,2.375,3.011,4.299,3.011
c2.98,0,6.614,0.565,7.82-3.167c2.391-7.398,4.658-3.842,7.755-5.778c2.563-1.602,4.456-3.275,7.196-1.874
c3.357,1.717,4.615,2.702,6.824,1.991c7.07-2.272,9.751-2.456,10.316-5.937c1.518-9.358,1.442-9.292,1.734-10.029l3.85-9.738
c0.91-2.302-0.176-4.911-2.452-5.886c-1.307-0.56-2.813-1.015-3.634-2.93l-1.581-3.69c-0.954-2.225-3.476-3.322-5.754-2.502
c-1.819,0.655-3.943,1.787-6.245,0.082c-5.322-3.942-6.444-5.335-9.053-4.776l-1.78,0.381
C72.352,80.459,69.667,78.289,69.667,75.376z"/>
</g>
<g>
<path class="st6" d="M233.414,208.78c1.286,0.391,2.586-0.564,2.586-1.9v-76.696c-0.084-0.186-0.18-0.367-0.294-0.536
c-6.446-9.534-4.425-7.037-9.964-12.345c-1.723-1.651-4.543-1.226-5.702,0.861c-1.333,2.398-3.195,1.873-11.06,1.873
c-1.328,0-0.39-0.327-16.362,8.918c-3.742,2.166-6.709-3.501-10.047-3.501c-3.182,0-6.074-0.508-7.053,2.521
c-2.164,6.695-1.21,5.162-4.954,8.466c-1.442,1.272-3.624,1.204-4.983-0.156l-4.932-4.932c-1.681-1.681-4.495-1.326-5.707,0.719
c-3.733,6.3-1.768,3.85-7.944,9.717c-1.018,0.967-1.389,2.432-0.954,3.767c4.431,13.609,3.132,11.783,7.557,13.75
c2.94,1.306,2.086,4.285,1.776,7.496c-0.204,2.107-2.156,3.597-4.242,3.237c-7.619-1.314-3.639-1.554-17.397,1.966
c-3.159,0.808-2.739,4.055-2.739,5.31c0,6.051-11.333,2.731-11.333,8.782c0,11.849-1.384,9.309,9.483,14.287
c1.745,0.799-0.494,1.12,38.85-11.011v0.053C231.483,208.109,220.104,204.729,233.414,208.78z"/>
</g>
<g>
<circle class="st8" cx="62.419" cy="111.87" r="6.606"/>
</g>
<g>
<path class="st5" d="M64.263,126.919l-1.293-4.83c-0.512-1.914,2.384-2.69,2.898-0.775l1.293,4.83
C67.674,128.056,64.778,128.837,64.263,126.919z"/>
</g>
<g>
<path class="st5" d="M92.24,162.988c0.131-0.712,0.75-1.229,1.473-1.229c0.013,0,1.24,0.202,2.222,0.202
c0.208,0,0.417-0.006,0.627-0.018c0,0,6.745-0.398,6.746-0.398c2.068,0,2.071,2.882,0.173,2.995
C97.066,164.919,91.729,165.773,92.24,162.988z M111.387,161.479c-0.484-0.674-0.331-1.609,0.343-2.093
c1.262-0.907,1.399-1.179,6.408-6.815c1.312-1.477,3.561,0.512,2.242,1.993C115.742,159.783,112.979,163.694,111.387,161.479z
M84.564,158.932c-0.992-1.466-1.092-1.91-4.103-9.104c-0.319-0.763,0.042-1.644,0.805-1.963c0.767-0.319,1.643,0.042,1.963,0.805
c3.059,7.308,3.048,7.442,3.82,8.582C88.155,158.883,85.68,160.579,84.564,158.932z M163.494,148.906
c0.067-0.769,0.726-1.371,1.5-1.371c0.042,0,0.084,0.002,0.126,0.005c3.237,0.279,2.595,0.281,9.121-1.12
c1.968-0.419,2.542,2.522,0.629,2.934C171.545,150.068,163.206,152.264,163.494,148.906z M145.528,148.866
c-1.963-0.17-1.726-3.143,0.256-2.989l9.667,0.832c1.887,0.164,1.769,2.995-0.125,2.995
C155.219,149.703,145.634,148.875,145.528,148.866z M125.845,145.819c3.726-1.619,6.297-1.116,10.273-0.774
c1.957,0.17,1.734,3.164-0.256,2.989c-3.54-0.305-5.745-0.801-8.822,0.536C125.219,149.36,124.035,146.603,125.845,145.819z
M193.281,147.593c-4.528-0.996-4.63-1.193-8.924-0.273c-0.105,0.023-0.212,0.034-0.317,0.034c-1.779,0-2.059-2.59-0.312-2.967
c4.896-1.048,5.529-0.75,10.196,0.277C195.839,145.084,195.233,148.032,193.281,147.593z M75.479,142.787
c-0.533-0.344-1.098-0.641-1.681-0.885c-2.961-1.239-8.512-5.805-5.868-7.457c2.252-1.402,1.902,2.547,7.024,4.689
c1.682,0.704,3.591,1.668,2.598,3.206C77.105,143.032,76.179,143.239,75.479,142.787z"/>
</g>
<g>
<path class="st2" d="M220.241,140.217L220.241,140.217c-0.781-1.445-2.585-1.984-4.031-1.203l-9.24,4.991l-4.991-9.24
c-0.781-1.445-2.585-1.984-4.031-1.204h0c-1.445,0.781-1.984,2.585-1.204,4.031l4.991,9.24l-9.24,4.991
c-1.445,0.781-1.984,2.585-1.203,4.031v0c0.781,1.445,2.585,1.984,4.031,1.203l9.24-4.991l4.991,9.24
c0.781,1.445,2.585,1.984,4.031,1.204l0,0c1.445-0.781,1.984-2.585,1.204-4.031l-4.991-9.24l9.24-4.991
C220.483,143.467,221.022,141.662,220.241,140.217z"/>
</g>
<g>
<path class="st4" d="M88,228.452c-0.196,0-0.394-0.029-0.585-0.088L10.243,204.73C7.705,203.952,6,201.647,6,198.993V34.61
c0-1.923,0.887-3.681,2.433-4.824c1.545-1.143,3.485-1.476,5.325-0.913L88.585,51.79C89.426,52.047,90,52.823,90,53.702v172.75
c0,0.634-0.301,1.231-0.811,1.608C88.841,228.317,88.423,228.452,88,228.452z M11.994,32.608c-0.421,0-0.833,0.134-1.183,0.394
C10.295,33.383,10,33.969,10,34.61v164.383c0,0.885,0.568,1.653,1.415,1.913L86,223.748V55.181L12.586,32.697
C12.39,32.638,12.191,32.608,11.994,32.608z"/>
</g>
<g>
<path class="st4" d="M244.018,227.048c-0.591,0-1.188-0.089-1.775-0.269c-21.08-6.456-36.168-11.084-46.981-14.401
c-20.53-6.297-26.108-8.008-27.129-8.247c-0.04,0.002-0.083,0.003-0.132,0.003c-1.104,0-2-0.896-2-2V29.568c0-1.104,0.896-2,2-2
l0.114-0.005c1.218-0.084,1.214-0.083,33.118,9.708c10.75,3.299,25.165,7.723,44.525,13.652c2.538,0.778,4.243,3.083,4.243,5.737
v164.383c0,1.923-0.887,3.681-2.433,4.824C246.515,226.646,245.28,227.048,244.018,227.048z M170,200.53
c2.671,0.735,9.202,2.739,26.434,8.025c10.813,3.316,25.901,7.945,46.98,14.4c0,0,0,0,0,0c0.613,0.186,1.259,0.076,1.774-0.304
c0.516-0.381,0.811-0.967,0.811-1.608V56.66c0-0.885-0.568-1.653-1.415-1.913c-19.361-5.929-33.776-10.353-44.527-13.652
c-19.055-5.848-27.015-8.291-30.059-9.149V200.53z"/>
</g>
<g>
<path class="st4" d="M88,228.452c-0.423,0-0.841-0.134-1.189-0.392c-0.51-0.377-0.811-0.974-0.811-1.608V53.702
c0-0.879,0.574-1.655,1.415-1.912c20.108-6.159,35.075-10.751,46.233-14.175c33.044-10.141,33.043-10.146,34.25-10.051L168,27.568
c1.104,0,2,0.896,2,2v172.566c0,1.104-0.896,2-2,2c-0.048,0-0.091-0.001-0.131-0.003c-1.085,0.258-7.074,2.095-29.002,8.821
c-11.57,3.549-27.719,8.501-50.281,15.411C88.394,228.423,88.196,228.452,88,228.452z M90,55.181v168.567
c21.216-6.498,36.571-11.208,47.694-14.619c18.561-5.692,25.508-7.823,28.306-8.593V31.948c-3.113,0.877-11.343,3.403-31.179,9.49
C123.925,44.782,109.4,49.239,90,55.181z"/>
</g>
<g>
<path class="st4" d="M88,216.035c-0.203,0-0.407-0.031-0.605-0.094l-4.925-1.566c-6.611-2.106-6.611-2.106-32.834-10.133
l-28.807-8.82c-1.692-0.519-2.829-2.056-2.829-3.825V48.131c0-1.282,0.591-2.454,1.622-3.216c1.031-0.761,2.325-0.985,3.549-0.608
l10.278,3.147l55.12,16.331C89.417,64.036,90,64.816,90,65.702v148.333c0,0.638-0.304,1.237-0.819,1.614
C88.834,215.902,88.419,216.035,88,216.035z M22,48.13v143.467l28.807,8.82c26.244,8.033,26.244,8.033,32.877,10.146L86,211.3
V67.195L32.303,51.286c-0.006-0.001-0.011-0.003-0.017-0.005L22,48.13z"/>
</g>
<g>
<path class="st4" d="M234.012,210.87c-0.393,0-0.79-0.058-1.18-0.177l-10.279-3.128l-55.118-16.226
c-0.851-0.25-1.435-1.031-1.435-1.918V42.035c0-0.637,0.304-1.236,0.817-1.613c0.514-0.377,1.175-0.487,1.784-0.294l12,3.784
c13.574,4.13,23.873,7.23,31.674,9.578c12.206,3.674,18.333,5.518,21.419,6.621c2.763,0.987,4.432,1.754,4.311,4.08L238,64.329
v142.55c0,1.274-0.588,2.44-1.614,3.2C235.683,210.601,234.856,210.87,234.012,210.87z M170,187.925l53.694,15.807
c0.006,0.001,0.012,0.003,0.017,0.005l10.286,3.13L234,64.519c-1.813-0.858-8.533-2.881-22.877-7.198
c-7.805-2.349-18.107-5.45-31.705-9.588L170,44.762V187.925z"/>
</g>
<g>
<path class="st4" d="M88,216.035c-0.422,0-0.839-0.134-1.188-0.391c-0.511-0.377-0.813-0.975-0.813-1.609V65.702
c0-0.884,0.581-1.664,1.429-1.917l68-20.275c0.009-0.003,0.018-0.005,0.027-0.008l12-3.392c0.601-0.169,1.251-0.048,1.75,0.33
c0.5,0.378,0.793,0.969,0.793,1.595v147.333c0,0.877-0.572,1.653-1.411,1.911l-80,24.667
C88.396,216.005,88.198,216.035,88,216.035z M90,67.192v144.133l76-23.433V44.678l-9.443,2.669L90,67.192z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -1,246 +0,0 @@
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: '&copy; OpenStreetMap contributors &copy; 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);
}
});
}
// Autocomplete data - loaded from database
let familyNames = [];
let fuse;
// Load families from database for autocomplete
async function loadFamiliesForAutocomplete() {
try {
console.log('📚 Loading families from database for autocomplete...');
const response = await fetch('/api/families');
const families = await response.json();
// Extract unique family names
familyNames = [...new Set(families.map(f => f.family))];
console.log('✅ Loaded', familyNames.length, 'family names for autocomplete');
// Initialize or update Fuse.js
fuse = new Fuse(familyNames, {
includeScore: true,
threshold: 0.4 // Lower = stricter matching
});
} catch (error) {
console.error('❌ Failed to load families for autocomplete:', error);
}
}
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 || !fuse) 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);
// Add Family Form Functions
function toggleAddFamilyForm() {
const modal = document.getElementById('addFamilyModal');
const form = document.getElementById('addFamilyForm');
const message = document.getElementById('formMessage');
if (modal.style.display === 'block') {
modal.style.display = 'none';
form.reset();
message.textContent = '';
message.className = 'form-message';
} else {
modal.style.display = 'block';
}
}
async function addFamily(event) {
event.preventDefault();
const familyName = document.getElementById('familyName').value.trim();
const cityName = document.getElementById('cityName').value.trim();
const latitude = parseFloat(document.getElementById('latitude').value);
const longitude = parseFloat(document.getElementById('longitude').value);
const messageEl = document.getElementById('formMessage');
messageEl.textContent = 'Adding family...';
messageEl.className = 'form-message info';
console.log('📝 Adding family:', { familyName, cityName, latitude, longitude });
try {
console.log('🌐 Sending POST request to /api/families');
const response = await fetch('/api/families', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
family: familyName,
city: cityName,
lat: latitude,
lng: longitude
})
});
console.log('📡 Response status:', response.status, response.statusText);
console.log('📡 Response headers:', Object.fromEntries(response.headers.entries()));
const contentType = response.headers.get('content-type');
let data;
if (contentType && contentType.includes('application/json')) {
data = await response.json();
console.log('📦 Response data:', data);
} else {
const text = await response.text();
console.error('⚠️ Non-JSON response:', text);
data = { error: 'Server returned non-JSON response', details: text.substring(0, 200) };
}
if (response.ok) {
console.log('✅ Family added successfully!');
messageEl.textContent = '✅ Family added successfully!';
messageEl.className = 'form-message success';
// Reload autocomplete with updated family list
await loadFamiliesForAutocomplete();
// Add marker to map
L.marker([latitude, longitude]).addTo(map)
.bindPopup(`<strong>${familyName}</strong><br>City: ${cityName}`)
.openPopup();
map.setView([latitude, longitude], 10);
// Reset form after 2 seconds
setTimeout(() => {
toggleAddFamilyForm();
}, 2000);
} else {
console.error('❌ Server error:', data);
messageEl.textContent = `❌ Error (${response.status}): ${data.error || data.message || 'Unknown error'}`;
messageEl.className = 'form-message error';
}
} catch (error) {
console.error('❌ Add family error:', error);
messageEl.textContent = `❌ Failed: ${error.message || 'Network error'}`;
messageEl.className = 'form-message error';
}
}
// Close modal when clicking outside
window.onclick = function(event) {
const modal = document.getElementById('addFamilyModal');
if (event.target === modal) {
toggleAddFamilyForm();
}
}
// Allow Enter key to trigger search
document.getElementById('searchInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
searchFamily();
}
});
// Load families for autocomplete when page loads
loadFamiliesForAutocomplete();

View File

@ -1,352 +0,0 @@
/* 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;
}
/* Modern Modal styles with animations */
.modal {
display: none;
position: fixed;
z-index: 2000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
transform: translateY(50px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.modal-content {
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
margin: 8% auto;
padding: 0;
border-radius: 20px;
width: 90%;
max-width: 480px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.1);
animation: slideUp 0.4s ease-out;
overflow: hidden;
}
.modal-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 24px 30px;
color: white;
position: relative;
}
.modal-content h2 {
margin: 0;
font-size: 24px;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
}
.modal-content h2::before {
content: "📍";
font-size: 28px;
}
.close {
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
color: rgba(255, 255, 255, 0.9);
font-size: 32px;
font-weight: 300;
line-height: 1;
cursor: pointer;
transition: all 0.3s ease;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.close:hover {
background-color: rgba(255, 255, 255, 0.2);
transform: translateY(-50%) rotate(90deg);
color: white;
}
.modal-body {
padding: 30px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #2d3748;
font-size: 14px;
display: flex;
align-items: center;
gap: 6px;
}
.form-group label::before {
content: "•";
color: #667eea;
font-size: 20px;
}
.form-group input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e2e8f0;
border-radius: 12px;
font-size: 15px;
transition: all 0.3s ease;
background-color: white;
font-family: inherit;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
transform: translateY(-1px);
}
.form-group input:hover {
border-color: #cbd5e0;
}
.form-actions {
display: flex;
gap: 12px;
margin-top: 28px;
padding-top: 20px;
border-top: 2px solid #f1f3f5;
}
.form-actions button {
flex: 1;
padding: 14px 24px;
border: none;
border-radius: 12px;
cursor: pointer;
font-weight: 600;
font-size: 15px;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-submit {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-submit:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
}
.btn-submit:active {
transform: translateY(0);
}
.btn-submit::before {
content: "✓";
font-size: 18px;
}
.btn-cancel {
background-color: #e2e8f0;
color: #4a5568;
}
.btn-cancel:hover {
background-color: #cbd5e0;
transform: translateY(-2px);
}
.btn-cancel::before {
content: "✕";
font-size: 18px;
}
.form-message {
padding: 14px 18px;
border-radius: 12px;
margin-bottom: 20px;
text-align: center;
font-weight: 500;
animation: slideUp 0.3s ease-out;
font-size: 14px;
}
.form-message.success {
background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
color: #155724;
border: 2px solid #b1dfbb;
box-shadow: 0 4px 12px rgba(39, 174, 96, 0.2);
}
.form-message.error {
background: linear-gradient(135deg, #f8d7da 0%, #f5c6cb 100%);
color: #721c24;
border: 2px solid #f1b0b7;
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.2);
}
.form-message.info {
background: linear-gradient(135deg, #d1ecf1 0%, #bee5eb 100%);
color: #0c5460;
border: 2px solid #a6d9e3;
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.2);
}
/* Add Family button with modern style */
.btn-add-family {
padding: 12px 20px;
border: none;
border-radius: 10px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
cursor: pointer;
font-weight: 600;
margin-left: 8px;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
display: inline-flex;
align-items: center;
gap: 6px;
}
.btn-add-family:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
.btn-add-family:active {
transform: translateY(0);
}

1
frontend/public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

57
frontend/src/App.css Normal file
View File

@ -0,0 +1,57 @@
html, body, #root {
height: 100%;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.app-container {
display: flex;
flex-direction: column;
height: 100%;
}
header {
background-color: #2c3e50;
padding: 20px;
text-align: center;
color: white;
}
header h1 {
margin-bottom: 10px;
font-size: 2rem;
}
.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;
}
main {
flex: 1;
display: flex;
}
.map-container {
flex: 1;
min-height: 500px;
width: 100%;
}

26
frontend/src/App.jsx Normal file
View File

@ -0,0 +1,26 @@
import { useState } from 'react';
import './App.css';
import Header from './components/Header';
import MapView from './components/MapView';
function App() {
const [query, setQuery] = useState('');
const [families, setFamilies] = useState([]);
const handleSearch = async () => {
const res = await fetch(`http://localhost:8000/search?family=${encodeURIComponent(query)}`);
const data = await res.json();
setFamilies(data);
};
return (
<div className="app-container">
<Header query={query} setQuery={setQuery} onSearch={handleSearch} />
<main>
<MapView families={families} />
</main>
</div>
);
}
export default App;

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,21 @@
function Header({ query, setQuery, onSearch }) {
return (
<header>
<h1>
<img src="../public/oramap-logo.svg" alt="Logo" style={{ height: '40px', marginRight: '10px', verticalAlign: 'middle' }} />
Ora
</h1>
<div className="search-bar">
<input
type="text"
placeholder="Enter family name..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<button onClick={onSearch}>Search</button>
</div>
</header>
);
}
export default Header;

View File

@ -0,0 +1,43 @@
import { useEffect, useRef } from 'react';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
function MapView({ families }) {
const mapRef = useRef(null);
const containerRef = useRef(null);
useEffect(() => {
if (!mapRef.current && containerRef.current) {
const map = L.map(containerRef.current).setView([15.3545, 44.2064], 7);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors'
}).addTo(map);
mapRef.current = map;
}
}, []);
useEffect(() => {
const map = mapRef.current;
if (!map) return;
// Remove old markers
map.eachLayer((layer) => {
if (layer instanceof L.Marker) map.removeLayer(layer);
});
families.forEach((fam) => {
L.marker([fam.lat, fam.lng]).addTo(map)
.bindPopup(`<strong>${fam.family}</strong><br/>${fam.city}`);
});
if (families.length > 0) {
map.setView([families[0].lat, families[0].lng], 8);
}
}, [families]);
return <div ref={containerRef} className="map-container"></div>;
}
export default MapView;

View File

@ -1,45 +0,0 @@
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;

68
frontend/src/index.css Normal file
View File

@ -0,0 +1,68 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

10
frontend/src/main.jsx Normal file
View File

@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import './App.css';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

7
frontend/vite.config.js Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})

4
old-project/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules/
package-lock.json
package.json

View File

@ -0,0 +1,14 @@
when:
event:
- push
- pull_request
branch:
- master
steps:
- name: Test
image: alpine:latest
commands:
- echo "Running tests..."
- sleep 1
- echo "Tests completed successfully!"

View File

@ -0,0 +1,6 @@
# How to Run
```
npm init -y
npm install express
node server.js
```

0
old-project/README.md Normal file
View File

View 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>

View File

Before

Width:  |  Height:  |  Size: 975 KiB

After

Width:  |  Height:  |  Size: 975 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View 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: '&copy; OpenStreetMap contributors &copy; 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(`/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);

View 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;
}

View File

@ -0,0 +1,3 @@
npm init -y
npm install express
node server.js

19
old-project/server.js Normal file
View File

@ -0,0 +1,19 @@
const express = require('express');
const app = express();
const families = require('../data/families.json');
app.use(express.static('public'));
app.get('/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);
});
const port = 3000;
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});

View File

@ -1,11 +1,24 @@
apiVersion: v2
name: oramap
description: Ora Map - Family Location Mapping Application with MongoDB (Microservices Architecture)
description: A Helm chart for Kubernetes
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# Chart version
version: 0.3.0
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0
# Application version
appVersion: "1.0.0"
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "1.16.0"

View File

@ -1,225 +0,0 @@
# Ora Map Helm Chart
Helm chart for deploying Ora Map application in Kubernetes with microservices architecture.
## Architecture
This chart deploys three main components:
- **Backend**: Node.js Express API server with MongoDB integration (Port 3000)
- **Frontend**: Nginx serving static files with API proxy (Port 80)
- **MongoDB**: Database for storing family location data (Port 27017)
## Installation
### Prerequisites
- Kubernetes cluster 1.19+
- Helm 3.0+
- Docker images built and pushed to Harbor registry
### Install Chart
```bash
# From the repository root
helm install oramap ./oramap
# Or with custom values
helm install oramap ./oramap -f custom-values.yaml
# Install in a specific namespace
helm install oramap ./oramap -n my-namespace --create-namespace
```
### Upgrade Chart
```bash
helm upgrade oramap ./oramap
```
### Uninstall Chart
```bash
helm uninstall oramap
```
## Configuration
The following table lists the configurable parameters and their default values:
| Parameter | Description | Default |
|-----------|-------------|---------|
| `backend.image.repository` | Backend image repository | `harbor.dvirlabs.com/my-apps/oramap-backend` |
| `backend.image.tag` | Backend image tag | `latest` |
| `backend.replicaCount` | Number of backend replicas | `1` |
| `backend.containerPort` | Backend container port | `3000` |
| `backend.resources.limits.cpu` | Backend CPU limit | `500m` |
| `backend.resources.limits.memory` | Backend memory limit | `512Mi` |
| `frontend.image.repository` | Frontend image repository | `harbor.dvirlabs.com/my-apps/oramap-frontend` |
| `frontend.image.tag` | Frontend image tag | `latest` |
| `frontend.replicaCount` | Number of frontend replicas | `1` |
| `frontend.containerPort` | Frontend container port | `80` |
| `service.type` | Kubernetes service type | `ClusterIP` |
| `ingress.enabled` | Enable ingress | `true` |
| `ingress.className` | Ingress class name | `traefik` |
| `ingress.hosts[0].host` | Ingress hostname | `oramap.dvirlabs.com` |
| `mongodb.enabled` | Enable MongoDB deployment | `true` |
| `mongodb.image.repository` | MongoDB image repository | `mongo` |
| `mongodb.image.tag` | MongoDB image tag | `7.0` |
| `mongodb.persistence.enabled` | Enable persistent storage | `true` |
| `mongodb.persistence.size` | PVC size | `5Gi` |
| `mongodb.persistence.storageClass` | Storage class name | `""` (default) |
| `mongodb.database` | Database name | `oramap` |
### Example Custom Values
```yaml
# custom-values.yaml
backend:
image:
tag: "v1.0.0"
mongodb:
persistence:
enabled: true
size: 10Gi
storageClass: "fast-ssd"
replicaCount: 3
frontend:
image:
tag: "v1.0.0"
replicaCount: 2
ingress:
hosts:
- host: oramap.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: oramap-tls
hosts:
- oramap.example.com
```
## Deployment Components
### Backend Deployment
- Runs Express.js API server
- Health checks on `/api/health`
- Configurable resources and replicas
### Frontend Deployment
- Runs Nginx serving static files
- Proxies `/api/*` requests to backend service
- Uses ConfigMap for nginx configuration
- Health checks on `/`
### Services
- **Backend Service**: Internal ClusterIP service on port 3000
- **Frontend Service**: ClusterIP
### MongoDB StatefulSet
- Persistent database storage for family data
- Health checks using mongosh
- Configurable storage size and class
- Automatic PVC creation
## Database Setup
### Initial Data Seeding
After deployment, seed the database with initial family data:
```bash
# Access the backend pod
kubectl exec -it deployment/oramap-backend -- sh
# Run the seed script
npm run seed
# Or force re-seed
npm run seed:force
```
### MongoDB Access
```bash
# Port-forward to MongoDB
kubectl port-forward svc/oramap-mongodb 27017:27017
# Check StatefulSet
kubectl get statefulset oramap-mongodb
kubectl get pvc
```
### View Logs
```bash
# Backend logs
kubectl logs -l app=oramap-backend
# Frontend logs
kubectl logs -l app=oramap-frontend
# MongoDB logs
kubectl logs oramap-mongodb-0
```
## CI/CD Integration
The Woodpecker CI pipeline automatically:
1. Builds backend and frontend Docker images
2. Tags with branch name and commit SHA
3. Pushes to Harbor registry
4. Updates this chart's values in the GitOps repository
## Accessing the Application
After installation, the application will be available at:
- **External**: https://oramap.dvirlabs.com (via Ingress)
- **Internal Backend API**: http://oramap-backend:3000
- **Internal Frontend**: http://oramap-frontend:80
## Troubleshooting
### Check Pod Status
```bash
kubectl get pods -l release=oramap
```
### View Logs
```bash
# Backend logs
kubectl logs -l app=oramap-backend
# Frontend logs
kubectl logs -l app=oramap-frontend
```
### Check Services
```bash
kubectl get svc -l release=oramap
```
### Test Backend Health
```bash
kubectl port-forward svc/oramap-backend 3000:3000
curl http://localhost:3000/api/health
```
## Version History
- **0.2.0**: Microservices architecture with separate backend and frontend
- **0.1.0**: Initial monolithic deployment

View File

@ -1,52 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "oramap.fullname" . }}-nginx-config
labels:
app: {{ include "oramap.name" . }}-frontend
chart: {{ include "oramap.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
data:
default.conf: |
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 service in Kubernetes
location /api/ {
proxy_pass http://{{ include "oramap.fullname" . }}-backend:{{ .Values.service.backend.port }};
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;
}
# No cache for CSS and JS to allow quick updates
location ~* \.(css|js)$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# Cache images only
location ~* \.(jpg|jpeg|png|gif|ico|svg)$ {
expires 7d;
add_header Cache-Control "public, max-age=604800";
}
}

View File

@ -1,111 +1,38 @@
---
# Backend Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "oramap.fullname" . }}-backend
name: {{ include "oramap.fullname" . }}
labels:
app: {{ include "oramap.name" . }}-backend
app: {{ include "oramap.name" . }}
chart: {{ include "oramap.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
component: backend
spec:
replicas: {{ .Values.backend.replicaCount }}
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ include "oramap.name" . }}-backend
app: {{ include "oramap.name" . }}
release: {{ .Release.Name }}
component: backend
template:
metadata:
labels:
app: {{ include "oramap.name" . }}-backend
app: {{ include "oramap.name" . }}
release: {{ .Release.Name }}
component: backend
spec:
containers:
- name: backend
image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag }}"
imagePullPolicy: {{ .Values.backend.image.pullPolicy }}
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: {{ .Values.backend.containerPort }}
- containerPort: {{ .Values.containerPort }}
name: http
env:
- name: NODE_ENV
value: "production"
- name: PORT
value: "{{ .Values.backend.containerPort }}"
{{- if .Values.mongodb.enabled }}
- name: MONGODB_URI
value: "mongodb://{{ include \"oramap.fullname\" . }}-mongodb:27017/{{ .Values.mongodb.database }}"
{{- end }}
livenessProbe:
httpGet:
path: /api/health
port: http
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /api/health
port: http
initialDelaySeconds: 5
periodSeconds: 10
resources:
{{- toYaml .Values.backend.resources | nindent 12 }}
---
# Frontend Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "oramap.fullname" . }}-frontend
labels:
app: {{ include "oramap.name" . }}-frontend
chart: {{ include "oramap.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
component: frontend
spec:
replicas: {{ .Values.frontend.replicaCount }}
selector:
matchLabels:
app: {{ include "oramap.name" . }}-frontend
release: {{ .Release.Name }}
component: frontend
template:
metadata:
labels:
app: {{ include "oramap.name" . }}-frontend
release: {{ .Release.Name }}
component: frontend
spec:
containers:
- name: frontend
image: "{{ .Values.frontend.image.repository }}:{{ .Values.frontend.image.tag }}"
imagePullPolicy: {{ .Values.frontend.image.pullPolicy }}
ports:
- containerPort: {{ .Values.frontend.containerPort }}
name: http
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/conf.d/default.conf
subPath: default.conf
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 5
periodSeconds: 10
resources:
{{- toYaml .Values.frontend.resources | nindent 12 }}
volumes:
- name: nginx-config
configMap:
name: {{ include "oramap.fullname" . }}-nginx-config
{{- toYaml .Values.resources | nindent 12 }}

View File

@ -21,9 +21,9 @@ spec:
pathType: {{ .pathType }}
backend:
service:
name: {{ include "oramap.fullname" $ }}-frontend
name: {{ include "oramap.fullname" $ }}
port:
number: {{ $.Values.service.frontend.port }}
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- if .Values.ingress.tls }}

View File

@ -1,20 +0,0 @@
{{- if .Values.mongodb.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "oramap.fullname" . }}-mongodb
labels:
{{- include "oramap.labels" . | nindent 4 }}
app.kubernetes.io/component: database
spec:
type: ClusterIP
clusterIP: None
ports:
- port: 27017
targetPort: 27017
protocol: TCP
name: mongodb
selector:
{{- include "oramap.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: database
{{- end }}

View File

@ -1,75 +0,0 @@
{{- if .Values.mongodb.enabled }}
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ include "oramap.fullname" . }}-mongodb
labels:
{{- include "oramap.labels" . | nindent 4 }}
app.kubernetes.io/component: database
spec:
serviceName: {{ include "oramap.fullname" . }}-mongodb
replicas: 1
selector:
matchLabels:
{{- include "oramap.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: database
template:
metadata:
labels:
{{- include "oramap.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: database
spec:
containers:
- name: mongodb
image: "{{ .Values.mongodb.image.repository }}:{{ .Values.mongodb.image.tag }}"
imagePullPolicy: {{ .Values.mongodb.image.pullPolicy }}
ports:
- name: mongodb
containerPort: 27017
protocol: TCP
env:
- name: MONGO_INITDB_DATABASE
value: {{ .Values.mongodb.database }}
livenessProbe:
exec:
command:
- mongosh
- --eval
- "db.adminCommand('ping')"
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
readinessProbe:
exec:
command:
- mongosh
- --eval
- "db.adminCommand('ping')"
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
resources:
{{- toYaml .Values.mongodb.resources | nindent 12 }}
volumeMounts:
- name: data
mountPath: /data/db
{{- if .Values.mongodb.persistence.enabled }}
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
{{- if .Values.mongodb.persistence.storageClass }}
storageClassName: {{ .Values.mongodb.persistence.storageClass }}
{{- end }}
resources:
requests:
storage: {{ .Values.mongodb.persistence.size }}
{{- else }}
volumes:
- name: data
emptyDir: {}
{{- end }}
{{- end }}

View File

@ -1,46 +1,19 @@
---
# Backend Service
apiVersion: v1
kind: Service
metadata:
name: {{ include "oramap.fullname" . }}-backend
name: {{ include "oramap.fullname" . }}
labels:
app: {{ include "oramap.name" . }}-backend
app: {{ include "oramap.name" . }}
chart: {{ include "oramap.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
component: backend
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.backend.port }}
targetPort: {{ .Values.service.backend.targetPort }}
- port: {{ .Values.service.port }}
targetPort: {{ .Values.containerPort }}
protocol: TCP
name: http
selector:
app: {{ include "oramap.name" . }}-backend
app: {{ include "oramap.name" . }}
release: {{ .Release.Name }}
component: backend
---
# Frontend Service
apiVersion: v1
kind: Service
metadata:
name: {{ include "oramap.fullname" . }}-frontend
labels:
app: {{ include "oramap.name" . }}-frontend
chart: {{ include "oramap.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
component: frontend
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.frontend.port }}
targetPort: {{ .Values.service.frontend.targetPort }}
protocol: TCP
name: http
selector:
app: {{ include "oramap.name" . }}-frontend
release: {{ .Release.Name }}
component: frontend

View File

@ -1,65 +1,15 @@
replicaCount: 1
# Backend API configuration
backend:
image:
repository: harbor.dvirlabs.com/my-apps/oramap-backend
tag: "v1.1.0"
pullPolicy: Always
repository: harbor.dvirlabs.com/shay/oramap
tag: "1"
pullPolicy: IfNotPresent
containerPort: 3000
replicaCount: 1
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
# Frontend Nginx configuration
frontend:
image:
repository: harbor.dvirlabs.com/my-apps/oramap-frontend
tag: "v1.1.0"
pullPolicy: Always
containerPort: 80
replicaCount: 1
resources:
limits:
cpu: 200m
memory: 256Mi
requests:
cpu: 50m
memory: 64Mi
service:
type: ClusterIP
backend:
port: 3000
targetPort: 3000
frontend:
port: 80
targetPort: 80
# MongoDB configuration
mongodb:
enabled: true
image:
repository: mongo
tag: "7.0"
pullPolicy: IfNotPresent
persistence:
enabled: true
storageClass: ""
size: 5Gi
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 256Mi
database: oramap
ingress:
enabled: true