Restructure project and update Helm chart for microservices
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Directory Changes: - Moved old unused files to 'old/' directory: - server.js, package.json, package-lock.json, node_modules - data/, public/, run_ora_map.sh, README copy.md - Clean root structure with only backend/ and frontend/ dirs - Updated .gitignore to ignore old directories Helm Chart Updates (oramap/): - Updated Chart.yaml to v0.2.0 with proper description - Split values.yaml for separate backend and frontend configurations - Created separate deployments for backend and frontend - Created separate services for backend (port 3000) and frontend (port 80) - Added ConfigMap for nginx configuration in Kubernetes - Added volume mounts for nginx config in frontend deployment - Updated ingress to route to frontend service - Added proper health checks for both services - Added resource limits and requests Frontend Updates: - Enhanced nginx.conf with dynamic backend routing - Supports both Docker Compose and Kubernetes service discovery This enables proper microservices deployment in Kubernetes with: - Separate scaling for frontend and backend - Independent deployments and rollbacks - Proper service discovery and load balancing
This commit is contained in:
parent
fccd8a0166
commit
c457db534b
16
.gitignore
vendored
16
.gitignore
vendored
@ -1,3 +1,19 @@
|
||||
node_modules/
|
||||
package-lock.json
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
nul
|
||||
|
||||
# Old files
|
||||
old/
|
||||
old-project/
|
||||
oramap-chart/
|
||||
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
# How to Run
|
||||
```
|
||||
npm init -y
|
||||
npm install express
|
||||
node server.js
|
||||
```
|
||||
@ -1,10 +0,0 @@
|
||||
[
|
||||
{ "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 }
|
||||
]
|
||||
@ -13,9 +13,19 @@ server {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# API proxy to backend
|
||||
# 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/ {
|
||||
proxy_pass http://backend:3000;
|
||||
# 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';
|
||||
|
||||
@ -1,24 +1,11 @@
|
||||
apiVersion: v2
|
||||
name: oramap
|
||||
description: A Helm chart for Kubernetes
|
||||
description: Ora Map - Family Location Mapping Application (Microservices Architecture)
|
||||
|
||||
# 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
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.1.0
|
||||
# Chart version
|
||||
version: 0.2.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "1.16.0"
|
||||
# Application version
|
||||
appVersion: "1.0.0"
|
||||
|
||||
45
oramap/templates/configmap.yaml
Normal file
45
oramap/templates/configmap.yaml
Normal file
@ -0,0 +1,45 @@
|
||||
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;
|
||||
}
|
||||
|
||||
# Cache static assets
|
||||
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}
|
||||
@ -1,38 +1,107 @@
|
||||
---
|
||||
# Backend Deployment
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "oramap.fullname" . }}
|
||||
name: {{ include "oramap.fullname" . }}-backend
|
||||
labels:
|
||||
app: {{ include "oramap.name" . }}
|
||||
app: {{ include "oramap.name" . }}-backend
|
||||
chart: {{ include "oramap.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
component: backend
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
replicas: {{ .Values.backend.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ include "oramap.name" . }}
|
||||
app: {{ include "oramap.name" . }}-backend
|
||||
release: {{ .Release.Name }}
|
||||
component: backend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ include "oramap.name" . }}
|
||||
app: {{ include "oramap.name" . }}-backend
|
||||
release: {{ .Release.Name }}
|
||||
component: backend
|
||||
spec:
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
- name: backend
|
||||
image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.backend.image.pullPolicy }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.containerPort }}
|
||||
- containerPort: {{ .Values.backend.containerPort }}
|
||||
name: http
|
||||
env:
|
||||
- name: NODE_ENV
|
||||
value: "production"
|
||||
- name: PORT
|
||||
value: "{{ .Values.backend.containerPort }}"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: http
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
{{- toYaml .Values.backend.resources | nindent 12 }}
|
||||
---
|
||||
# Frontend Deployment
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "oramap.fullname" . }}-frontend
|
||||
labels:
|
||||
app: {{ include "oramap.name" . }}-frontend
|
||||
chart: {{ include "oramap.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
component: frontend
|
||||
spec:
|
||||
replicas: {{ .Values.frontend.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ include "oramap.name" . }}-frontend
|
||||
release: {{ .Release.Name }}
|
||||
component: frontend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ include "oramap.name" . }}-frontend
|
||||
release: {{ .Release.Name }}
|
||||
component: frontend
|
||||
spec:
|
||||
containers:
|
||||
- name: frontend
|
||||
image: "{{ .Values.frontend.image.repository }}:{{ .Values.frontend.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.frontend.image.pullPolicy }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.frontend.containerPort }}
|
||||
name: http
|
||||
volumeMounts:
|
||||
- name: nginx-config
|
||||
mountPath: /etc/nginx/conf.d/default.conf
|
||||
subPath: default.conf
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- toYaml .Values.frontend.resources | nindent 12 }}
|
||||
volumes:
|
||||
- name: nginx-config
|
||||
configMap:
|
||||
name: {{ include "oramap.fullname" . }}-nginx-config
|
||||
|
||||
@ -21,9 +21,9 @@ spec:
|
||||
pathType: {{ .pathType }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "oramap.fullname" $ }}
|
||||
name: {{ include "oramap.fullname" $ }}-frontend
|
||||
port:
|
||||
number: {{ $.Values.service.port }}
|
||||
number: {{ $.Values.service.frontend.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
|
||||
@ -1,19 +1,46 @@
|
||||
---
|
||||
# Backend Service
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "oramap.fullname" . }}
|
||||
name: {{ include "oramap.fullname" . }}-backend
|
||||
labels:
|
||||
app: {{ include "oramap.name" . }}
|
||||
app: {{ include "oramap.name" . }}-backend
|
||||
chart: {{ include "oramap.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
component: backend
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: {{ .Values.containerPort }}
|
||||
- port: {{ .Values.service.backend.port }}
|
||||
targetPort: {{ .Values.service.backend.targetPort }}
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
app: {{ include "oramap.name" . }}
|
||||
app: {{ include "oramap.name" . }}-backend
|
||||
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,15 +1,45 @@
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: harbor.dvirlabs.com/shay/oramap
|
||||
tag: "1"
|
||||
# Backend API configuration
|
||||
backend:
|
||||
image:
|
||||
repository: harbor.dvirlabs.com/my-apps/oramap-backend
|
||||
tag: "latest"
|
||||
pullPolicy: IfNotPresent
|
||||
containerPort: 3000
|
||||
replicaCount: 1
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
|
||||
containerPort: 3000
|
||||
# Frontend Nginx configuration
|
||||
frontend:
|
||||
image:
|
||||
repository: harbor.dvirlabs.com/my-apps/oramap-frontend
|
||||
tag: "latest"
|
||||
pullPolicy: IfNotPresent
|
||||
containerPort: 80
|
||||
replicaCount: 1
|
||||
resources:
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 256Mi
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
backend:
|
||||
port: 3000
|
||||
targetPort: 3000
|
||||
frontend:
|
||||
port: 80
|
||||
targetPort: 80
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
|
||||
@ -1,26 +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>
|
||||
</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>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 975 KiB |
BIN
public/logo.png
BIN
public/logo.png
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB |
124
public/script.js
124
public/script.js
@ -1,124 +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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
@ -1,98 +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;
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
npm init -y
|
||||
npm install express
|
||||
node server.js
|
||||
19
server.js
19
server.js
@ -1,19 +0,0 @@
|
||||
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}`);
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user