Compare commits
2 Commits
master
...
vite-react
| Author | SHA1 | Date | |
|---|---|---|---|
| e42c62820b | |||
| f884de3b3a |
@ -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
@ -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/
|
||||
|
||||
121
.woodpecker.yaml
@ -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
|
||||
33
Dockerfile
@ -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"]
|
||||
|
||||
|
||||
408
README.md
@ -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:
|
||||
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!
|
||||
Start the Client:
|
||||
cd frontend
|
||||
npm run dev
|
||||
@ -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
|
||||
@ -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"]
|
||||
BIN
backend/__pycache__/main.cpython-313.pyc
Normal 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
@ -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)
|
||||
@ -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);
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
@ -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`);
|
||||
});
|
||||
@ -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`);
|
||||
});
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
@ -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?
|
||||
@ -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
@ -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
@ -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
@ -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>
|
||||
@ -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
28
frontend/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@ -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()">×</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>
|
||||
143
frontend/public/oramap-logo.svg
Normal 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 |
@ -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: '© 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 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();
|
||||
@ -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
@ -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
@ -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
@ -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;
|
||||
1
frontend/src/assets/react.svg
Normal 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 |
21
frontend/src/components/Header.jsx
Normal 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;
|
||||
43
frontend/src/components/MapView.jsx
Normal 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: '© 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;
|
||||
@ -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
@ -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
@ -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
@ -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
@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
package-lock.json
|
||||
package.json
|
||||
|
||||
14
old-project/.woodpecker.yaml
Normal 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!"
|
||||
6
old-project/README copy.md
Normal file
@ -0,0 +1,6 @@
|
||||
# How to Run
|
||||
```
|
||||
npm init -y
|
||||
npm install express
|
||||
node server.js
|
||||
```
|
||||
0
old-project/README.md
Normal file
26
old-project/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>
|
||||
|
Before Width: | Height: | Size: 975 KiB After Width: | Height: | Size: 975 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
124
old-project/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(`/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
old-project/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;
|
||||
}
|
||||
3
old-project/run_ora_map.sh
Normal file
@ -0,0 +1,3 @@
|
||||
npm init -y
|
||||
npm install express
|
||||
node server.js
|
||||
19
old-project/server.js
Normal 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}`);
|
||||
});
|
||||
@ -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"
|
||||
|
||||
225
oramap/README.md
@ -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
|
||||
@ -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";
|
||||
}
|
||||
}
|
||||
@ -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 }}
|
||||
|
||||
@ -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 }}
|
||||
|
||||
@ -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 }}
|
||||
@ -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 }}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
containerPort: 3000
|
||||
replicaCount: 1
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
image:
|
||||
repository: harbor.dvirlabs.com/shay/oramap
|
||||
tag: "1"
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# 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
|
||||
containerPort: 3000
|
||||
|
||||
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
|
||||
port: 80
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
|
||||