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:24-slim
|
||||||
FROM node:20-alpine AS base
|
|
||||||
|
|
||||||
# Production stage
|
|
||||||
FROM base AS production
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy backend package files
|
COPY package*.json ./
|
||||||
COPY backend/package*.json ./backend/
|
RUN npm install
|
||||||
WORKDIR /app/backend
|
|
||||||
RUN npm install --production
|
|
||||||
|
|
||||||
# Copy backend source and data
|
COPY . .
|
||||||
COPY backend/ ./
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# 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"]
|
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.
|
Start the Client:
|
||||||
|
cd frontend
|
||||||
## 📋 Table of Contents
|
npm run dev
|
||||||
- [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!
|
|
||||||
@ -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
|
apiVersion: v2
|
||||||
name: oramap
|
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
|
type: application
|
||||||
|
|
||||||
# Chart version
|
# This is the chart version. This version number should be incremented each time you make changes
|
||||||
version: 0.3.0
|
# 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
|
# This is the version number of the application being deployed. This version number should be
|
||||||
appVersion: "1.0.0"
|
# 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
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
name: {{ include "oramap.fullname" . }}-backend
|
name: {{ include "oramap.fullname" . }}
|
||||||
labels:
|
labels:
|
||||||
app: {{ include "oramap.name" . }}-backend
|
app: {{ include "oramap.name" . }}
|
||||||
chart: {{ include "oramap.chart" . }}
|
chart: {{ include "oramap.chart" . }}
|
||||||
release: {{ .Release.Name }}
|
release: {{ .Release.Name }}
|
||||||
heritage: {{ .Release.Service }}
|
heritage: {{ .Release.Service }}
|
||||||
component: backend
|
|
||||||
spec:
|
spec:
|
||||||
replicas: {{ .Values.backend.replicaCount }}
|
replicas: {{ .Values.replicaCount }}
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app: {{ include "oramap.name" . }}-backend
|
app: {{ include "oramap.name" . }}
|
||||||
release: {{ .Release.Name }}
|
release: {{ .Release.Name }}
|
||||||
component: backend
|
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: {{ include "oramap.name" . }}-backend
|
app: {{ include "oramap.name" . }}
|
||||||
release: {{ .Release.Name }}
|
release: {{ .Release.Name }}
|
||||||
component: backend
|
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: backend
|
- name: {{ .Chart.Name }}
|
||||||
image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag }}"
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||||
imagePullPolicy: {{ .Values.backend.image.pullPolicy }}
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
ports:
|
ports:
|
||||||
- containerPort: {{ .Values.backend.containerPort }}
|
- containerPort: {{ .Values.containerPort }}
|
||||||
name: http
|
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:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /
|
path: /
|
||||||
port: http
|
port: http
|
||||||
initialDelaySeconds: 10
|
|
||||||
periodSeconds: 30
|
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /
|
path: /
|
||||||
port: http
|
port: http
|
||||||
initialDelaySeconds: 5
|
|
||||||
periodSeconds: 10
|
|
||||||
resources:
|
resources:
|
||||||
{{- toYaml .Values.frontend.resources | nindent 12 }}
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
volumes:
|
|
||||||
- name: nginx-config
|
|
||||||
configMap:
|
|
||||||
name: {{ include "oramap.fullname" . }}-nginx-config
|
|
||||||
|
|||||||
@ -21,9 +21,9 @@ spec:
|
|||||||
pathType: {{ .pathType }}
|
pathType: {{ .pathType }}
|
||||||
backend:
|
backend:
|
||||||
service:
|
service:
|
||||||
name: {{ include "oramap.fullname" $ }}-frontend
|
name: {{ include "oramap.fullname" $ }}
|
||||||
port:
|
port:
|
||||||
number: {{ $.Values.service.frontend.port }}
|
number: {{ $.Values.service.port }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if .Values.ingress.tls }}
|
{{- 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
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
metadata:
|
metadata:
|
||||||
name: {{ include "oramap.fullname" . }}-backend
|
name: {{ include "oramap.fullname" . }}
|
||||||
labels:
|
labels:
|
||||||
app: {{ include "oramap.name" . }}-backend
|
app: {{ include "oramap.name" . }}
|
||||||
chart: {{ include "oramap.chart" . }}
|
chart: {{ include "oramap.chart" . }}
|
||||||
release: {{ .Release.Name }}
|
release: {{ .Release.Name }}
|
||||||
heritage: {{ .Release.Service }}
|
heritage: {{ .Release.Service }}
|
||||||
component: backend
|
|
||||||
spec:
|
spec:
|
||||||
type: {{ .Values.service.type }}
|
type: {{ .Values.service.type }}
|
||||||
ports:
|
ports:
|
||||||
- port: {{ .Values.service.backend.port }}
|
- port: {{ .Values.service.port }}
|
||||||
targetPort: {{ .Values.service.backend.targetPort }}
|
targetPort: {{ .Values.containerPort }}
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
name: http
|
name: http
|
||||||
selector:
|
selector:
|
||||||
app: {{ include "oramap.name" . }}-backend
|
app: {{ include "oramap.name" . }}
|
||||||
release: {{ .Release.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
|
replicaCount: 1
|
||||||
|
|
||||||
# Backend API configuration
|
image:
|
||||||
backend:
|
repository: harbor.dvirlabs.com/shay/oramap
|
||||||
image:
|
tag: "1"
|
||||||
repository: harbor.dvirlabs.com/my-apps/oramap-backend
|
pullPolicy: IfNotPresent
|
||||||
tag: "v1.1.0"
|
|
||||||
pullPolicy: Always
|
|
||||||
containerPort: 3000
|
|
||||||
replicaCount: 1
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpu: 500m
|
|
||||||
memory: 512Mi
|
|
||||||
requests:
|
|
||||||
cpu: 100m
|
|
||||||
memory: 128Mi
|
|
||||||
|
|
||||||
# Frontend Nginx configuration
|
containerPort: 3000
|
||||||
frontend:
|
|
||||||
image:
|
|
||||||
repository: harbor.dvirlabs.com/my-apps/oramap-frontend
|
|
||||||
tag: "v1.1.0"
|
|
||||||
pullPolicy: Always
|
|
||||||
containerPort: 80
|
|
||||||
replicaCount: 1
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpu: 200m
|
|
||||||
memory: 256Mi
|
|
||||||
requests:
|
|
||||||
cpu: 50m
|
|
||||||
memory: 64Mi
|
|
||||||
|
|
||||||
service:
|
service:
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
backend:
|
port: 80
|
||||||
port: 3000
|
|
||||||
targetPort: 3000
|
|
||||||
frontend:
|
|
||||||
port: 80
|
|
||||||
targetPort: 80
|
|
||||||
|
|
||||||
# MongoDB configuration
|
|
||||||
mongodb:
|
|
||||||
enabled: true
|
|
||||||
image:
|
|
||||||
repository: mongo
|
|
||||||
tag: "7.0"
|
|
||||||
pullPolicy: IfNotPresent
|
|
||||||
persistence:
|
|
||||||
enabled: true
|
|
||||||
storageClass: ""
|
|
||||||
size: 5Gi
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpu: 500m
|
|
||||||
memory: 512Mi
|
|
||||||
requests:
|
|
||||||
cpu: 100m
|
|
||||||
memory: 256Mi
|
|
||||||
database: oramap
|
|
||||||
|
|
||||||
ingress:
|
ingress:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||