Add first decent cut of the Zola static site.

This commit is contained in:
2026-03-08 23:20:49 +05:30
parent 7c07e75dd9
commit 2f9b2bdb5a
15 changed files with 885 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
public/
content/spaces/

19
config.toml Normal file
View File

@@ -0,0 +1,19 @@
base_url = "https://ooru.space"
title = "Public and Community spaces in India"
description = "Directory of community and public spaces across India."
compile_sass = false
minify_html = true
[markdown]
[[taxonomies]]
name = "states"
feed = false
[[taxonomies]]
name = "cities"
feed = false
[[taxonomies]]
name = "categories"
feed = false

4
content/_index.md Normal file
View File

@@ -0,0 +1,4 @@
+++
title = "Community & Public Spaces in India"
sort_by = "title"
+++

1
static/india.geojson Normal file

File diff suppressed because one or more lines are too long

265
static/main.js Normal file
View File

@@ -0,0 +1,265 @@
document.addEventListener('DOMContentLoaded', () => {
const $ = s => document.querySelector(s), $$ = s => document.querySelectorAll(s);
const items = $$('[data-space]');
if (!items.length) {
return;
}
const elSearch = $('#search'), elCount = $('#count');
const elChk = $$('[data-filter]');
const elMap = $('[data-map]'), elShowMap = $('#show-map'), elToggles = $$('[data-toggle]');
const isMobile = matchMedia('(max-width: 900px)').matches;
let map, isMapLoaded = false, selItem = null;
const markers = [], markerItems = new Map();
// Build city+state mappings for filtering.
const cityToState = {}, stateToCities = {};
items.forEach(i => {
i._text = i.textContent.toLowerCase();
const { city, state } = i.dataset;
if (city && state) {
cityToState[city] = state;
(stateToCities[state] ||= new Set()).add(city);
}
});
// Checkbox groups for bulk toggling.
const groupBoxes = {};
elToggles.forEach(t => {
if (t.dataset.toggle) groupBoxes[t.dataset.toggle] = $$(`input[name="${t.dataset.toggle}"]`);
});
// Map logic.
function loadMap() {
if (isMapLoaded || isMobile) return;
// Lazy-load only if it's enabled + not mobile.
isMapLoaded = true;
document.head.append(
Object.assign(document.createElement('link'), { rel: 'stylesheet', href: 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css' })
);
const s = Object.assign(document.createElement('script'), { src: 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js' });
s.onload = initMap;
document.head.append(s);
}
const defStyle = { radius: 7, color: '#fff', weight: 1.5, fillColor: 'blue', fillOpacity: 0.85 };
const hiStyle = { radius: 10, color: '#fff', weight: 2, fillColor: 'blue', fillOpacity: 1 };
function styleMarker(m, style, open) {
m.setStyle(style); m.setRadius(style.radius);
if (open) { m.bringToFront(); m.openPopup(); } else m.closePopup();
}
// Initialize map with LeafletJS. Dim non-India areas.
function initMap() {
if (!$('#map') || typeof L === 'undefined') return;
map = L.map('map', { maxBounds: [[5, 67], [38, 98]], maxBoundsViscosity: 1, minZoom: 4 }).setView([22, 79], 5);
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap</a>, &copy; <a href="https://carto.com/">CARTO</a>',
subdomains: 'abcd'
}).addTo(map);
const addLabels = () => L.tileLayer(
'https://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}{r}.png',
{ subdomains: 'abcd', pane: 'shadowPane' }
).addTo(map);
// Fetch India's boundaries and apply to the map.
fetch('/india.geojson').then(r => r.json()).then(india => {
const world = [[-90, -180], [-90, 180], [90, 180], [90, -180]];
const polys = india.geometry.type === 'Polygon' ? [india.geometry.coordinates] : india.geometry.coordinates;
L.polygon([world, ...polys.map(p => p[0].map(c => [c[1], c[0]]))], {
color: 'none', fillColor: '#ddd', fillOpacity: 0.6, interactive: false
}).addTo(map);
addLabels();
}).catch(addLabels);
items.forEach(item => {
const lat = parseFloat(item.dataset.lat), lng = parseFloat(item.dataset.lng);
if (isNaN(lat) || isNaN(lng)) return;
// Build the pin-popup card.
const q = [item.dataset.name, item.dataset.address, item.dataset.city].filter(Boolean).join(', ');
const popup = `<strong>${item.dataset.name}</strong>`
+ (item.dataset.address ? `<br><span class="popup-address">${item.dataset.address}</span>` : '')
+ `<br><a href="https://www.google.com/maps/search/${encodeURIComponent(q)}" target="_blank" rel="noopener">Search on Google Maps &rarr;</a>`;
const marker = L.circleMarker([lat, lng], defStyle).bindPopup(popup, { autoClose: false }).addTo(map);
markers.push({ marker, item });
markerItems.set(item, marker);
item.addEventListener('mouseenter', () => {
if (selItem === item) return;
styleMarker(marker, hiStyle, true);
});
item.addEventListener('mouseleave', () => {
if (selItem === item) return;
styleMarker(marker, defStyle, false);
});
item.addEventListener('click', () => {
if (selItem) {
selItem.setAttribute('aria-selected', 'false');
const prev = markerItems.get(selItem);
if (prev) {
styleMarker(prev, defStyle, false);
}
}
if (selItem === item) {
selItem = null;
return;
}
selItem = item;
item.setAttribute('aria-selected', 'true');
styleMarker(marker, hiStyle, true);
});
marker.on('mouseover', () => {
if (selItem !== item) styleMarker(marker, hiStyle, true);
});
marker.on('mouseout', () => {
if (selItem !== item) styleMarker(marker, defStyle, false);
});
marker.on('click', () => {
if (selItem) {
selItem.setAttribute('aria-selected', 'false');
const prev = markerItems.get(selItem);
if (prev) styleMarker(prev, defStyle, false);
}
if (selItem === item) {
selItem = null;
return;
}
selItem = item;
item.setAttribute('aria-selected', 'true');
styleMarker(marker, hiStyle, true);
item.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
});
});
updateMap();
}
function updateMap() {
if (!map || elMap?.style.display === 'none') return;
const bounds = [];
markers.forEach(({ marker, item }) => {
item.hidden ? marker.remove() : (marker.addTo(map), bounds.push(marker.getLatLng()));
});
if (!bounds.length) return;
map.fitBounds(bounds, { padding: [30, 30], maxZoom: 12 });
}
// Filtering logic.
function cascadeFilters(changed) {
const cb = groupBoxes.city, sb = groupBoxes.state;
if (!cb || !sb) return;
if (changed.name === 'city') {
const need = new Set();
cb.forEach(c => {
if (c.checked && cityToState[c.value]) {
need.add(cityToState[c.value]);
}
});
sb.forEach(c => { c.checked = need.has(c.value); });
} else if (changed.name === 'state') {
const need = new Set();
sb.forEach(c => {
if (c.checked) {
(stateToCities[c.value] || []).forEach(v => need.add(v));
}
});
cb.forEach(c => { c.checked = need.has(c.value); });
}
}
function updateToggleLabels() {
elToggles.forEach(t => {
const boxes = groupBoxes[t.dataset.toggle];
if (!boxes) return;
t.textContent = Array.from(boxes).every(c => c.checked) ? 'Unselect all' : 'Select all';
});
}
function filter() {
const q = (elSearch?.value || '').toLowerCase();
const active = {};
elChk.forEach(cb => {
if (cb.checked) {
(active[cb.name] ||= []).push(cb.value);
}
});
let n = 0;
items.forEach(item => {
const show = (!q || item._text.includes(q)) && Object.entries(active).every(([k, v]) =>
k === 'category' ? v.some(x => item.dataset.categories.split(',').includes(x)) : v.includes(item.dataset[k])
);
item.hidden = !show;
if (show) {
n++;
}
});
elCount.textContent = n;
updateMap();
}
// Bind various control events.
elSearch?.addEventListener('input', filter);
elChk.forEach(cb => cb.addEventListener('change', e => {
cascadeFilters(e.target); updateToggleLabels(); filter();
}));
elShowMap?.addEventListener('change', () => {
if (!elMap) return;
if (elShowMap.checked) {
elMap.style.display = '';
isMapLoaded ? (map?.invalidateSize(), updateMap()) : loadMap();
} else elMap.style.display = 'none';
});
elToggles.forEach(t => t.addEventListener('click', e => {
e.preventDefault();
const boxes = groupBoxes[t.dataset.toggle];
if (!boxes) return;
const all = Array.from(boxes).every(c => c.checked);
boxes.forEach(c => c.checked = !all);
updateToggleLabels(); filter();
}));
// On mobile, just don't show the map.
if (isMobile) {
if (elShowMap) {
elShowMap.closest('label').style.display = 'none';
elShowMap.checked = false;
}
if (elMap) {
elMap.style.display = 'none';
}
} else if (!elShowMap || elShowMap?.checked) {
loadMap();
}
// Filter toggle for mobile.
const elFilterToggle = $('#filter-toggle');
const elFilterGroups = $('.filter-groups');
if (elFilterToggle && elFilterGroups) {
elFilterToggle.addEventListener('click', e => {
e.preventDefault();
const open = elFilterGroups.classList.toggle('open');
elFilterToggle.textContent = open ? 'Hide filters' : 'Show filters';
});
}
// Reset all controls on boot.
if (elSearch) elSearch.value = '';
elChk.forEach(cb => cb.checked = true);
updateToggleLabels(); filter();
});

79
static/samagata.svg Normal file
View File

@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="253.349"
height="74.237"
viewBox="0 0 67.032 19.642"
version="1.1"
id="svg11"
sodipodi:docname="samagata.svg"
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview11"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1"
inkscape:cx="127"
inkscape:cy="37"
inkscape:window-width="2560"
inkscape:window-height="1367"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg11" />
<g
fill="#a82929"
id="g2">
<path
d="M11.897 19.642H7.416l2.241-2.241zm0-4.514H7.416l2.241 2.241zm3.651-4.746l-2.576 3.664 3.12-.544zm-8.132-5.79h4.481l-2.24-2.241zm8.95 12.493l-3.895 2.221.837-3.058zm2.814-4.416l-2.254 3.876-.811-3.065zM2.856 17.056l3.892 2.218-.837-3.055zM.042 12.639l2.254 3.872.808-3.065zM2.387 2.96L.166 6.852l3.058-.837z"
id="path1" />
<path
d="M6.182 5.149L3.87 8.989l-.765-3.074zm-2.287 5.269l2.283 3.856-3.071-.785zm11.53-1.296l-2.43-3.765 3.097.668zM6.804.146L2.928 2.4l3.065.808zm9.66 2.306L12.62.146l.769 3.074zm2.716 4.475l-2.169-3.921-.876 3.045zM11.897 0H7.416l2.241 2.24zM0 7.523v4.481l2.241-2.241zm19.346 0v4.481l-2.241-2.241z"
id="path2" />
</g>
<g
fill="#333333"
id="g6"
transform="translate(0,3.096)">
<path
d="M 28.021,4.258 Q 27.973,3.764 27.769,3.367 27.566,2.97 27.207,2.737 26.848,2.495 26.316,2.504 q -0.543,0 -0.862,0.3 -0.32,0.3 -0.32,0.727 0,0.378 0.213,0.64 0.213,0.252 0.572,0.455 0.368,0.203 0.824,0.407 0.368,0.155 0.746,0.349 0.378,0.194 0.688,0.446 0.31,0.242 0.494,0.591 0.184,0.339 0.184,0.804 0,0.543 -0.281,0.969 -0.281,0.426 -0.804,0.669 -0.514,0.242 -1.25,0.242 -0.417,0 -0.882,-0.126 -0.455,-0.126 -0.833,-0.359 l 0.048,0.407 H 24.262 L 24.223,6.893 h 0.465 q 0.078,0.843 0.601,1.279 0.533,0.426 1.269,0.426 0.368,0 0.669,-0.136 0.3,-0.136 0.475,-0.388 0.174,-0.252 0.174,-0.591 0,-0.397 -0.242,-0.669 Q 27.401,6.543 27.033,6.349 26.674,6.155 26.277,5.981 25.88,5.807 25.502,5.613 25.134,5.419 24.833,5.167 24.533,4.915 24.358,4.576 24.184,4.227 24.184,3.762 q 0,-0.271 0.097,-0.572 0.097,-0.3 0.329,-0.572 0.233,-0.271 0.63,-0.436 0.407,-0.174 1.017,-0.174 0.368,0 0.824,0.107 0.465,0.107 0.843,0.378 L 27.885,2.086 h 0.581 v 2.17 z"
id="path3" />
<use
xlink:href="#B"
id="use3" />
<path
d="M 34.697,8.948 V 8.541 q 0.291,0 0.407,-0.136 Q 35.22,8.269 35.24,8.027 35.269,7.785 35.269,7.465 V 5.45 q 0,-0.155 0,-0.329 0.01,-0.184 0.039,-0.359 -0.165,0.01 -0.349,0.019 -0.184,0 -0.339,0.01 V 4.277 q 0.436,0 0.678,-0.039 0.252,-0.048 0.378,-0.107 0.136,-0.058 0.203,-0.126 h 0.349 q 0.019,0.097 0.019,0.233 0.01,0.126 0.019,0.271 0.3,-0.252 0.678,-0.407 0.388,-0.155 0.746,-0.155 0.475,0 0.785,0.174 0.32,0.165 0.504,0.475 0.339,-0.281 0.765,-0.465 0.426,-0.184 0.833,-0.184 0.824,0 1.221,0.494 0.397,0.484 0.388,1.628 l -0.01,1.676 q 0,0.174 -0.01,0.359 0,0.174 -0.029,0.359 0.155,-0.01 0.32,-0.01 0.165,-0.01 0.3,-0.019 V 8.948 H 40.625 V 8.541 q 0.271,0 0.378,-0.136 0.116,-0.136 0.136,-0.378 0.029,-0.242 0.029,-0.562 V 6.07 Q 41.158,5.305 40.916,4.946 40.674,4.587 40.18,4.587 q -0.281,0 -0.552,0.126 -0.271,0.116 -0.484,0.3 0.049,0.165 0.068,0.368 0.029,0.194 0.029,0.407 -0.01,0.484 -0.01,0.979 0,0.484 0,0.979 0,0.174 -0.01,0.359 -0.01,0.174 -0.029,0.359 0.155,-0.01 0.31,-0.01 0.165,-0.01 0.3,-0.019 V 8.949 H 37.651 V 8.542 q 0.291,0 0.397,-0.136 0.116,-0.136 0.136,-0.378 0.029,-0.242 0.029,-0.562 V 6.06 Q 38.223,5.295 37.99,4.936 37.767,4.568 37.273,4.577 q -0.281,0 -0.543,0.126 -0.262,0.116 -0.455,0.3 0,0.116 0,0.242 0.01,0.126 0.01,0.271 v 2.229 q 0,0.174 -0.01,0.359 -0.01,0.174 -0.029,0.359 0.155,-0.01 0.31,-0.01 0.155,-0.01 0.291,-0.019 v 0.514 z m 11.609,0 Q 46.287,8.793 46.277,8.677 46.267,8.561 46.248,8.435 45.919,8.764 45.541,8.939 45.163,9.104 44.756,9.104 44.058,9.104 43.7,8.765 43.351,8.416 43.351,7.903 q 0,-0.446 0.262,-0.775 0.262,-0.329 0.688,-0.533 0.426,-0.213 0.93,-0.31 0.504,-0.107 0.988,-0.107 V 5.606 q 0,-0.31 -0.058,-0.572 Q 46.103,4.763 45.919,4.598 45.745,4.424 45.367,4.424 45.115,4.414 44.863,4.521 44.611,4.618 44.475,4.85 q 0.078,0.077 0.097,0.184 0.029,0.097 0.029,0.184 0,0.136 -0.116,0.31 -0.116,0.165 -0.397,0.155 -0.233,0 -0.359,-0.155 -0.116,-0.165 -0.116,-0.378 0,-0.349 0.252,-0.62 0.262,-0.271 0.707,-0.426 0.446,-0.155 0.998,-0.155 0.833,0 1.24,0.436 0.417,0.436 0.417,1.376 0,0.349 0,0.669 0,0.32 -0.01,0.64 0,0.32 0,0.678 0,0.145 -0.01,0.329 -0.01,0.184 -0.029,0.388 0.165,-0.01 0.339,-0.019 0.174,-0.01 0.329,-0.01 V 8.95 Z M 46.219,6.622 q -0.31,0.019 -0.64,0.087 -0.32,0.068 -0.581,0.203 -0.262,0.136 -0.426,0.349 -0.155,0.213 -0.155,0.514 0.019,0.329 0.213,0.484 0.203,0.155 0.475,0.155 0.339,0 0.601,-0.126 0.262,-0.136 0.514,-0.368 -0.01,-0.107 -0.01,-0.213 0,-0.116 0,-0.242 0,-0.087 0,-0.32 0.01,-0.242 0.01,-0.523 z M 44.145,2.572 V 1.874 h 2.626 v 0.659 z m 6.579,9.002 q -0.853,0 -1.376,-0.194 -0.514,-0.184 -0.746,-0.484 -0.223,-0.291 -0.223,-0.62 0,-0.262 0.116,-0.484 Q 48.621,9.569 48.824,9.395 49.027,9.23 49.279,9.133 48.94,9.026 48.765,8.833 48.591,8.63 48.591,8.339 q 0,-0.31 0.242,-0.61 0.252,-0.3 0.717,-0.407 Q 49.114,7.128 48.852,6.76 48.59,6.392 48.581,5.888 q -0.01,-0.581 0.291,-1.017 0.31,-0.436 0.785,-0.678 0.484,-0.242 1.008,-0.242 0.3,0 0.62,0.087 0.32,0.077 0.581,0.252 0.107,-0.281 0.281,-0.514 0.174,-0.233 0.407,-0.368 0.242,-0.136 0.504,-0.136 0.281,0 0.426,0.145 0.145,0.145 0.145,0.388 0,0.077 -0.049,0.194 -0.039,0.107 -0.145,0.184 -0.107,0.077 -0.291,0.077 -0.155,0 -0.281,-0.097 -0.116,-0.107 -0.145,-0.252 -0.203,0.039 -0.339,0.242 -0.126,0.194 -0.165,0.397 0.252,0.223 0.388,0.533 0.145,0.3 0.145,0.649 0,0.552 -0.3,0.979 -0.291,0.426 -0.765,0.669 -0.475,0.242 -1.027,0.242 -0.165,0 -0.31,-0.019 -0.145,-0.029 -0.32,-0.029 -0.31,0 -0.514,0.145 -0.194,0.145 -0.145,0.349 0.049,0.213 0.407,0.281 0.359,0.068 1.037,0.097 0.746,0.029 1.279,0.184 0.543,0.145 0.833,0.455 0.3,0.31 0.3,0.843 0,0.397 -0.223,0.707 -0.213,0.31 -0.581,0.514 -0.359,0.213 -0.804,0.32 -0.446,0.107 -0.891,0.107 z m 0.087,-0.475 q 0.785,0 1.211,-0.291 0.436,-0.291 0.436,-0.659 0,-0.31 -0.213,-0.484 Q 52.032,9.5 51.644,9.423 51.266,9.355 50.753,9.346 50.501,9.327 50.239,9.317 49.977,9.307 49.755,9.269 49.532,9.424 49.406,9.647 49.29,9.87 49.28,10.112 q 0,0.446 0.417,0.717 0.417,0.271 1.114,0.271 z M 50.685,7.165 q 0.329,0 0.543,-0.165 0.223,-0.174 0.329,-0.455 0.116,-0.291 0.116,-0.63 0,-0.397 -0.116,-0.746 Q 51.441,4.82 51.218,4.607 50.995,4.394 50.646,4.394 q -0.475,0 -0.736,0.378 -0.262,0.378 -0.262,0.911 0,0.426 0.116,0.765 0.126,0.329 0.359,0.523 0.233,0.194 0.562,0.194 z"
id="path4" />
<use
xlink:href="#B"
x="24.156"
id="use4" />
<path
d="m 60.646,9.103 q -0.242,0 -0.494,-0.068 Q 59.91,8.977 59.706,8.793 59.503,8.599 59.386,8.241 59.27,7.882 59.27,7.291 L 59.289,4.646 H 58.572 V 4.103 q 0.262,-0.01 0.533,-0.194 0.271,-0.184 0.475,-0.494 0.203,-0.31 0.281,-0.669 h 0.455 v 1.357 h 1.492 v 0.504 l -1.492,0.019 -0.019,2.587 q 0,0.368 0.058,0.649 0.068,0.271 0.213,0.426 0.155,0.145 0.417,0.145 0.223,0 0.455,-0.136 0.242,-0.136 0.455,-0.446 l 0.329,0.291 Q 61.991,8.481 61.759,8.675 61.526,8.869 61.313,8.956 61.1,9.053 60.925,9.072 60.751,9.101 60.644,9.101 Z"
id="path5" />
<path
d="M 65.491,8.948 Q 65.472,8.793 65.462,8.677 65.452,8.561 65.433,8.435 q -0.329,0.329 -0.707,0.504 -0.378,0.165 -0.785,0.165 -0.698,0 -1.056,-0.339 -0.349,-0.349 -0.349,-0.862 0,-0.446 0.262,-0.775 0.262,-0.329 0.688,-0.533 0.426,-0.213 0.93,-0.31 0.504,-0.107 0.988,-0.107 V 5.606 q 0,-0.31 -0.058,-0.572 Q 65.288,4.763 65.104,4.598 64.93,4.424 64.552,4.424 64.3,4.414 64.048,4.521 63.796,4.618 63.66,4.85 q 0.077,0.077 0.097,0.184 0.029,0.097 0.029,0.184 0,0.136 -0.116,0.31 -0.116,0.165 -0.397,0.155 -0.233,0 -0.359,-0.155 -0.116,-0.165 -0.116,-0.378 0,-0.349 0.252,-0.62 0.262,-0.271 0.707,-0.426 0.446,-0.155 0.998,-0.155 0.833,0 1.24,0.436 0.417,0.436 0.417,1.376 0,0.349 0,0.669 0,0.32 -0.01,0.64 0,0.32 0,0.678 0,0.145 -0.01,0.329 -0.01,0.184 -0.029,0.388 0.165,-0.01 0.339,-0.019 0.174,-0.01 0.329,-0.01 V 8.95 Z M 65.404,6.622 q -0.31,0.019 -0.64,0.087 -0.32,0.068 -0.581,0.203 -0.262,0.136 -0.426,0.349 -0.155,0.213 -0.155,0.514 0.019,0.329 0.213,0.484 0.203,0.155 0.475,0.155 0.339,0 0.601,-0.126 0.262,-0.136 0.514,-0.368 -0.01,-0.107 -0.01,-0.213 0,-0.116 0,-0.242 0,-0.087 0,-0.32 0.01,-0.242 0.01,-0.523 z"
id="path6" />
</g>
<defs
id="defs11">
<path
id="B"
d="M32.527 8.948q-.019-.155-.029-.271-.01-.116-.029-.242-.329.329-.707.504-.378.165-.785.165-.698 0-1.056-.339-.349-.349-.349-.862 0-.446.262-.775.262-.329.688-.533.426-.213.93-.31.504-.107.988-.107v-.572q0-.31-.058-.572-.058-.271-.242-.436-.174-.174-.552-.174-.252-.01-.504.097-.252.097-.388.329.078.077.097.184.029.097.029.184 0 .136-.116.31-.116.165-.397.155-.233 0-.359-.155-.116-.165-.116-.378 0-.349.252-.62.262-.271.707-.426.446-.155.998-.155.833 0 1.24.436.417.436.417 1.376 0 .349 0 .669 0 .32-.01.64 0 .32 0 .678 0 .145-.01.329-.01.184-.029.388.165-.01.339-.019.174-.01.329-.01v.514zm-.087-2.326q-.31.019-.64.087-.32.068-.581.203-.262.136-.426.349-.155.213-.155.514.019.329.213.484.203.155.475.155.339 0 .601-.126.262-.136.514-.368-.01-.107-.01-.213 0-.116 0-.242 0-.087 0-.32.01-.242.01-.523z" />
<path
id="C"
d="M28.724 17.767q-.43 0-.779-.196-.35-.203-.558-.552-.203-.356-.203-.81 0-.485.203-.871.203-.387.552-.608.35-.227.786-.227.43 0 .779.209.35.203.558.552.209.35.209.798 0 .473-.209.865-.203.387-.552.614-.35.227-.786.227zm.049-.245q.325 0 .515-.184.19-.184.27-.479.086-.295.086-.62 0-.276-.055-.54-.049-.27-.16-.485-.11-.221-.288-.35-.172-.129-.417-.129-.319 0-.522.184-.203.184-.301.479-.092.295-.092.632 0 .38.104.724.104.338.319.552.215.215.54.215z" />
<path
id="D"
d="M34.456 17.669v-.184q.19 0 .264-.092.074-.098.086-.264.012-.172.012-.393l.006-1.399q0-.098 0-.196.006-.098.025-.203-.11.006-.221.012-.104.006-.215.012v-.258q.27 0 .405-.025.141-.024.209-.061.067-.037.11-.08h.178q.006.043.006.098.006.055.012.123.006.067.006.153.135-.117.295-.209.166-.092.344-.147.184-.055.362-.055.534 0 .779.338.245.331.245 1.037v1.141q0 .123-.006.221 0 .092-.018.196.098-.006.19-.006.098-.006.196-.012v.252h-1.252v-.184q.19 0 .264-.092.074-.098.086-.264.012-.172.012-.393v-.859q-.006-.528-.178-.798-.166-.27-.528-.264-.221.006-.436.117-.209.11-.35.282.006.049.006.11 0 .061 0 .129l-.006 1.565q0 .123-.006.221 0 .092-.018.196.098-.006.19-.006.098-.006.196-.012v.252z" />
</defs>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

274
static/style.css Normal file
View File

@@ -0,0 +1,274 @@
:root {
--text-heading: 1.1rem;
--text-regular: 0.9rem;
--text-small: 0.8rem;
--text-xsmall: 0.7rem;
--color-primary: blue;
--color-muted: #888;
--color-border: #ddd;
--color-hover: #f5f5f5;
}
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
}
html, body {
height: 100%;
overflow: hidden;
}
body {
font-family: system-ui, sans-serif;
line-height: 1.5;
display: flex;
flex-direction: column;
}
a {
text-decoration: none;
color: var(--color-primary);
&:visited { color: purple; }
&:hover { color: inherit; }
&.light {
color: var(--color-muted);
&:hover { color: var(--color-primary); }
}
}
input[type="checkbox"] {
appearance: none;
width: 0.9em;
height: 0.9em;
border: 1.5px solid #999;
border-radius: 2px;
cursor: pointer;
flex-shrink: 0;
position: relative;
background: #fff;
&:checked {
background: var(--color-primary);
border-color: var(--color-primary);
}
&:checked::after {
content: '';
position: absolute;
inset: 0;
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 12 12' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M2 6L5 9.5L10 2.5' stroke='white' stroke-width='2.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/80% no-repeat;
}
&:focus-visible {
outline: 1.5px solid var(--color-primary);
outline-offset: 1px;
}
}
/* Header */
header {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--color-border);
& h1 { font-size: var(--text-heading); }
& p { font-size: var(--text-small); }
}
.header-mail {
margin-left: auto;
font-size: var(--text-xsmall);
color: var(--color-muted);
}
/* Three-column layout */
.layout {
display: flex;
flex: 1;
min-height: 0;
}
/* Filters sidebar */
.filters {
width: 230px;
flex-shrink: 0;
overflow-y: auto;
padding: 0.75rem;
border-right: 1px solid var(--color-border);
display: flex;
flex-direction: column;
font-size: var(--text-small);
}
.filter-toggle { display: none; }
.show-map-toggle {
display: flex;
align-items: center;
gap: 0.35rem;
margin-top: 0.5rem;
cursor: pointer;
}
.sidebar-footer {
font-size: var(--text-xsmall);
color: var(--color-muted);
margin-top: auto;
padding-top: 1rem;
& p + p { margin-top: 0.5rem; }
}
.search input {
width: 100%;
padding: 0.3rem 0.4rem;
font-size: inherit;
}
fieldset {
border: none;
padding: 0;
margin: 1rem 0 0;
& legend {
font-weight: 700;
width: 100%;
display: flex;
justify-content: space-between;
align-items: baseline;
padding: 0;
margin-bottom: 0.5rem;
}
& [data-toggle] {
font-size: var(--text-xsmall);
font-weight: normal;
cursor: pointer;
}
& label {
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.05rem 0;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
& .filter-link {
display: none;
margin-left: auto;
font-size: var(--text-xsmall);
}
&:hover .filter-link { display: inline; }
}
}
/* Results */
.results {
flex: 1;
overflow-y: auto;
padding: 0.5rem;
border-right: 1px solid var(--color-border);
min-width: 0;
}
.count { padding: 0.25rem; }
[data-space] {
padding: 0.75rem 0.5rem;
border-bottom: 1px solid var(--color-border);
& h3 { font-size: var(--text-regular); }
& .meta, & .address {
color: var(--color-muted);
display: flex;
justify-content: space-between;
gap: 0.5rem;
font-size: var(--text-xsmall);
}
& p { margin-top: 0.15rem; }
}
[data-space]:hover,
[data-space][aria-selected="true"] {
background: var(--color-hover);
cursor: pointer;
}
/* Map */
.map-aside {
flex: 1;
min-width: 0;
}
#map {
width: 100%;
height: 100%;
}
.leaflet-container a {
color: var(--color-primary) !important;
}
.popup-address {
color: var(--color-muted);
font-size: var(--text-xsmall);
}
/* Taxonomy listing pages */
.taxonomy-list {
list-style: none;
padding: 1rem;
columns: 3;
& li { padding: 0.2rem 0; }
}
/* Detail pages */
main.detail {
max-width: 700px;
padding: 1rem;
overflow-y: auto;
}
/* Mobile */
@media (max-width: 750px) {
html, body { height: auto; overflow: auto; }
body { display: block; }
header { flex-direction: column; align-items: flex-start; }
.header-mail { margin-left: 0; }
.layout { flex-direction: column; flex: none; }
.filters {
width: auto;
overflow-y: visible;
border-right: none;
border-bottom: 1px solid var(--color-border);
gap: 0.5rem;
& fieldset { max-height: 200px; overflow-y: auto; }
}
.results { border-right: none; overflow-y: visible; }
.map-aside { height: 300px; }
.taxonomy-list { columns: 2; }
.filter-toggle { display: block; text-align: center; }
.filters .filter-groups { display: none; }
.filters .filter-groups.open { display: contents; }
}
@media (max-width: 500px) {
.taxonomy-list { columns: 1; }
}

21
templates/base.html Normal file
View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{% block title %}{{ config.title }}{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta property="og:image" content="/thumb.png">
<link rel="shortcut icon" href="/favicon.svg" />
<link rel="stylesheet" href="/style.css">
{% block meta %}{% endblock %}
</head>
<body>
<header>
<h1><a href="/">ooru.space</a></h1>
{% block subtitle %}{% endblock %}
<span class="header-mail">Add or correct something? Send an <a href="mailto:ooru@samagata.org">e-mail</a></span>
</header>
{% block content %}{% endblock %}
{% block scripts %}<script src="/main.js" defer></script>{% endblock %}
</body>
</html>

16
templates/index.html Normal file
View File

@@ -0,0 +1,16 @@
{% extends "layout.html" %}
{% import "macros.html" as m %}
{% block meta %}<meta name="description" content="{{ config.description }}">{% endblock %}
{% block subtitle %}<p>{{ config.description }}</p>{% endblock %}
{% block sidebar %}
{% set spaces = section.pages %}
{{ m::sidebar(pages=spaces, current_taxonomy="none") }}
{% endblock %}
{% block results %}
{% set spaces = section.pages %}
{{ m::results(pages=spaces) }}
{% endblock %}

16
templates/layout.html Normal file
View File

@@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block content %}
<div class="layout">
<aside class="filters" {% block filters_attrs %}data-filters{% endblock %}>
{% block sidebar %}{% endblock %}
</aside>
<div class="results" id="results">
{% block results %}{% endblock %}
</div>
<aside class="map-aside" data-map>
<div id="map"></div>
</aside>
</div>
{% endblock %}

84
templates/macros.html Normal file
View File

@@ -0,0 +1,84 @@
{% macro filter_group(name, label, values, taxonomy) %}
<fieldset>
<legend>{{ label }} <a class="light" data-toggle="{{ name }}">Unselect all</a></legend>
{% for v in values | sort %}
<label><input type="checkbox" name="{{ name }}" value="{{ v }}" data-filter checked> {{ v }}<a class="filter-link light" href="/{{ taxonomy }}/{{ v | slugify }}/">View</a></label>
{% endfor %}
</fieldset>
{% endmacro %}
{% macro filters(pages, current_taxonomy) %}
{% if current_taxonomy != "categories" %}
{% set_global cat_set = [] %}
{% for page in pages %}
{% for c in page.taxonomies.categories %}
{% if c not in cat_set %}
{% set_global cat_set = cat_set | concat(with=c) %}
{% endif %}
{% endfor %}
{% endfor %}
{{ self::filter_group(name="category", label="Categories", values=cat_set, taxonomy="categories") }}
{% endif %}
{% if current_taxonomy != "states" %}
{% set_global state_set = [] %}
{% for page in pages %}
{% for s in page.taxonomies.states %}
{% if s not in state_set %}
{% set_global state_set = state_set | concat(with=s) %}
{% endif %}
{% endfor %}
{% endfor %}
{{ self::filter_group(name="state", label="States", values=state_set, taxonomy="states") }}
{% endif %}
{% if current_taxonomy != "cities" %}
{% set_global city_set = [] %}
{% for page in pages %}
{% for c in page.taxonomies.cities %}
{% if c not in city_set %}
{% set_global city_set = city_set | concat(with=c) %}
{% endif %}
{% endfor %}
{% endfor %}
{{ self::filter_group(name="city", label="Cities", values=city_set, taxonomy="cities") }}
{% endif %}
{% endmacro %}
{% macro sidebar(pages, current_taxonomy) %}
<a href="#" class="filter-toggle" id="filter-toggle">Show filters</a>
<div class="filter-groups">
<div class="search">
<input type="search" id="search" placeholder="Search spaces..." aria-label="Search spaces">
</div>
<label class="show-map-toggle"><input type="checkbox" id="show-map" checked> Show map</label>
{{ self::filters(pages=pages, current_taxonomy=current_taxonomy) }}
<footer class="sidebar-footer">
<p><a href="/spaces.csv">Download data (CSV)</a><br /><a class="light" href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank" rel="noopener">CC BY-SA 4.0</a> License</p>
<p>Maintained by <a class="light" href="https://samagata.org">Samagata Foundation</a></p>
<p><a href="https://gitea.samagata.org/ooru.space/ooru.space">git source</a></p>
</footer>
</div>
{% endmacro %}
{% macro results(pages) %}
<h3 class="count"><span id="count">{{ pages | length }}</span> space(s)</h3>
{% for page in pages %}
<article data-space
data-state="{{ page.taxonomies.states | first }}"
data-city="{{ page.taxonomies.cities | first }}"
data-categories="{{ page.taxonomies.categories | join(sep=',') }}"
data-lat="{{ page.extra.lat }}"
data-lng="{{ page.extra.lng }}"
data-name="{{ page.title }}"
data-address="{{ page.extra.address | default(value='') }}{% if page.extra.pincode %}, {{ page.extra.pincode }}{% endif %}"
>
<h3><a href="{{ page.permalink }}">{{ page.title }}</a></h3>
<p class="meta">
<span class="meta-categories">{% for cat in page.taxonomies.categories %}<a class="light" href="/categories/{{ cat | slugify }}/">{{ cat }}</a>{% if not loop.last %}, {% endif %}{% endfor %}</span>
</p>
{{ page.content | safe }}
{% if page.extra.address %}<p class="address">{{ page.extra.address }}{% if page.extra.pincode %}, {{ page.extra.pincode }}{% endif %}<span class="meta-location"><a class="light" href="/cities/{{ page.taxonomies.cities | first | slugify }}/">{{ page.taxonomies.cities | first }}</a>, <a class="light" href="/states/{{ page.taxonomies.states | first | slugify }}/">{{ page.taxonomies.states | first }}</a></span></p>{% endif %}
</article>
{% endfor %}
{% endmacro %}

43
templates/page.html Normal file
View File

@@ -0,0 +1,43 @@
{% extends "layout.html" %}
{% block title %}{{ page.title }} / {{ config.title }}{% endblock %}
{% block meta %}<meta name="description" content="{{ page.description }}">{% endblock %}
{% block subtitle %}<p>{{ page.title }}, {{ page.taxonomies.cities | join(sep=", ") }}</p>{% endblock %}
{% block filters_attrs %}{% endblock %}
{% block sidebar %}
<p>View public community spaces across
{% for cat in page.taxonomies.categories %}<a href="/categories/{{ cat | slugify }}/">{{ cat }}</a>{% if not loop.last %}, {% endif %}{% endfor %}
or spaces in <a href="/cities/{{ page.taxonomies.cities | first | slugify }}/">{{ page.taxonomies.cities | first }}</a>, <a href="/states/{{ page.taxonomies.states | first | slugify }}/">{{ page.taxonomies.states | first }}</a>.</p>
<br />
<p><a href="/">&larr; All spaces</a></p>
{% endblock %}
{% block results %}
<article data-space
data-state="{{ page.taxonomies.states | first }}"
data-city="{{ page.taxonomies.cities | first }}"
data-categories="{{ page.taxonomies.categories | join(sep=',') }}"
data-lat="{{ page.extra.lat }}"
data-lng="{{ page.extra.lng }}"
data-name="{{ page.title }}"
data-address="{{ page.extra.address | default(value='') }}{% if page.extra.pincode %}, {{ page.extra.pincode }}{% endif %}"
>
<h3>{{ page.title }}</h3>
<p class="meta">
<span class="meta-categories">{% for cat in page.taxonomies.categories %}<a class="light" href="/categories/{{ cat | slugify }}/">{{ cat }}</a>{% if not loop.last %}, {% endif %}{% endfor %}</span>
</p>
{{ page.content | safe }}
{% if page.extra.address %}<p class="address">{{ page.extra.address }}{% if page.extra.pincode %}, {{ page.extra.pincode }}{% endif %}<span class="meta-location"><a class="light" href="/cities/{{ page.taxonomies.cities | first | slugify }}/">{{ page.taxonomies.cities | first }}</a>, <a class="light" href="/states/{{ page.taxonomies.states | first | slugify }}/">{{ page.taxonomies.states | first }}</a></span></p>{% endif %}
<br />
{% if page.extra.url %}
<p>Visit <a href="{{ page.extra.url }}" rel="noopener">{{ page.extra.url | replace(from="https://", to="") | replace(from="http://", to="") | trim_end_matches(pat="/") }}</a></p>
{% endif %}
</article>
{% endblock %}

16
templates/section.html Normal file
View File

@@ -0,0 +1,16 @@
{% extends "layout.html" %}
{% import "macros.html" as m %}
{% block title %}{{ section.title }} / {{ config.title }}{% endblock %}
{% block meta %}<meta name="description" content="{{ section.description | default(value=config.description) }}">{% endblock %}
{% block subtitle %}<p>{{ section.title }}</p>{% endblock %}
{% block sidebar %}
{{ m::sidebar(pages=section.pages, current_taxonomy="none") }}
{% endblock %}
{% block results %}
{{ m::results(pages=section.pages) }}
{% endblock %}

View File

@@ -0,0 +1,23 @@
{% extends "base.html" %}
{% block title %}{{ taxonomy.name | title }} / {{ config.title }}{% endblock %}
{% block meta %}<meta name="description" content="Browse by {{ taxonomy.name }} - {{ config.description }}">{% endblock %}
{% block subtitle %}
<p>Browse by
{% if taxonomy.name == "categories" %}category
{% elif taxonomy.name == "cities" %}city
{% else %}state{% endif %}
</p>
{% endblock %}
{% block content %}
<main>
<ul class="taxonomy-list">
{% for term in terms %}
<li><a href="{{ term.permalink }}">{{ term.name }}</a> ({{ term.pages | length }})</li>
{% endfor %}
</ul>
</main>
{% endblock %}

View File

@@ -0,0 +1,22 @@
{% extends "layout.html" %}
{% import "macros.html" as m %}
{% block title %}{{ term.name }} / {{ config.title }}{% endblock %}
{% block meta %}<meta name="description" content="{{ term.name }} - {{ config.description }}">{% endblock %}
{% block subtitle %}
{% if taxonomy.name == "categories" %}
<p><strong>{{ term.name }}</strong> spaces</p>
{% else %}
<p>Spaces in <strong>{{ term.name }}</strong></p>
{% endif %}
{% endblock %}
{% block sidebar %}
{{ m::sidebar(pages=term.pages, current_taxonomy=taxonomy.name) }}
{% endblock %}
{% block results %}
{{ m::results(pages=term.pages) }}
{% endblock %}