Init project

This commit is contained in:
2025-06-18 15:26:47 +02:00
commit 1bdc9dacb6
11 changed files with 2428 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules

45
app.js Normal file
View File

@@ -0,0 +1,45 @@
import express from 'express';
import {createClient} from 'db-vendo-client/index.js';
import {profile as dbnavProfile} from 'db-vendo-client/p/dbnav/index.js';
import {mapRouteParsers} from 'db-vendo-client/lib/api-parsers.js';
import {createHafasRestApi as createApi} from 'hafas-rest-api';
const config = {
hostname: process.env.HOSTNAME || 'localhost',
port: process.env.PORT ? parseInt(process.env.PORT) : 3000,
name: 'db-vendo-client',
description: 'db-vendo-client',
homepage: 'https://github.com/public-transport/db-vendo-client',
version: '6',
docsLink: 'https://github.com/public-transport/db-vendo-client',
openapiSpec: true,
logging: true,
aboutPage: true,
enrichStations: true,
etags: 'strong',
csp: 'default-src \'none\'; style-src \'self\' \'unsafe-inline\'; img-src https:',
mapRouteParsers,
};
const start = async () => {
const app = express();
const vendo = createClient(
dbnavProfile,
'traveldrafter',
config,
);
const api = await createApi(vendo, config);
app.use("/api", api);
app.use('/web', express.static('web'));
app.listen(config.port, (err) => {
if (err) {
console.error(err);
}
});
};
start();

2164
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

18
package.json Normal file
View File

@@ -0,0 +1,18 @@
{
"name": "traveldrafter",
"version": "0.0.1",
"main": "index.js",
"scripts": {
"start": "node app.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "clerie",
"license": "AGPL-3.0-or-later",
"description": "",
"type": "module",
"dependencies": {
"db-vendo-client": "^6.8.2",
"express": "^5.1.0",
"hafas-rest-api": "^5.1.3"
}
}

28
web/api.js Normal file
View File

@@ -0,0 +1,28 @@
export function fetchApi(pathcomponents, query) {
query.pretty = true;
let url = '/api/' + pathcomponents.join("/") + "?" + new URLSearchParams(query).toString();
return fetch(url).then(response => {
if (!response.ok) {
throw new Error("Fetching api failed");
}
return response;
}).then(response => response.json());
}
export function fetchLocations(query) {
return fetchApi(["locations"], {
query: query,
addresses: false,
poi: false,
subStop: false,
entrances: false,
linesOfStops: false,
});
}
export function fetchJourneys(from_, to) {
return fetchApi(["journeys"], {
from: from_,
to: to,
});
}

26
web/index.html Normal file
View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<link rel="stylesheet" href="style.css" />
<script type="module" src="traveldrafter.js"></script>
</head>
<body>
<div id="journey-search-from">Select…</div>
<div id="journey-search-to">Select…</div>
<div id="journey-search-submit">Search</div>
<div id="journey-search-result"></div>
<div id="locations-search" class="popup">
<div class="popup-close">&times;</div>
<div id="locations-search-content" class="popup-content">
<div class="container">
<input id="locations-search-query" class="form-control" type="text" />
<div id="locations-search-response"></div>
</div>
</div>
</div>
</body>
</html>

39
web/journeys-search.js Normal file
View File

@@ -0,0 +1,39 @@
import { fetchJourneys } from './api.js';
import { attachLocationsSearch } from './locations-search.js';
let element_from = document.querySelector("#journey-search-from");
let element_to = document.querySelector("#journey-search-to");
let element_submit = document.querySelector("#journey-search-submit");
let element_result = document.querySelector("#journey-search-result");
export function setupJourneysSearch() {
attachLocationsSearch(element_from);
attachLocationsSearch(element_to);
element_submit.addEventListener("click", event => {
element_result.innerText = "Loading…";
fetchJourneys(element_from.dataset.locationId, element_to.dataset.locationId).then(result => {
for (let journey of result.journeys) {
element_result.appendChild(createJourneyElement(journey));
}
});
});
}
function createJourneyElement(journey) {
let el = document.createElement("div");
for (let leg of journey.legs) {
el.appendChild(createJourneyLegElement(leg));
}
return el;
}
function createJourneyLegElement(leg) {
let el = document.createElement("div");
el.innerText = JSON.stringify(leg);
el.innerText = leg?.line?.name + ": " + leg.origin.name + " > " + leg.destination.name;
return el;
}

36
web/locations-search.js Normal file
View File

@@ -0,0 +1,36 @@
import { fetchLocations } from './api.js';
let element_locations_search = document.querySelector("#locations-search");
let element_query = document.querySelector("#locations-search-query");
let element_response = document.querySelector("#locations-search-response");
export function setupLocationsSearch() {
element_query.addEventListener("change", (event) => {
element_response.innerText = "Loading…";
fetchLocations(event.target.value).then(result => {
element_response.innerText = "";
result.forEach(lr => {
let location_element = document.createElement("div");
location_element.innerText = lr.name;
location_element.dataset.locationId = lr.id;
location_element.addEventListener("click", event => {
console.log(event.target.dataset.locationId);
element_locations_search.locationSelectedCallback(event.target.innerText, event.target.dataset.locationId);
element_locations_search.style.display = "none";
});
element_response.appendChild(location_element);
});
});
});
}
export function attachLocationsSearch(search_element) {
search_element.addEventListener("click", event => {
element_locations_search.locationSelectedCallback = (location_name, location_id) => {
search_element.innerText = location_name;
search_element.dataset.locationId = location_id;
};
element_locations_search.style.display = "block";
});
}

7
web/popup.js Normal file
View File

@@ -0,0 +1,7 @@
export function setupPopups() {
document.querySelectorAll(".popup-close").forEach(element => {
element.addEventListener("click", event => {
event.target.parentElement.style.display="none";
});
});
}

54
web/style.css Normal file
View File

@@ -0,0 +1,54 @@
* {
font-family: sans-serif;
}
.container {
margin-right: auto;
margin-left: auto;
max-width: 768px;
}
.popup {
position: fixed;
height: 100%;
width: 100%;
top: 0;
left: 0;
z-index: 10000;
background-color: rgba(0, 0, 0, 0.9);
color: white;
display: none;
}
.popup .popup-close {
margin: 60px;
margin-left: auto;
font-size: 60px;
width: 70px;
}
.popup .popup-content {
position: relative;
margin-top: 20px;
margin-bottom: 20px;
}
.popup input {
color: white;
}
input.form-control {
width: 100%;
background-color: transparent;
border: none;
border-bottom-style: solid;
border-width: 1px;
font-size: 2em;
}

10
web/traveldrafter.js Normal file
View File

@@ -0,0 +1,10 @@
import * as Api from './api.js';
import { setupPopups } from "./popup.js";
import { setupLocationsSearch } from './locations-search.js';
import { setupJourneysSearch } from './journeys-search.js';
window.Api = Api;
setupPopups();
setupLocationsSearch();
setupJourneysSearch();