feat: overhaul UI with new paper-themed layout and Libre Franklin typography

This commit is contained in:
GuavaPopper
2026-06-13 04:36:24 +07:00
parent 26995d6b47
commit 586f901824
2 changed files with 350 additions and 232 deletions
+7 -8
View File
@@ -4,12 +4,12 @@ Aplikasi WebGIS sederhana untuk menampilkan peta choropleth jumlah penduduk per
## Fitur ## Fitur
- Peta dasar citra satelit dari Esri. - Peta dasar terang dari CARTO (gaya atlas/kartografi).
- Layer batas kecamatan Kota Pontianak. - Layer batas kecamatan Kota Pontianak dengan label tempat di atas layer choropleth.
- Pewarnaan choropleth berdasarkan jumlah penduduk. - Pewarnaan choropleth berdasarkan jumlah penduduk.
- Popup dan panel info saat area kecamatan diarahkan cursor. - Sidebar berisi total penduduk, daftar kecamatan dengan bar pembanding, dan legenda.
- Zoom otomatis ke batas kecamatan saat area diklik. - Hover pada daftar kecamatan menyorot wilayahnya di peta (dan sebaliknya).
- Legenda rentang jumlah penduduk. - Zoom otomatis ke batas kecamatan saat area atau baris daftar diklik.
## Struktur File ## Struktur File
@@ -25,7 +25,6 @@ Aplikasi WebGIS sederhana untuk menampilkan peta choropleth jumlah penduduk per
- HTML, CSS, dan JavaScript - HTML, CSS, dan JavaScript
- Leaflet - Leaflet
- Proj4js - Proj4js
- Turf.js
- GeoJSON - GeoJSON
## Cara Menjalankan ## Cara Menjalankan
@@ -35,7 +34,7 @@ Jalankan project melalui web server lokal agar pemanggilan file GeoJSON dapat be
Jika menggunakan Laragon, letakkan folder project di direktori `www`, lalu buka: Jika menggunakan Laragon, letakkan folder project di direktori `www`, lalu buka:
```text ```text
http://localhost/GIS_Brutal/ http://localhost/WebGIS_Choropleth/
``` ```
Atau gunakan server lokal lain dari direktori project: Atau gunakan server lokal lain dari direktori project:
@@ -56,4 +55,4 @@ File `Admin_Kecamatan.json` berisi batas administrasi kecamatan Kota Pontianak.
## Catatan ## Catatan
Aplikasi membutuhkan koneksi internet untuk memuat library CDN dan tile peta Esri. Aplikasi membutuhkan koneksi internet untuk memuat library CDN dan tile peta CARTO.
+330 -211
View File
@@ -3,220 +3,308 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WebGIS Choropleth Kecamatan Pontianak</title> <title>Sebaran Penduduk Kota Pontianak — Peta Tematik per Kecamatan</title>
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,600;9..144,700&family=Libre+Franklin:wght@400;500;600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" /> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
<style> <style>
:root { :root {
--bg-1: #091821; --paper: #f7f3ea;
--bg-2: #103447; --paper-deep: #efe9db;
--panel: rgba(9, 24, 33, 0.78); --ink: #292420;
--text-main: #e8f5f9; --ink-soft: #6f675b;
--text-sub: #acd1db; --rule: #d9d1bf;
--accent: #ffd166; --accent: #a8501a;
--border: rgba(255, 255, 255, 0.15);
--shadow: 0 12px 45px rgba(0, 0, 0, 0.35);
} }
* { * { box-sizing: border-box; }
box-sizing: border-box;
}
html, html, body {
body {
width: 100%;
height: 100%; height: 100%;
margin: 0; margin: 0;
font-family: "Space Grotesk", sans-serif; font-family: "Libre Franklin", sans-serif;
color: var(--text-main); color: var(--ink);
background: background: var(--paper);
radial-gradient(circle at 12% 20%, rgba(255, 209, 102, 0.14), transparent 40%),
radial-gradient(circle at 87% 12%, rgba(77, 174, 255, 0.2), transparent 45%),
linear-gradient(130deg, var(--bg-1), var(--bg-2));
overflow: hidden;
} }
.app { .layout {
position: relative; display: grid;
width: 100%; grid-template-columns: 340px 1fr;
height: 100%; height: 100%;
padding: 16px;
animation: enter 0.7s ease;
} }
@keyframes enter { /* ---------- Sidebar ---------- */
from {
transform: translateY(8px); .panel {
opacity: 0; display: flex;
} flex-direction: column;
to { border-right: 1px solid var(--rule);
transform: translateY(0); background: var(--paper);
opacity: 1; overflow-y: auto;
}
} }
#map { .panel-inner {
width: 100%; padding: 28px 26px 20px;
height: 100%; display: flex;
border-radius: 18px; flex-direction: column;
border: 1px solid var(--border); flex: 1;
box-shadow: var(--shadow);
overflow: hidden;
} }
.hud { .overline {
position: absolute; font-size: 0.68rem;
left: 30px; font-weight: 600;
top: 30px; letter-spacing: 0.18em;
z-index: 1000; text-transform: uppercase;
width: min(420px, calc(100% - 60px));
padding: 16px 18px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 14px;
backdrop-filter: blur(7px);
box-shadow: var(--shadow);
}
.hud h1 {
margin: 0 0 6px;
font-size: 1.24rem;
line-height: 1.3;
font-weight: 700;
letter-spacing: 0.2px;
}
.hud p {
margin: 0;
font-size: 0.92rem;
color: var(--text-sub);
line-height: 1.4;
}
.chip {
display: inline-flex;
margin-top: 10px;
padding: 5px 9px;
border-radius: 999px;
border: 1px solid rgba(255, 209, 102, 0.4);
color: var(--accent); color: var(--accent);
font-size: 0.78rem; margin: 0 0 14px;
font-family: "IBM Plex Mono", monospace;
letter-spacing: 0.2px;
background: rgba(255, 209, 102, 0.08);
} }
.leaflet-container { h1 {
background: #0b202d; font-family: "Fraunces", serif;
font-weight: 700;
font-size: 1.85rem;
line-height: 1.15;
margin: 0 0 12px;
}
.lede {
font-size: 0.84rem;
line-height: 1.55;
color: var(--ink-soft);
margin: 0 0 22px;
}
.total {
border-top: 1px solid var(--ink);
border-bottom: 1px solid var(--rule);
padding: 14px 0;
margin-bottom: 22px;
}
.total-num {
font-family: "Fraunces", serif;
font-weight: 600;
font-size: 2rem;
line-height: 1;
font-variant-numeric: tabular-nums;
}
.total-label {
font-size: 0.72rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--ink-soft);
margin-top: 6px;
}
.section-title {
font-size: 0.68rem;
font-weight: 600;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--ink-soft);
margin: 0 0 10px;
}
.rows {
margin: 0 0 24px;
border-top: 1px solid var(--rule);
}
.row {
display: grid;
grid-template-columns: 1fr auto;
grid-template-areas: "name val" "bar bar";
width: 100%;
padding: 9px 2px 10px;
border: 0;
border-bottom: 1px solid var(--rule);
background: none;
font: inherit; font: inherit;
color: inherit;
text-align: left;
cursor: pointer;
} }
.legend, .row:hover,
.info-box { .row.is-active {
padding: 10px 12px; background: var(--paper-deep);
background: rgba(7, 20, 28, 0.92);
border: 1px solid var(--border);
border-radius: 10px;
color: var(--text-main);
line-height: 1.35;
box-shadow: var(--shadow);
} }
.legend h4 { .row-name {
margin: 0 0 8px; grid-area: name;
font-size: 0.88rem; font-size: 0.82rem;
color: #dff4ff; font-weight: 500;
} }
.row-val {
grid-area: val;
font-size: 0.82rem;
font-variant-numeric: tabular-nums;
color: var(--ink-soft);
}
.row-bar {
grid-area: bar;
height: 5px;
margin-top: 7px;
background: var(--paper-deep);
}
.row-bar i {
display: block;
height: 100%;
}
.legend-rows { margin-bottom: 24px; }
.legend-row { .legend-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 10px;
margin: 4px 0; padding: 3px 0;
font-size: 0.8rem; font-size: 0.78rem;
color: var(--text-sub); font-variant-numeric: tabular-nums;
font-family: "IBM Plex Mono", monospace; color: var(--ink-soft);
} }
.swatch { .swatch {
width: 16px; width: 22px;
height: 16px; height: 13px;
border-radius: 4px; border: 1px solid rgba(41, 36, 32, 0.25);
border: 1px solid rgba(255, 255, 255, 0.26); flex: 0 0 22px;
flex: 0 0 16px;
} }
.panel-foot {
margin-top: auto;
padding-top: 16px;
border-top: 1px solid var(--rule);
font-size: 0.7rem;
line-height: 1.6;
color: var(--ink-soft);
}
.panel-foot .status { font-variant-numeric: tabular-nums; }
/* ---------- Map ---------- */
.map-wrap { position: relative; }
#map {
width: 100%;
height: 100%;
background: #eef0ea;
}
.leaflet-container { font: inherit; }
.info-box { .info-box {
width: 230px; padding: 10px 14px;
background: #fffdf8;
border: 1px solid var(--rule);
border-left: 3px solid var(--accent);
box-shadow: 0 1px 3px rgba(41, 36, 32, 0.12);
color: var(--ink);
line-height: 1.45;
font-size: 0.8rem;
min-width: 200px;
} }
.info-box b { .info-box b {
color: #fff; font-family: "Fraunces", serif;
font-size: 0.95rem;
} }
.status { .leaflet-bar a,
position: absolute; .leaflet-bar a:hover {
right: 26px; background: #fffdf8;
bottom: 20px; color: var(--ink);
z-index: 1000; border-bottom-color: var(--rule);
padding: 8px 10px; border-radius: 0;
border-radius: 9px;
border: 1px solid var(--border);
background: rgba(9, 24, 33, 0.86);
color: var(--text-sub);
font-family: "IBM Plex Mono", monospace;
font-size: 0.76rem;
} }
@media (max-width: 720px) { .leaflet-touch .leaflet-bar { border: 1px solid var(--rule); }
.app {
padding: 10px; .leaflet-popup-content-wrapper {
background: #fffdf8;
color: var(--ink);
border: 1px solid var(--rule);
border-radius: 0;
box-shadow: 0 1px 3px rgba(41, 36, 32, 0.15);
} }
.hud { .leaflet-popup-tip { background: #fffdf8; }
left: 16px;
top: 16px; .leaflet-container .leaflet-control-attribution {
width: calc(100% - 32px); background: rgba(247, 243, 234, 0.85);
color: var(--ink-soft);
font-size: 0.62rem;
} }
.hud h1 { /* ---------- Mobile ---------- */
font-size: 1.05rem;
@media (max-width: 820px) {
.layout {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
} }
.status { .panel {
right: 16px; max-height: 46vh;
bottom: 12px; border-right: 0;
font-size: 0.7rem; border-bottom: 1px solid var(--rule);
} }
.panel-inner { padding: 20px 18px 14px; }
h1 { font-size: 1.4rem; }
.total-num { font-size: 1.5rem; }
} }
</style> </style>
</head> </head>
<body> <body>
<div class="app"> <div class="layout">
<div id="map"></div> <aside class="panel">
<div class="panel-inner">
<section class="hud"> <p class="overline">Peta Tematik · Kalimantan Barat</p>
<h1>WebGIS Choropleth Kecamatan Kota Pontianak</h1> <h1>Sebaran Penduduk Kota Pontianak</h1>
<p> <p class="lede">
Intensitas warna menunjukkan jumlah penduduk setiap kecamatan di Kota Pontianak. Choropleth jumlah penduduk pada enam kecamatan di Kota Pontianak.
Semakin pekat warnanya, semakin besar jumlah penduduknya. Klik
kecamatan pada daftar atau peta untuk memperbesar wilayahnya.
</p> </p>
<span class="chip">Leaflet + Proj4 + Turf</span>
</section>
<div class="status">Memuat data spasial...</div> <div class="total">
<div class="total-num" id="total-pop"></div>
<div class="total-label">Total penduduk · 6 kecamatan</div>
</div>
<p class="section-title">Kecamatan</p>
<div class="rows" id="rows"></div>
<p class="section-title">Jumlah penduduk (jiwa)</p>
<div class="legend-rows" id="legend"></div>
<div class="panel-foot">
Sumber batas administrasi: Admin_Kecamatan.json (EPSG:32749, dikonversi ke WGS 84).<br />
<span class="status" id="status">Memuat data spasial…</span>
</div>
</div>
</aside>
<div class="map-wrap">
<div id="map"></div>
</div>
</div> </div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/proj4js/2.9.2/proj4.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/proj4js/2.9.2/proj4.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@turf/turf@6/turf.min.js"></script>
<script> <script>
const statusEl = document.querySelector('.status'); const statusEl = document.getElementById('status');
const map = L.map('map', { const map = L.map('map', {
zoomControl: true, zoomControl: true,
@@ -225,31 +313,23 @@
preferCanvas: true preferCanvas: true
}); });
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { L.tileLayer('https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png', {
attribution: 'Tiles &copy; Esri &mdash; Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community' attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> &copy; <a href="https://carto.com/attributions">CARTO</a>',
subdomains: 'abcd'
}).addTo(map); }).addTo(map);
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}', { map.createPane('labels');
attribution: 'Labels &copy; Esri' map.getPane('labels').style.zIndex = 650;
map.getPane('labels').style.pointerEvents = 'none';
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}{r}.png', {
pane: 'labels',
subdomains: 'abcd'
}).addTo(map); }).addTo(map);
proj4.defs('EPSG:32749', '+proj=utm +zone=49 +south +datum=WGS84 +units=m +no_defs +type=crs'); proj4.defs('EPSG:32749', '+proj=utm +zone=49 +south +datum=WGS84 +units=m +no_defs +type=crs');
const palette = ['#fff8dc', '#fee7ad', '#f9c76f', '#f39f4d', '#d97706', '#a84906']; const palette = ['#fdf3d8', '#f7dfa2', '#eec06c', '#dd9440', '#bc651f', '#8c400d'];
function round2(value) {
return Math.round((value + Number.EPSILON) * 100) / 100;
}
function getQuantileBreaks(values, steps) {
const sorted = [...values].sort((a, b) => a - b);
const breaks = [];
for (let i = 0; i <= steps; i++) {
const idx = Math.floor((i / steps) * (sorted.length - 1));
breaks.push(sorted[idx]);
}
return breaks;
}
function convertCoords(coords) { function convertCoords(coords) {
if (!Array.isArray(coords[0])) { if (!Array.isArray(coords[0])) {
@@ -301,63 +381,96 @@
info.update = function (props) { info.update = function (props) {
this._div.innerHTML = props this._div.innerHTML = props
? `<b>${props.Ket}</b><br>Jumlah Penduduk: ${props.pop.toLocaleString('id-ID')} jiwa` ? `<b>${props.Ket}</b><br>${props.pop.toLocaleString('id-ID')} jiwa`
: 'Arahkan cursor ke kecamatan'; : 'Arahkan kursor ke kecamatan';
}; };
info.addTo(map); info.addTo(map);
function createLegend(breaks) { function renderLegend(breaks) {
const legend = L.control({ position: 'bottomright' }); const wrap = document.getElementById('legend');
legend.onAdd = function () { for (let i = 0; i < palette.length; i++) {
const div = L.DomUtil.create('div', 'legend');
div.innerHTML = '<h4>Jumlah Penduduk (jiwa)</h4>';
for (let i = 0; i < 6; i++) {
const from = breaks[i]; const from = breaks[i];
const to = breaks[i + 1]; const to = breaks[i + 1];
const label = to const label = to
? `${from.toLocaleString('id-ID')} - ${to.toLocaleString('id-ID')}` ? `${from.toLocaleString('id-ID')} ${to.toLocaleString('id-ID')}`
: `> ${from.toLocaleString('id-ID')}`; : `> ${from.toLocaleString('id-ID')}`;
const row = document.createElement('div'); const row = document.createElement('div');
row.className = 'legend-row'; row.className = 'legend-row';
row.innerHTML = `<span class="swatch" style="background:${palette[i]}"></span><span>${label}</span>`; row.innerHTML = `<span class="swatch" style="background:${palette[i]}"></span><span>${label}</span>`;
div.appendChild(row); wrap.appendChild(row);
}
} }
return div; function renderRows(features, breaks, layerByName, layerRef) {
}; const wrap = document.getElementById('rows');
const maxPop = Math.max(...features.map((f) => f.properties.pop));
const sorted = [...features].sort((a, b) => b.properties.pop - a.properties.pop);
legend.addTo(map); sorted.forEach((feature) => {
} const { Ket, pop } = feature.properties;
const row = document.createElement('button');
row.type = 'button';
row.className = 'row';
row.dataset.name = Ket;
row.innerHTML = `
<span class="row-name">${Ket}</span>
<span class="row-val">${pop.toLocaleString('id-ID')}</span>
<span class="row-bar"><i style="width:${(pop / maxPop) * 100}%;background:${colorFromBreaks(pop, breaks)}"></i></span>
`;
function onEachFeatureFactory(layerRef) { row.addEventListener('mouseenter', () => {
return function onEachFeature(layer) { const layer = layerByName.get(Ket);
const feature = layer.feature; if (layer) highlight(layer);
layer.on({
mouseover: (e) => {
const target = e.target;
target.setStyle({
weight: 3,
color: '#ffffff',
fillOpacity: 0.94
}); });
target.bringToFront(); row.addEventListener('mouseleave', () => {
info.update(target.feature.properties); const layer = layerByName.get(Ket);
}, if (layer) {
layerRef.resetStyle(layer);
info.update();
setActiveRow(null);
}
});
row.addEventListener('click', () => {
const layer = layerByName.get(Ket);
if (layer) map.fitBounds(layer.getBounds(), { padding: [24, 24] });
});
wrap.appendChild(row);
});
}
function setActiveRow(name) {
document.querySelectorAll('.row').forEach((row) => {
row.classList.toggle('is-active', row.dataset.name === name);
});
}
function highlight(layer) {
layer.setStyle({ weight: 2.4, color: '#292420', fillOpacity: 0.95 });
layer.bringToFront();
info.update(layer.feature.properties);
setActiveRow(layer.feature.properties.Ket);
}
function bindFeatureEvents(layerRef) {
return function (layer) {
layer.on({
mouseover: (e) => highlight(e.target),
mouseout: (e) => { mouseout: (e) => {
layerRef.resetStyle(e.target); layerRef.resetStyle(e.target);
info.update(); info.update();
setActiveRow(null);
}, },
click: (e) => { click: (e) => {
map.fitBounds(e.target.getBounds(), { padding: [16, 16] }); map.fitBounds(e.target.getBounds(), { padding: [24, 24] });
} }
}); });
const p = feature.properties; const p = layer.feature.properties;
layer.bindPopup( layer.bindPopup(
`<b>${p.Ket}</b><br/>Jumlah Penduduk: <b>${p.pop.toLocaleString('id-ID')} jiwa</b>` `<b>${p.Ket}</b><br/>Jumlah penduduk: <b>${p.pop.toLocaleString('id-ID')} jiwa</b>`
); );
}; };
} }
@@ -366,7 +479,7 @@
const candidates = [ const candidates = [
'./Admin_Kecamatan.json', './Admin_Kecamatan.json',
'Admin_Kecamatan.json', 'Admin_Kecamatan.json',
'/GIS_Brutal/Admin_Kecamatan.json', '/WebGIS_Choropleth/Admin_Kecamatan.json',
'/Admin_Kecamatan.json' '/Admin_Kecamatan.json'
]; ];
@@ -406,35 +519,41 @@
const rawGeoJson = await fetchGeoJsonWithFallback(); const rawGeoJson = await fetchGeoJsonWithFallback();
const geojson = convertFeatureCollectionToWGS84(rawGeoJson); const geojson = convertFeatureCollectionToWGS84(rawGeoJson);
const values = geojson.features.map((f) => { let totalPop = 0;
const name = f.properties.Ket; geojson.features.forEach((f) => {
const pop = populationData[name] || 0; const pop = populationData[f.properties.Ket] || 0;
f.properties.pop = pop; f.properties.pop = pop;
return pop; totalPop += pop;
}); });
const breaks = [0, 50000, 95000, 115000, 135000, 150000]; const breaks = [0, 50000, 95000, 115000, 135000, 150000];
const layerRef = L.geoJSON(geojson, { const layerRef = L.geoJSON(geojson, {
style: (feature) => ({ style: (feature) => ({
color: '#0e2733', color: '#8a8270',
weight: 1.4, weight: 1,
fillColor: colorFromBreaks(feature.properties.pop, breaks), fillColor: colorFromBreaks(feature.properties.pop, breaks),
fillOpacity: 0.86 fillOpacity: 0.82
}) })
}); });
layerRef.eachLayer(onEachFeatureFactory(layerRef)); const layerByName = new Map();
layerRef.eachLayer((layer) => {
layerByName.set(layer.feature.properties.Ket, layer);
});
layerRef.eachLayer(bindFeatureEvents(layerRef));
layerRef.addTo(map); layerRef.addTo(map);
map.fitBounds(layerRef.getBounds(), { padding: [18, 18] }); map.fitBounds(layerRef.getBounds(), { padding: [24, 24] });
createLegend(breaks);
statusEl.textContent = 'Data termuat: 6 kecamatan'; document.getElementById('total-pop').textContent = totalPop.toLocaleString('id-ID');
renderRows(geojson.features, breaks, layerByName, layerRef);
renderLegend(breaks);
statusEl.textContent = `Data termuat: ${geojson.features.length} kecamatan.`;
} catch (error) { } catch (error) {
statusEl.textContent = 'Terjadi error saat memuat data'; statusEl.textContent = 'Terjadi error saat memuat data spasial.';
console.error(error); console.error(error);
alert(`Data gagal dimuat: ${error.message}`);
} }
} }