initial commit

This commit is contained in:
s3lph 2023-01-04 02:32:00 +01:00
commit 13263c19fd
Signed by: s3lph
GPG key ID: 8AC98A811E5BEFF5
26 changed files with 1312 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
out/

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2023 CCC Basel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

52
README.md Normal file
View file

@ -0,0 +1,52 @@
# chaostreff.ch
Source code and build scripts for [chaostreff.ch](https://chaostreff.ch/)
## Local Development Setup
```shell-session
$ python3 -m virtualenv venv
$ . venv/bin/activate
(venv) $ pip install -r requirements.txt
(venv) $ ./build.py
(venv) $ ./run.py
```
run.py includes a SpaceAPI proxy to work around missing CORS headers.
## Production Deployment
```shell-session
$ python3 -m virtualenv venv
$ . venv/bin/activate
(venv) $ pip install -r requirements.txt
(venv) $ ./build.py
```
Then deploy the contents of `out/` to the webroot.
## License
With exception of the sources listed below, this project is licensed under the [MIT License](LICENSE).
### `static/*/bootstrap*`
* Copyright (c) 2011-2023 The Bootstrap Authors
* MIT License
* https://github.com/twbs/bootstrap
### `static/{css,js}/leaflet*`
* Copyright (c) 2010-2022, Volodymyr Agafonkin
* Copyright (c) 2010-2011, CloudMade
* BSD-2-Clause license
* https://github.com/Leaflet/Leaflet
### `static/img/leaflet-marker-*`
* Copyright (c) 2010-2019, Vladimir Agafonkin
* Copyright (c) 2010-2011, CloudMade
* Copyright (c) 2013-2020, Thomas Pointhuber
* BSD-2-Clause license
* https://github.com/pointhi/leaflet-color-markers

29
build.py Executable file
View file

@ -0,0 +1,29 @@
#!/usr/bin/env python3
import jinja2
import os
import shutil
import yaml
with open('config.yml', 'r') as f:
config = yaml.safe_load(f)
os.makedirs('out', exist_ok = True)
shutil.copytree('static', 'out/static', dirs_exist_ok=True)
for lang in config['languages'].keys():
with open(f'l10n/{lang}.yml', 'r') as l:
l10n = yaml.safe_load(l)
env = jinja2.Environment(loader=jinja2.FileSystemLoader("src"))
env.globals.update(config['jinja_environment'])
env.globals.update(l10n)
env.globals['languages'] = config['languages']
env.globals['lang'] = lang
os.makedirs(os.path.join('out', lang), exist_ok = True)
for fname in os.listdir('src'):
tmpl = env.get_template(fname)
with open(os.path.join('out', lang, fname), 'w') as of:
of.write(tmpl.render())

129
config.yml Normal file
View file

@ -0,0 +1,129 @@
---
languages:
de: Deutsch
fr: Français
it: Italiano
en: English
jinja_environment:
baseurl: /
spaceapi_proxy_endpoint: /spaceapi
hackerspaces:
- name: 'CCC Basel'
address: 'Birsfelderstrasse 6, 4132 Muttenz'
lat: 47.53230
lon: 7.63421
open:
de: 'Dienstags ab 19:30'
fr: 'Mardi à partir de 19:30'
it: 'Martedì da 19:30'
en: 'Every Tuesday from 19:30'
contact:
web: 'https://ccc-basel.ch/'
spaceapi: '/https/spaceapi.kabelsalat.ch/'
- name: 'CCC Zürich'
address: 'Neue Hard 12, 8005 Zürich'
lat: 47.3869804
lon: 8.5201701
open:
de: 'Mittwochs ab 19:00'
fr: 'Mercredi à partir de 19:00'
it: 'Mercoledì da 19:00'
en: 'Every Wednesday from 19:00'
contact:
web: 'https://ccczh.ch/'
spaceapi: '/https/www.ccczh.ch/api/v13/'
- name: 'Chaostreff Bern'
address: 'Kyburgstrasse 13, 3013 Bern'
lat: 46.9563416
lon: 7.4483308
open:
de: 'Dienstags ab 19:00'
fr: 'Mardi à partir de 19:00'
it: 'Martedì da 19:00'
en: 'Every Tuesday from 19:00'
contact:
web: 'https://chaostreffbern.ch/'
spaceapi: '/https/www.chaosbern.ch/spaceapi.json'
- name: 'Coredump'
address: 'Eichwiesstrasse 4, 8645 Jona'
lat: 47.2250881
lon: 8.8338872
open:
de: 'Montags ab 20:00'
fr: 'Lundi à partir de 20:00'
it: 'Lunedì da 20:00'
en: 'Every Monday from 20:00'
contact:
web: 'https://coredump.ch/'
spaceapi: '/https/status.crdmp.ch/'
- name: 'FIXME Lausanne'
address: 'Les Ateliers de Renens, Chemin du Closel 3, 1020 Renens'
lat: 46.532372
lon: 6.591292
open:
de: 'Mittwochs ab 19:00'
fr: 'Mercredi à partir de 19:00'
it: 'Mercoledì da 19:00'
en: 'Every Wednesday from 19:00'
contact:
web: 'https://fixme.ch/'
spaceapi: '/https/fixme.ch/cgi-bin/spaceapi.py'
- name: 'LuXeria'
address: 'Ebikonerstrasse 75, CH-6043 Adligenswil'
lat: 47.073
lon: 8.357
open:
de: 'Mittwochs ab 20:00 <span class="badge bg-primary">Videokonferenz</span>'
fr: 'Mercredi à partir de 20:00 <span class="badge bg-primary">visioconférence</span>'
it: 'Mercoledì da 20:00 <span class="badge bg-primary">videoconferenza</span>'
en: 'Every Wednesday from 20:00 <span class="badge bg-primary">video conference</span>'
contact:
web: 'https://luxeria.ch/'
spaceapi: '/https/luxeria.ch/spaceapi.json'
- name: 'ODENWILUSENZ'
address: 'Hardmorgenweg 21, 8222 Beringen'
lat: 47.6951648
lon: 8.5806972
open:
de: 'Mittwochs ab 19:00'
fr: 'Mercredi à partir de 19:00'
it: 'Mercoledì da 19:00'
en: 'Every Wednesday from 19:00'
contact:
web: '/odenwilusenz.ch/'
- name: 'Post Tenebras Lab'
address: 'Avenue de la Praille 36, 1227 Carouge'
lat: 46.187134981155396
lon: 6.133659482002258
open:
de: 'Dienstags ab 19:00'
fr: 'Mardi à partir de 19:00'
it: 'Martedì da 19:00'
en: 'Every Tuesday from 19:00'
contact:
web: 'https://www.posttenebraslab.ch'
spaceapi: '/https/www.posttenebraslab.ch/status/status.json'
- name: 'Ruum42'
address: 'Andreasstrasse 5, 9000 St. Gallen'
lat: 47.4207758
lon: 9.3555784
open:
de: 'Dienstags ab 18:30'
fr: 'Mardi à partir de 18:30'
it: 'Martedì da 18:30'
en: 'Every Tuesday from 18:30'
contact:
web: 'https://ruum42.ch/'

40
l10n/de.yml Normal file
View file

@ -0,0 +1,40 @@
---
h_pagetitle: "Chaostreffs und Hackerspaces in der Schweiz"
h_spaces: "Chaostreffs &amp; Hackerspaces"
h_contact: "Kontakt"
h_services: "Dienste"
h_language: "Sprache"
legend_marker_header: 'Die Farben der Marker auf der Karte geben Echtzeitinformationen (gemäss <a href="https://spaceapi.io">SpaceAPI</a>) der einzelnen Hackerspaces wieder:'
legend_marker_green: "Aktuell für Besucher geöffnet"
legend_marker_red: "Aktuell für Besucher geschlossen"
legend_marker_blue: "Keine Information vorhanden"
th_name: "Name"
th_address: "Adresse"
th_hours: "Öffnungszeiten"
th_contact: "Kontakt"
contact_web: Web
table_footer_missing: "Euer Chaostreff oder Hackerspace fehlt in dieser Liste? Lasst es uns wissen, und wir fügen euch gerne hinzu."
contact_table_link: "Die Kontaktinformationen der einzelnen Spaces &amp; Treffs sind in der obenstehenden Tabelle verlinkt."
contact_swiss_chaos: >-
Wenn du generell zum schweizer Chaos Kontakt aufnehmen willst, machst du das am besten über die Mailingliste
<a href="https://lists.chaostreff.ch/postorius/lists/swiss-chaos.chaostreff.ch/">swiss-chaos</a>.
services_header: "Wir betreiben für die Chaostreffs in der Schweiz folgende Dienste:"
services_ml: "<strong>Mailinglisten</strong> unter der Domain <tt>chaostreff.ch</tt>"
services_dns: "<strong>DNS-Delegation</strong> für <tt>$kanton.chaostreff.ch</tt>"
services_irc: "<strong>IRC-Server</strong> <tt>irc.chaostreff.ch</tt>"
services_footer: >-
Falls ihr für euren neuen Chaostreff einen dieser Dienste benutzen möchtet,
<a href="#contact">kontaktiert uns</a> am besten einfach.
footer: 'chaostreff.ch wird betrieben vom <a href="https://ccc-basel.ch/">CCC Basel</a>. Diese Seite ist freie Software lizensiert unter der MIT-Lizenz.'
marker_popup_open: Aktuell geöffnet
marker_popup_close: Aktuell geschlossen

39
l10n/en.yml Normal file
View file

@ -0,0 +1,39 @@
---
h_pagetitle: "Chaostreffs and Hackerspaces in Switzerland"
h_spaces: "Chaostreffs &amp; Hackerspaces"
h_contact: "Contact"
h_services: "Services"
h_language: "Language"
legend_marker_header: 'The colors of the markers on the map indicate real-time status information (according to <a href="https://spaceapi.io">SpaceAPI</a>) of each hackerspace:'
legend_marker_green: "Currently open for visitors"
legend_marker_red: "Currently closed for visitors"
legend_marker_blue: "No information available"
th_name: "Name"
th_address: "Address"
th_hours: "Opening Hours"
th_contact: "Contact"
contact_web: Web
table_footer_missing: "Your Chaostreff or hackerspace is missing from this list? Let us know, we'll gladly add you to the list."
contact_table_link: "The contact information of individual hackerspaces can be found in the above table."
contact_swiss_chaos: >-
If you want to get in touch with the swiss chaos in general, your best approach would be the
<a href="https://lists.chaostreff.ch/postorius/lists/swiss-chaos.chaostreff.ch/">swiss-chaos</a> mailing list.
services_header: "We operate the following services for Chaostreffs in Switzerland:"
services_ml: "<strong>Mailing lists</strong> on the domain <tt>chaostreff.ch</tt>"
services_dns: "<strong>DNS delegation</strong> for <tt>$canton.chaostreff.ch</tt>"
services_irc: "The <strong>IRC server</strong> <tt>irc.chaostreff.ch</tt>"
services_footer: >-
If you want to make use of one of these services, please
<a href="#contact">get in touch</a>.
footer: 'chaostreff.ch is operated by <a href="https://ccc-basel.ch/">CCC Basel</a>. This page is free software licensed under the MIT license.'
marker_popup_open: Currently open
marker_popup_close: Currently closed

40
l10n/fr.yml Normal file
View file

@ -0,0 +1,40 @@
---
h_pagetitle: "Chaostreffs et Hackerspaces en Suisse"
h_spaces: "Chaostreffs &amp; Hackerspaces"
h_contact: "Contact"
h_services: "Services"
h_language: "Langue"
legend_marker_header: 'Die Farben der Marker auf der Karte geben Echtzeitinformationen (gemäss <a href="https://spaceapi.io">SpaceAPI</a>) der einzelnen Hackerspaces wieder:'
legend_marker_green: "Ouvert maintenant pour les visiteurs"
legend_marker_red: "Fermé maintenant pour les visiteurs"
legend_marker_blue: "Pas de information"
th_name: "Nom"
th_address: "Adresse"
th_hours: "Heures d'ouverture"
th_contact: "Contact"
contact_web: Web
table_footer_missing: "Euer Chaostreff oder Hackerspace fehlt in dieser Liste? Lasst es uns wissen, und wir fügen euch gerne hinzu."
contact_table_link: "Die Kontaktinformationen der einzelnen Spaces &amp; Treffs sind in der obenstehenden Tabelle verlinkt."
contact_swiss_chaos: >-
Wenn du generell zum schweizer Chaos Kontakt aufnehmen willst, machst du das am besten über die Mailingliste
<a href="https://lists.chaostreff.ch/postorius/lists/swiss-chaos.chaostreff.ch/">swiss-chaos</a>.
services_header: "Wir betreiben für die Chaostreffs in der Schweiz folgende Dienste:"
services_ml: "<strong>Mailinglisten</strong> unter der Domain <tt>chaostreff.ch</tt>"
services_dns: "<strong>DNS-Delegation</strong> für <tt>$kanton.chaostreff.ch</tt>"
services_irc: "<strong>IRC-Server</strong> <tt>irc.chaostreff.ch</tt>"
services_footer: >-
Falls ihr für euren neuen Chaostreff einen dieser Dienste benutzen möchtet,
<a href="#contact">kontaktiert uns</a> am besten einfach.
footer: 'chaostreff.ch wird betrieben vom <a href="https://ccc-basel.ch/">CCC Basel</a>. Diese Seite ist freie Software lizensiert unter der MIT-Lizenz.'
marker_popup_open: Ouvert maintenant
marker_popup_close: Fermé maintenant

41
l10n/it.yml Normal file
View file

@ -0,0 +1,41 @@
---
h_pagetitle: "Chaostreffs e Hackerspaces in Svizzera"
h_spaces: "Chaostreffs &amp; Hackerspaces"
h_contact: "Contatto"
h_services: "Servizi"
h_language: "Lingua"
legend_marker_header: >-
I colori degli spilli nella mappa stanno indicando le condizioni degli hackerspaces in tempo reale
(secondo lo <a href="https://spaceapi.io">SpaceAPI</a>):
legend_marker_green: "Ora aperto per i visitatori"
legend_marker_red: "Ora chiuso per i visitatori"
legend_marker_blue: "Nessuna informazione"
th_name: "Nome"
th_address: "Indirizzo"
th_hours: "Orario"
th_contact: "Contatto"
contact_web: web
table_footer_missing: "Il vostro hackerspace manca nella lista lassù? Contattaci per completare la lista per favore."
contact_table_link: "I contatti degli hackerspaces sono linkato nella tabella lassù."
contact_swiss_chaos: >-
Se vuoi contattare generalmente il caos svizzero, usa la mailing list
<a href="https://lists.chaostreff.ch/postorius/lists/swiss-chaos.chaostreff.ch/">swiss-chaos</a>.
services_header: "Eserciamo i seguiti servizi per gli Chaostreffs in Svizzera:"
services_ml: "<strong>Mailing lists </strong> sotto la domain <tt>chaostreff.ch</tt>"
services_dns: "<strong>Delegazioni di DNS</strong> per <tt>$cantone.chaostreff.ch</tt>"
services_irc: "<strong>Server IRC</strong> <tt>irc.chaostreff.ch</tt>"
services_footer: >-
Se vuoi utilizzare uno die quesi servizi, per favore <a href="#contact">contattaci</a>.
footer: 'chaostreff.ch è esercita del <a href="https://ccc-basel.ch/">CCC Basel</a>. Questa pagina è software libero licenzato per la licenza MIT.'
marker_popup_open: Ora operto
marker_popup_close: Ora chiuso

39
run.py Executable file
View file

@ -0,0 +1,39 @@
#!/usr/bin/env python3
import os
import bottle
import urllib.parse
import requests
@bottle.get('/spaceapi/<schema>/<host>')
@bottle.get('/spaceapi/<schema>/<host>/')
@bottle.get('/spaceapi/<schema>/<host>/<path:path>')
def spaceapi_proxy(schema, host, path=''):
url = urllib.parse.urlunsplit((schema, host, path, '', ''))
import logging
logging.error(url)
r = requests.get(url)
return r.json()
@bottle.get('/static/<path:path>')
def static(path):
return bottle.static_file(path, root='out/static')
@bottle.get('/<lang>')
def langred(lang):
bottle.redirect(f'/{lang}/')
@bottle.get('/<lang>/')
def lang(lang):
return bottle.static_file('index.html', root=os.path.join('out', lang))
@bottle.get('/<lang>/<path:path>')
def langpath(lang, path):
return bottle.static_file(path, root=os.path.join('out', lang))
@bottle.get('/')
def index():
bottle.redirect('/de/')
bottle.debug()
bottle.run()

51
src/chaostreff.js Normal file
View file

@ -0,0 +1,51 @@
window.onload = (ev) => {
let spaces = [
{% for space in hackerspaces %}
{ name: '{{ space.name }}', address: '{{ space.address }}', lat: {{ space.lat }}, lon: {{ space.lon }}, open: '{{ space.open[lang] }}', web: '{{ space.contact.web }}', spaceapi: {% if 'spaceapi' in space %}'{{ space.spaceapi }}'{% else %}null{% endif %} },
{% endfor %}
];
let SPACEAPI_PROXY = '{{ spaceapi_proxy_endpoint }}';
let blueIcon = new L.Icon({
iconUrl: '{{ baseurl }}static/img/leaflet-marker-blue.png',
shadowUrl: '{{ baseurl }}static/img/leaflet-marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});
let map = L.map('leaflet-map').setView([46.925, 8.224], 8);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
minZoom: 6,
maxBounds: L.latLngBounds(L.latLng(45.817936, 5.956135), L.latLng(47.8, 10.49234)),
maxBoundsViscosity: 1.0,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);
let newtable = '';
for (let i = 0; i < spaces.length; ++i) {
let marker = L.marker([spaces[i].lat, spaces[i].lon], {icon: blueIcon})
.bindPopup(`<b>${spaces[i].name}</b><address>${spaces[i].address.replaceAll(',', '<br/>')}</address><a href="${spaces[i].web}">${spaces[i].web}</a>`)
.addTo(map);
if (spaces[i].spaceapi !== undefined) {
fetch(SPACEAPI_PROXY + spaces[i].spaceapi, {
headers: { 'Accept': 'application/json' },
}).then(resp => resp.json()).then(json => {
if (json.state == undefined || json.state.open == undefined) {
return;
}
console.log(json);
if (json.state.open === true) {
marker._icon.src = '{{ baseurl }}static/img/leaflet-marker-green.png';
marker._popup._content += '<br/><b>{{ marker_popup_open }}</b>';
} else if (json.state.open === false) {
marker._icon.src = '{{ baseurl }}static/img/leaflet-marker-red.png';
marker._popup._content += '<br/><b>{{ marker_popup_close }}</b>';
}
});
}
}
};

119
src/index.html Normal file
View file

@ -0,0 +1,119 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="encoding" charset="utf-8" />
<title>{{ h_pagetitle }}</title>
<link rel="stylesheet" href="{{ baseurl }}static/css/bootstrap.min.css" />
<link rel="stylesheet" href="{{ baseurl }}static/css/leaflet.css" />
<link rel="stylesheet" href="{{ baseurl }}static/css/chaostreff.css" />
<link rel="prefetch" href="{{ baseurl }}static/img/leaflet-marker-blue.png" />
<link rel="prefetch" href="{{ baseurl }}static/img/leaflet-marker-green.png" />
<link rel="prefetch" href="{{ baseurl }}static/img/leaflet-marker-red.png" />
<script src="{{ baseurl }}static/js/bootstrap.bundle.min.js"></script>
<script src="{{ baseurl }}static/js/leaflet.js"></script>
<script src="chaostreff.js"></script>
</head>
<body>
<header class="navbar navbar-expand-sm navbar-dark bg-dark">
<nav class="container-fluid">
<a class="navbar-brand" href="#">{{ h_pagetitle }}</a>
<ul class="navbar-nav navbar-expand me-auto">
<li class="nav-item"><a href="#spaces" class="nav-link">{{ h_spaces }}</a></li>
<li class="nav-item"><a href="#contact" class="nav-link">{{ h_contact }}</a></li>
<li class="nav-item me-auto"><a href="#services" class="nav-link">{{ h_services }}</a></li>
<li class="nav-item dropdown ms-auto">
<a class="nav-link dropdown-toggle" href="#" id="navbar-dropdown-language" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-translate" viewBox="0 0 16 16">
<!--
https://icons.getbootstrap.com/icons/translate/
Copyright (c) 2011-2023 The Bootstrap Authors
Licensed under the MIT license
https://github.com/twbs/bootstrap/blob/main/LICENSE
-->
<path d="M4.545 6.714 4.11 8H3l1.862-5h1.284L8 8H6.833l-.435-1.286H4.545zm1.634-.736L5.5 3.956h-.049l-.679 2.022H6.18z"/>
<path d="M0 2a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v3h3a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-3H2a2 2 0 0 1-2-2V2zm2-1a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H2zm7.138 9.995c.193.301.402.583.63.846-.748.575-1.673 1.001-2.768 1.292.178.217.451.635.555.867 1.125-.359 2.08-.844 2.886-1.494.777.665 1.739 1.165 2.93 1.472.133-.254.414-.673.629-.89-1.125-.253-2.057-.694-2.82-1.284.681-.747 1.222-1.651 1.621-2.757H14V8h-3v1.047h.765c-.318.844-.74 1.546-1.272 2.13a6.066 6.066 0 0 1-.415-.492 1.988 1.988 0 0 1-.94.31z"/>
</svg>
{{ h_language }}
</a>
<ul class="dropdown-menu dropdown-menu-dark dropdown-menu-end" aria-labelledby="navbar-dropdown-language">
{% for language, name in languages.items() %}
<li><a rel="self" href="/{{ language }}" class="{% if language == lang %}active{% endif %} dropdown-item" alt="{{ name }}" title="{{ name }}">{{ name }}</a></li>
{% endfor %}
</ul>
</li>
</ul>
</nav>
</header>
<main class="container-fluid">
<section id="map">
<div id="leaflet-map" class=""></div>
<aside class="container-fluid">
<div class="row">
<div class="col-lg-6 col-md-12 col-sm-12">
{{ legend_marker_header }}
</div>
<div class="col-lg-2 col-md-4 col-sm-12">
<img class="leaflet-marker-inline" src="{{ baseurl }}static/img/leaflet-marker-green.png" /> {{ legend_marker_green }}
</div>
<div class="col-lg-2 col-md-4 col-sm-12">
<img class="leaflet-marker-inline" src="{{ baseurl }}static/img/leaflet-marker-red.png" /> {{ legend_marker_red }}
</div>
<div class="col-lg-2 col-md-4 col-sm-12">
<img class="leaflet-marker-inline" src="{{ baseurl }}static/img/leaflet-marker-blue.png" /> {{ legend_marker_blue }}
</div>
</div>
</aside>
</section>
<section id="spaces">
<h2 class="mt-2-lg mb-1-lg">{{ h_spaces }}</h2>
<div class="table-responsive-lg">
<table class="table table-striped table-hover">
<thead class="thead">
<tr>
<th scope="col">{{ th_name }}</th>
<th scope="col">{{ th_address }}</th>
<th scope="col">{{ th_hours }}</th>
<th scope="col">{{ th_contact }}</th>
</tr>
</thead>
<tbody id="hackerspaces-table">
{% for space in hackerspaces %}
<tr>
<td>{{ space.name }}</td>
<td><address>{{ space.address }}</address></td>
<td>{{ space.open[lang] }}</td>
<td><a href="{{ space.contact.web }}">{{ contact_web }}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<p>{{ table_footer_missing }}</p>
</section>
<section id="contact">
<h2 class="mt-2-lg mb-1-lg">{{ h_contact }}</h2>
<p>
{{ contact_table_link}}
</p>
<p>
{{ contact_swiss_chaos }}
</p>
</section>
<section id="services">
<h2 class="mt-2-lg mb-1-lg">{{ h_services }}</h2>
<p>{{ services_header }}</p>
<ul>
<li>{{ services_ml }}</li>
<li>{{ services_dns }}</li>
<li>{{ services_irc }}</li>
</ul>
<p>{{ services_footer }}
</p>
</section>
</main>
<footer class="text-center p-3 bg-light">
<p>{{ footer }}</p>
</footer>
</body>
</html>

12
src/sitemap.xml Normal file
View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{% for language in languages $}
<url>
<loc>{{ baseurl }}{{ language }}/</loc>
<changefreq>daily</changefreq>
<priority>0.5</priority>
</url>
{% endfor %}
</urlset>

7
static/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

10
static/css/chaostreff.css Normal file
View file

@ -0,0 +1,10 @@
img.leaflet-marker-inline {
height: 1em;
}
div#leaflet-map {
height: 500px;
width: 100%;
display: block;
}

656
static/css/leaflet.css Normal file
View file

@ -0,0 +1,656 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg {
max-width: none !important;
max-height: none !important;
}
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
width: auto;
padding: 0;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
svg.leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline-offset: 1px;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.5;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover,
.leaflet-bar a:focus {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
font-size: 13px;
font-size: 1.08333em;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.8);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
line-height: 1.4;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover,
.leaflet-control-attribution a:focus {
text-decoration: underline;
}
.leaflet-attribution-flag {
display: inline !important;
vertical-align: baseline !important;
width: 1em;
height: 0.6669em;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
white-space: nowrap;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px #fff;
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 24px 13px 20px;
line-height: 1.3;
font-size: 13px;
font-size: 1.08333em;
min-height: 1px;
}
.leaflet-popup-content p {
margin: 17px 0;
margin: 1.3em 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-top: -1px;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
pointer-events: auto;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
border: none;
text-align: center;
width: 24px;
height: 24px;
font: 16px/24px Tahoma, Verdana, sans-serif;
color: #757575;
text-decoration: none;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
color: #585858;
}
.leaflet-popup-scrolled {
overflow: auto;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-interactive {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}

View file

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-translate" viewBox="0 0 16 16">
<!--
https://icons.getbootstrap.com/icons/translate/
Copyright (c) 2011-2023 The Bootstrap Authors
Licensed under the MIT license
https://github.com/twbs/bootstrap/blob/main/LICENSE
-->
<path d="M4.545 6.714 4.11 8H3l1.862-5h1.284L8 8H6.833l-.435-1.286H4.545zm1.634-.736L5.5 3.956h-.049l-.679 2.022H6.18z"/>
<path d="M0 2a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v3h3a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-3H2a2 2 0 0 1-2-2V2zm2-1a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H2zm7.138 9.995c.193.301.402.583.63.846-.748.575-1.673 1.001-2.768 1.292.178.217.451.635.555.867 1.125-.359 2.08-.844 2.886-1.494.777.665 1.739 1.165 2.93 1.472.133-.254.414-.673.629-.89-1.125-.253-2.057-.694-2.82-1.284.681-.747 1.222-1.651 1.621-2.757H14V8h-3v1.047h.765c-.318.844-.74 1.546-1.272 2.13a6.066 6.066 0 0 1-.415-.492 1.988 1.988 0 0 1-.94.31z"/>
</svg>

After

Width:  |  Height:  |  Size: 1,004 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 B

7
static/js/bootstrap.bundle.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

6
static/js/leaflet.js Normal file

File diff suppressed because one or more lines are too long

1
static/js/leaflet.js.map Normal file

File diff suppressed because one or more lines are too long