This commit is contained in:
fenris 2026-03-06 08:37:53 +01:00
commit 2ec442c086
37 changed files with 2439 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.geany
build/

5
source/data/conf.json Normal file
View file

@ -0,0 +1,5 @@
{
"baseurl": "https://api.nextbike.net/api/api.php",
"apikey": "xEsFhYZR5jpO1Ug1"
}

View file

@ -0,0 +1,13 @@
{
"username": "Telefon-Nummer",
"password": "Passwort",
"bikename": "Rad-Nummer",
"login": "Anmelden",
"logout": "Abmelden",
"rent": "Leihe hinzufügen",
"start": "starten",
"open": "Schloss öffnen",
"pause": "unterbrechen",
"finish": "beenden"
}

View file

@ -0,0 +1,13 @@
{
"username": "phone number",
"password": "password",
"bikename": "bike number",
"login": "login",
"logout": "logout",
"rent": "add rental",
"start": "start",
"open": "open lock",
"pause": "break",
"finish": "finish"
}

View file

@ -0,0 +1,13 @@
{
"username": "telefon-nombro",
"password": "pasvorto",
"bikename": "biciklo-nombro",
"login": "ensaluti",
"logout": "elsaluti",
"rent": "aldoni pruntajxon",
"start": "komenci",
"open": "malfermi sxlosilon",
"pause": "halti",
"finish": "finigi"
}

BIN
source/fonts/strogg.ttf Normal file

Binary file not shown.

BIN
source/fonts/strogg.woff Normal file

Binary file not shown.

BIN
source/fonts/strogg.woff2 Normal file

Binary file not shown.

1
source/logic/base.ts Normal file
View file

@ -0,0 +1 @@
type int = number;

View file

@ -0,0 +1,53 @@
namespace mod_control.app
{
/**
*/
export function setup
(
platform : lib_mvc.type_model<mod_platform.app.web.type_state>
) : Promise<void>
{
let context_dom : HTMLElement = platform.state.element_dom;
context_dom.querySelector(".app_login > button").addEventListener
(
"click",
(event) =>
{
const username : string = ((document.querySelector(".app_username > input") as HTMLInputElement)).value;
const password : string = ((document.querySelector(".app_password > input") as HTMLInputElement)).value;
mod_model.app.login(platform.state.model, username, password);
}
);
context_dom.querySelector(".app_logout > button").addEventListener
(
"click",
(event) =>
{
mod_model.app.logout(platform.state.model);
}
);
context_dom.querySelector(".app_rent > button").addEventListener
(
"click",
(event) =>
{
mod_model.app.prepare_rental(platform.state.model);
}
);
return Promise.resolve<void>(undefined);
}
/**
*/
export function implementation_control
(
) : lib_mvc.type_control<mod_platform.app.web.type_state>
{
return {
"setup": (model) => setup(model),
};
}
}

View file

@ -0,0 +1,68 @@
namespace mod_control.rental
{
/**
*/
export function setup
(
platform : lib_mvc.type_model<mod_platform.rental.web.type_state>
) : Promise<void>
{
let context_dom : HTMLElement = platform.state.element_dom;
context_dom.querySelector(".rental_start").addEventListener
(
"click",
(event) =>
{
const bike_name : string = lib_call.convey
(
platform.state.model.state.id,
[
x => lib_string.coin(".rental[rel=\"{{rel}}\"] > .rental_bike > input", {"rel": x}),
x => (document.querySelector(x) as HTMLInputElement),
x => x.value,
]
);
mod_model.rental.start(platform.state.model, bike_name);
}
);
context_dom.querySelector(".rental_open > button").addEventListener
(
"click",
(event) =>
{
mod_model.rental.open(platform.state.model);
}
);
context_dom.querySelector(".rental_pause > button").addEventListener
(
"click",
(event) =>
{
mod_model.rental.pause(platform.state.model);
}
);
context_dom.querySelector(".rental_finish > button").addEventListener
(
"click",
(event) =>
{
mod_model.rental.end(platform.state.model);
}
);
return Promise.resolve<void>(undefined);
}
/**
*/
export function implementation_control
(
) : lib_mvc.type_control<mod_platform.rental.web.type_state>
{
return {
"setup": (model) => setup(model),
};
}
}

View file

@ -0,0 +1,24 @@
namespace lib_call
{
/**
*/
export function convey
(
value : any,
functions : Array<Function>
) : any
{
let result : any = value;
functions.forEach
(
function_ =>
{
result = function_(result);
}
);
return result;
}
}

View file

@ -0,0 +1,32 @@
namespace lib_conf
{
/**
*/
var _data : any;
/**
*/
export async function load
(
path : string
) : Promise<void>
{
_data = await fetch(path, {"method": "GET"}).then(x => x.json())
}
/**
*/
export function get
(
key : string
) : any
{
return _data[key];
}
}

View file

@ -0,0 +1,68 @@
namespace lib_dom
{
/**
* @author fenris
*/
export function clone(
element : Element,
options : {
context ?: Document;
} = {}
) : Node
{
options = Object.assign(
{
"context": document,
},
options
);
return options.context.importNode(element, true);
}
/**
* @author fenris
*/
export function request(
id : string,
options : {
context ?: Document;
arguments ?: (null | Record<string, any>);
} = {}
) : DocumentFragment
{
options = Object.assign(
{
"context": document,
"arguments": null,
},
options
);
const template : (null | Element) = document.querySelector("template#" + id);
if (template === null) {
throw (new Error("template not found: " + id));
}
else {
const fragment : DocumentFragment = (
clone(
template["content"],
{
"context": options.context,
}
) as DocumentFragment
);
if (options.arguments === null) {
// do nothing
}
else {
for (let index : int = 0; index < fragment.children.length; index += 1) {
const element : Element = fragment.children[index];
element.innerHTML = lib_string.coin(element.innerHTML, options.arguments);
}
}
return fragment;
}
}
}

168
source/logic/helpers/loc.ts Normal file
View file

@ -0,0 +1,168 @@
namespace lib_loc
{
/**
*/
var _data : Record<string, Record<string, string>> = {};
/**
*/
var _language_order : Array<string> = [];
/**
*/
var _resolve_path : ((language : string) => string);
/**
*/
async function load
(
language : string
) : Promise<void>
{
if (_data.hasOwnProperty(language))
{
// do nothing
}
else
{
try
{
_data[language] = ((await fetch(_resolve_path(language), {"method": "GET"}).then(x => x.json())) as Record<string, string>);
}
catch (error)
{
console.warn
(
lib_string.coin
(
"could not load localization data for language '{{language}}': {{error}}",
{
"language": language,
"error": String(error),
}
)
);
}
}
// return Promise.resolve<void>(undefined);
}
/**
*/
export function get
(
key : string,
options :
{
language ?: (null | string);
} = {}
) : string
{
options = Object.assign
(
{
"language": null,
},
options
);
const language_list : Array<string> = (
[]
.concat((options.language === null) ? [] : [options.language])
.concat(_language_order)
);
for (const language of language_list)
{
if (_data.hasOwnProperty(language) && _data[language].hasOwnProperty(key))
{
return _data[language][key];
}
else
{
// do nothing
}
}
return ("{" + key + "}");
}
/**
*/
export function translate_item
(
selector : string,
key : string,
options :
{
kind ?: string;
parameters ?: Record<string, any>;
language ?: (null | string);
context ?: (HTMLElement | DocumentFragment);
} = {}
) : void
{
options = Object.assign
(
{
"kind": "textcontent",
"parameters": {},
"language": null,
"context": document,
},
options
);
const dom_element : Element = options.context.querySelector(selector);
const value : string = get(key, {"language": options.language});
switch (options.kind)
{
default:
{
console.warn("unhandeld kind");
break;
}
case "textcontent":
{
dom_element.textContent = value;
break;
}
case "attribute":
{
dom_element.setAttribute(options.parameters["key"], value);
break;
}
}
}
/**
*/
export async function setup
(
language_order : Array<string>,
options :
{
resolve_path ?: ((language : string) => string),
} = {}
) : Promise<void>
{
options = Object.assign
(
{
"resolve_path": (language => lib_string.coin("localization/{{language}}.json", {"language": language})),
},
options
);
_resolve_path = options.resolve_path;
_language_order = language_order;
for await (const language of _language_order)
{
await load(language);
}
}
}

166
source/logic/helpers/mvc.ts Normal file
View file

@ -0,0 +1,166 @@
namespace lib_mvc
{
/**
*/
export type type_event<type_data> = {
type : string;
data : type_data;
};
/**
*/
export type type_model<type_state> =
{
listeners : Array<((event : type_event<any>) => Promise<void>)>;
state : type_state;
};
/**
*/
export type type_view<type_state> =
{
setup : ((model : type_model<type_state>) => Promise<void>);
update : ((model : type_model<type_state>, event : type_event<any>) => Promise<void>);
};
/**
*/
export type type_control<type_state> =
{
setup : ((model : type_model<type_state>) => Promise<void>);
};
/**
*/
export type type_complex<type_state> =
{
model : type_model<type_state>;
views : Array<type_view<type_state>>;
controls : Array<type_control<type_state>>;
};
/**
*/
export function model_make<type_state>
(
state : type_state
) : type_model<type_state>
{
return {
"listeners": [],
"state": state,
};
}
/**
*/
export function model_listen<type_state, type_data>
(
model : type_model<type_state>,
action : ((event : type_event<type_data>) => Promise<void>)
) : void
{
model.listeners.push(action);
}
/**
*/
export async function model_notify<type_state, type_data>
(
model : type_model<type_state>,
event : type_event<type_data>
) : Promise<void>
{
for await (const action of model.listeners)
{
action(event);
}
}
/**
*/
export async function view_setup<type_state>
(
view : type_view<type_state>,
model : type_model<type_state>,
options :
{
blocking ?: boolean;
} = {}
) : Promise<void>
{
options = Object.assign
(
{
"blocking": true,
},
options
);
await view.setup(model);
model_listen
(
model,
(info) =>
{
if (options.blocking)
{
return view.update(model, info);
}
else
{
view.update(model, info);
return Promise.resolve<void>(undefined);
}
}
);
}
/**
*/
export async function control_setup<type_state>
(
control : type_control<type_state>,
model : type_model<type_state>,
options :
{
} = {}
) : Promise<void>
{
options = Object.assign
(
{
},
options
);
await control.setup(model);
}
/**
*/
export async function complex_setup<type_state>
(
complex : type_complex<type_state>
) : Promise<void>
{
for await (const view of complex.views)
{
await view_setup<type_state>(view, complex.model);
}
for await (const control of complex.controls)
{
await control_setup<type_state>(control, complex.model);
}
}
}

View file

@ -0,0 +1,211 @@
namespace lib_nextbike
{
/**
*/
let _baseurl : (null | string) = null;
/**
*/
let _apikey : (null | string) = null;
/**
*/
let _testmode : boolean = false;
/**
*/
export function setup
(
baseurl : string,
apikey : string,
options :
{
testmode ?: boolean;
} = {}
) : void
{
options = Object.assign
(
{
"testmode": false,
},
options
);
_baseurl = baseurl;
_apikey = apikey;
_testmode = options.testmode;
}
/**
*/
async function api_call
(
action : string,
loginkey : (null | string),
input : Record<string, string>
) : Promise<any>
{
if ((_baseurl === null) || (_apikey === null))
{
throw (new Error("API not set up yet; execute 'setup' function"));
}
else
{
if (_testmode)
{
return {
"user": {
"loginkey": "",
},
};
}
else
{
return (
fetch
(
lib_string.coin
(
"{{baseurl}}?{{query}}",
{
"baseurl": _baseurl,
"query": lib_call.convey
(
{
"version": "v2",
"format": "json",
"apikey": _apikey,
"action": action,
"loginkey": loginkey,
},
[
Object.entries,
x => x.filter(([key, value]) => (value !== null)),
x => x.map(([key, value]) => lib_string.coin("{{key}}={{value}}", {"key": key, "value": value})),
x => x.join("&")
]
),
}
),
{
"method": "POST",
"body": JSON.stringify(input),
}
)
.then
(
response => response.json()
)
);
}
}
}
/**
*/
export async function login
(
mobile : string,
pin : string
) : Promise<any>
{
return api_call
(
"login",
null,
{"mobile": mobile, "pin": pin}
);
}
/**
*/
export async function logout
(
loginkey : string
) : Promise<any>
{
return api_call
(
"logout",
loginkey,
{}
);
}
/**
*/
export async function open_lock
(
loginkey : string,
bike : string
) : Promise<any>
{
return api_call
(
"openLock",
loginkey,
{"bike": bike}
);
}
/**
*/
export async function rental_begin
(
loginkey : string,
bike : string
) : Promise<any>
{
return api_call
(
"rent",
loginkey,
{"bike": bike}
);
}
/**
*/
export async function rental_break
(
loginkey : string,
bike : string
) : Promise<any>
{
return api_call
(
"rentalBreak",
loginkey,
{"bike": bike}
);
}
/**
*/
export async function rental_end
(
loginkey : string,
bike : string
) : Promise<any>
{
return api_call
(
"return",
loginkey,
{"bike": bike}
);
}
}

View file

@ -0,0 +1,39 @@
namespace lib_storage
{
/**
*/
export function write
(
key : string,
value : any
) : void
{
window.localStorage.setItem(key, JSON.stringify(value));
}
/**
*/
export function read
(
key : string
) : any
{
return JSON.parse(window.localStorage.getItem(key));
}
/**
*/
export function kill
(
key : string
) : any
{
return window.localStorage.removeItem(key);
}
}

View file

@ -0,0 +1,29 @@
namespace lib_string
{
/**
*/
export function coin
(
template : string,
arguments_ : Record<string, string>
) : string
{
let result : string = template;
Object.entries(arguments_).forEach
(
([key, value]) =>
{
result = result.replace
(
new RegExp("{{" + key + "}}", "g"),
value
);
}
);
return result;
}
}

179
source/logic/main.ts Normal file
View file

@ -0,0 +1,179 @@
/**
*/
async function do_rental
(
rental_model : lib_mvc.type_model<mod_model.rental.type_subject>,
target_dom : HTMLElement
) : Promise<{complex : lib_mvc.type_complex<mod_platform.rental.web.type_state>; element : HTMLElement;}>
{
const platform : lib_mvc.type_model<mod_platform.rental.web.type_state> = lib_mvc.model_make<mod_platform.rental.web.type_state>
(
mod_platform.rental.web.make(rental_model)
);
const rental_complex : lib_mvc.type_complex<mod_platform.rental.web.type_state> =
{
"model": platform,
"views":
[
mod_view.rental.console_.implementation_view(),
mod_view.rental.web.implementation_view(),
],
"controls":
[
mod_control.rental.implementation_control(),
],
};
await mod_platform.rental.web.setup(platform, target_dom);
await lib_mvc.complex_setup<mod_platform.rental.web.type_state>(rental_complex);
return Promise.resolve<{complex : lib_mvc.type_complex<mod_platform.rental.web.type_state>; element : HTMLElement;}>
(
{
"complex": rental_complex,
"element": platform.state.element_dom,
}
);
}
/**
*/
async function do_app
(
app_model : lib_mvc.type_model<mod_model.app.type_subject>,
target_dom : HTMLElement
) : Promise<{complex : lib_mvc.type_complex<mod_platform.app.web.type_state>; element : HTMLElement;}>
{
const platform : lib_mvc.type_model<mod_platform.app.web.type_state> = lib_mvc.model_make<mod_platform.app.web.type_state>
(
mod_platform.app.web.make(app_model)
);
const app_complex : lib_mvc.type_complex<mod_platform.app.web.type_state> =
{
"model": platform,
"views":
[
mod_view.app.console_.implementation_view(),
mod_view.app.web.implementation_view(),
],
"controls":
[
mod_control.app.implementation_control(),
],
};
await mod_platform.app.web.setup(platform, target_dom);
await lib_mvc.complex_setup<mod_platform.app.web.type_state>(app_complex);
return Promise.resolve<{complex : lib_mvc.type_complex<mod_platform.app.web.type_state>; element : HTMLElement;}>
(
{
"complex": app_complex,
"element": platform.state.element_dom,
}
);
}
/**
*/
async function main
(
) : Promise<void>
{
// consts
const styles : Record<string, mod_view.app.web.type_style> =
{
"default": {"sheet_name": "style/default.css", "force_language": null},
"strogg": {"sheet_name": "style/strogg.css", "force_language": "en"},
};
// args
const url : URL = new URL(window.location.href);
let parameters : Record<string, string> = {};
url.searchParams.forEach((value, key) => {parameters[key] = value;});
const style_name : string = (
(! ("style" in parameters))
? "default"
: ((! (parameters["style"] in styles)) ? "default" : parameters["style"])
);
const language : (null | string) = (parameters["language"] ?? null);
// vars
const style : mod_view.app.web.type_style = styles[style_name];
// load conf
await lib_conf.load("conf.json");
// setup localization
await lib_loc.setup
(
(
[
style.force_language,
language,
navigator.language,
]
.filter(x => (x !== null))
),
{
"resolve_path": (language => lib_string.coin("localization/{{language}}.json", {"language": language})),
}
);
// setup nextbike api
lib_nextbike.setup(lib_conf.get("baseurl"), lib_conf.get("apikey"), {"testmode": true});
// place stylesheet
{
let link_dom : HTMLElement = document.createElement("link");
link_dom.setAttribute("type", "text/css");
link_dom.setAttribute("rel", "stylesheet");
link_dom.setAttribute("href", style.sheet_name);
document.querySelector("head").appendChild(link_dom);
}
// exec
const loginkey : (null | string) = lib_storage.read("nextbike_loginkey");
let app_model : lib_mvc.type_model<mod_model.app.type_subject> = lib_mvc.model_make<mod_model.app.type_subject>(mod_model.app.inital(loginkey));
const app_deed = await do_app(app_model, document.querySelector("body"));
// sub:rentals
{
lib_mvc.model_listen<mod_model.app.type_subject, any>
(
app_model,
async (event) =>
{
switch (event.type)
{
default:
{
// do nothing
break;
}
case "new_rental":
{
let rental_model : lib_mvc.type_model<mod_model.rental.type_subject> = event.data["rental_model"];
let rentals_dom : HTMLElement = app_deed.element.querySelector(".app_rentals");
const rental_deed = await do_rental(rental_model, rentals_dom);
return Promise.resolve<void>(undefined);
break;
}
case "end_rental":
{
let rentals_dom : HTMLElement = app_deed.element.querySelector(".app_rentals");
let rental_dom : HTMLElement = document.querySelector
(
lib_string.coin(".rental[rel=\"{{rel}}\"]", {"rel": event.data["id"]})
);
rentals_dom.removeChild(rental_dom);
return Promise.resolve<void>(undefined);
break;
}
}
}
);
}
}

231
source/logic/model/app.ts Normal file
View file

@ -0,0 +1,231 @@
namespace mod_model.app
{
/**
*/
export enum enum_condition
{
logged_out,
logged_in,
}
/**
*/
export type type_subject =
{
condition : (null | enum_condition);
loginkey : (null | string);
rental_counter : int;
rentals :
{
offer : Record<mod_model.rental.type_id, lib_mvc.type_model<mod_model.rental.type_subject>>;
order : Array<mod_model.rental.type_id>;
};
};
/**
*/
export function inital
(
loginkey : (null | string)
) : type_subject
{
return {
"condition": ((loginkey === null) ? enum_condition.logged_out : enum_condition.logged_in),
"loginkey": loginkey,
"rental_counter": 0,
"rentals": {"offer": {}, "order": []},
};
}
/**
*/
export async function login
(
model : lib_mvc.type_model<type_subject>,
username : string,
password : string
) : Promise<void>
{
const result : any = await lib_nextbike.login(username, password);
model.state.loginkey = result["user"]["loginkey"];
model.state.condition = enum_condition.logged_in;
lib_storage.write("nextbike_loginkey", model.state.loginkey);
lib_mvc.model_notify<type_subject, {}>
(
model,
{
"type": "login",
"data": {}
}
);
}
/**
* @todo end rentals?
*/
export async function logout
(
model : lib_mvc.type_model<type_subject>
) : Promise<void>
{
const result : any = await lib_nextbike.logout(model.state.loginkey);
lib_storage.kill("nextbike_loginkey");
model.state.condition = enum_condition.logged_out;
model.state.rentals = {"offer": {}, "order": []};
lib_mvc.model_notify<type_subject, {}>
(
model,
{
"type": "logout",
"data": {}
}
);
}
/**
*/
export async function prepare_rental
(
model : lib_mvc.type_model<type_subject>
) : Promise<void>
{
model.state.rental_counter += 1;
const rental_id : mod_model.rental.type_id = model.state.rental_counter.toFixed(0);
const rental_subject : mod_model.rental.type_subject = mod_model.rental.initial(rental_id);
const rental_model : lib_mvc.type_model<mod_model.rental.type_subject> = lib_mvc.model_make(rental_subject);
lib_mvc.model_listen<mod_model.rental.type_subject, any>
(
rental_model,
async (event) =>
{
switch (event.type)
{
default:
{
// do nothing
break;
}
case "start":
{
await start_rental(model, rental_subject.bike_name);
break;
}
case "pause":
{
await pause_rental(model, rental_subject.bike_name);
break;
}
case "open":
{
await open_lock(model, rental_subject.bike_name);
break;
}
case "end":
{
await end_rental(model, rental_subject.id);
break;
}
}
}
);
model.state.rentals.offer[rental_id] = lib_mvc.model_make<mod_model.rental.type_subject>(rental_subject);
model.state.rentals.order.push(rental_id);
lib_mvc.model_notify<type_subject, {rental_model : lib_mvc.type_model<mod_model.rental.type_subject>}>
(
model,
{
"type": "new_rental",
"data": {"rental_model": rental_model}
}
);
}
/**
*/
async function start_rental
(
model : lib_mvc.type_model<type_subject>,
bike_name : string
) : Promise<void>
{
const result : any = await lib_nextbike.rental_begin(model.state.loginkey, bike_name);
lib_mvc.model_notify<type_subject, {}>
(
model,
{
"type": "start_rental",
"data": {}
}
);
}
/**
*/
async function pause_rental
(
model : lib_mvc.type_model<type_subject>,
bike_name : string
) : Promise<void>
{
const result : any = await lib_nextbike.rental_break(model.state.loginkey, bike_name);
lib_mvc.model_notify<type_subject, {}>
(
model,
{
"type": "pause_rental",
"data": {}
}
);
}
/**
*/
async function open_lock
(
model : lib_mvc.type_model<type_subject>,
bike_name : string
) : Promise<void>
{
const result : any = await lib_nextbike.open_lock(model.state.loginkey, bike_name);
lib_mvc.model_notify<type_subject, {}>
(
model,
{
"type": "open_lock",
"data": {}
}
);
}
/**
*/
function end_rental
(
model : lib_mvc.type_model<type_subject>,
rental_id : mod_model.rental.type_id
) : Promise<void>
{
lib_mvc.model_notify<type_subject, {}>
(
model,
{
"type": "end_rental",
"data": {"id": rental_id}
}
);
model.state.rentals.order = model.state.rentals.order.filter(x => (x !== rental_id));
delete model.state.rentals.offer[rental_id];
return Promise.resolve<void>(undefined);
}
}

View file

@ -0,0 +1,120 @@
namespace mod_model.rental
{
/**
*/
export type type_id = string;
/**
*/
export enum enum_condition
{
prior,
running,
paused,
}
/**
*/
export type type_subject =
{
id : type_id;
condition : enum_condition;
bike_name : (null | string);
};
/**
*/
export function initial
(
id : type_id
) : type_subject
{
return {
"id": id,
"condition": enum_condition.prior,
"bike_name": null,
};
}
/**
*/
export async function start
(
model : lib_mvc.type_model<type_subject>,
bike_name : string
) : Promise<void>
{
model.state.condition = enum_condition.running;
model.state.bike_name = bike_name;
await lib_mvc.model_notify<type_subject, {id : type_id}>
(
model,
{
"type": "start",
"data": {"id": model.state.id}
}
);
}
/**
*/
export async function pause
(
model : lib_mvc.type_model<type_subject>
) : Promise<void>
{
model.state.condition = enum_condition.paused;
await lib_mvc.model_notify<type_subject, {id : type_id}>
(
model,
{
"type": "pause",
"data": {"id": model.state.id}
}
);
}
/**
*/
export async function open
(
model : lib_mvc.type_model<type_subject>
) : Promise<void>
{
model.state.condition = enum_condition.running;
await lib_mvc.model_notify<type_subject, {id : type_id}>
(
model,
{
"type": "open",
"data": {"id": model.state.id}
}
);
}
/**
*/
export async function end
(
model : lib_mvc.type_model<type_subject>
) : Promise<void>
{
await lib_mvc.model_notify<type_subject, {id : type_id}>
(
model,
{
"type": "end",
"data": {"id": model.state.id}
}
);
}
}

View file

@ -0,0 +1,66 @@
namespace mod_platform.app.web
{
/**
*/
export type type_state =
{
model : lib_mvc.type_model<mod_model.app.type_subject>;
element_dom : (null | HTMLElement);
}
/**
*/
export function make
(
model : lib_mvc.type_model<mod_model.app.type_subject>
) : type_state
{
return {
"model": model,
"element_dom": null,
};
}
/**
*/
export function setup
(
platform : lib_mvc.type_model<type_state>,
target_dom : HTMLElement
) : Promise<void>
{
let fragment : DocumentFragment = lib_dom.request("app");
let app_dom : HTMLElement = fragment.querySelector(".app");
target_dom.appendChild(fragment);
app_dom.classList.add("empty");
platform.state.element_dom = app_dom;
// propagate events from the model
lib_mvc.model_listen<mod_model.app.type_subject, any>
(
platform.state.model,
(event) =>
{
return lib_mvc.model_notify<type_state, any>(platform, event);
}
);
return Promise.resolve<void>(undefined);
}
// imitate interface of model
// export function login(platform : lib_mvc.type_model<type_state>, username : string, password : string) : Promise<void> {return mod_model.app.login(platform.state.model, username, password);}
// export function logout(platform : lib_mvc.type_model<type_state>) : Promise<void> {return mod_model.app.logout(platform.state.model);}
// export function prepare_rental(platform : lib_mvc.type_model<type_state>) : Promise<void> {return mod_model.app.prepare_rental(platform.state.model);}
// function start_rental(platform : lib_mvc.type_model<type_state>, bike_name : string) : Promise<void> {return mod_model.app.start_rental(platform.model, bike_name);}
// function pause_rental(platform : lib_mvc.type_model<type_state>, bike_name : string) : Promise<void> {return mod_model.app.pause_rental(platform.model, bike_name);}
// function open_lock(platform : lib_mvc.type_model<type_state>, bike_name : string) : Promise<void> {return mod_model.app.open_lock(platform.model, bike_name);}
// function end_rental(platform : lib_mvc.type_model<type_state>, rental_id : mod_model.rental.type_id) : Promise<void> {return mod_model.app.end_rental(platform.model, rental_id);}
}

View file

@ -0,0 +1,54 @@
namespace mod_platform.rental.web
{
/**
*/
export type type_state =
{
model : lib_mvc.type_model<mod_model.rental.type_subject>;
element_dom : (null | HTMLElement);
}
/**
*/
export function make
(
model : lib_mvc.type_model<mod_model.rental.type_subject>
) : type_state
{
return {
"model": model,
"element_dom": null,
};
}
/**
*/
export function setup
(
platform : lib_mvc.type_model<type_state>,
target_dom : HTMLElement
) : Promise<void>
{
let fragment : DocumentFragment = lib_dom.request("rental");
let rental_dom : HTMLElement = fragment.querySelector(".rental");
target_dom.appendChild(fragment);
platform.state.element_dom = rental_dom;
// propagate events from the model
lib_mvc.model_listen<mod_model.rental.type_subject, any>
(
platform.state.model,
(event) =>
{
return lib_mvc.model_notify<type_state, any>(platform, event);
}
);
return Promise.resolve<void>(undefined);
}
}

View file

@ -0,0 +1,40 @@
namespace mod_view.app.console_
{
/**
*/
export function setup
(
platform : lib_mvc.type_model<mod_platform.app.web.type_state>,
) : Promise<void>
{
return Promise.resolve<void>(undefined);
}
/**
*/
function update
(
platform : lib_mvc.type_model<mod_platform.app.web.type_state>,
event : lib_mvc.type_event<any>
) : Promise<void>
{
console.info("app", event.type, platform.state.model, event.data);
return Promise.resolve<void>(undefined);
}
/**
*/
export function implementation_view
(
) : lib_mvc.type_view<mod_platform.app.web.type_state>
{
return {
"setup": (model) => setup(model),
"update": (model, event) => update(model, event),
};
}
}

View file

@ -0,0 +1,40 @@
namespace mod_view.rental.console_
{
/**
*/
export function setup
(
platform : lib_mvc.type_model<mod_platform.rental.web.type_state>
) : Promise<void>
{
return Promise.resolve<void>(undefined);
}
/**
*/
function update
(
platform : lib_mvc.type_model<mod_platform.rental.web.type_state>,
event : lib_mvc.type_event<any>
) : Promise<void>
{
console.info("rental", event.type, platform.state.model, event.data);
return Promise.resolve<void>(undefined);
}
/**
*/
export function implementation_view
(
) : lib_mvc.type_view<mod_platform.rental.web.type_state>
{
return {
"setup": (model) => setup(model),
"update": (model, event) => update(model, event),
};
}
}

View file

@ -0,0 +1,123 @@
namespace mod_view.app.web
{
/**
*/
export type type_style =
{
sheet_name : string;
force_language : (null | string);
};
/**
*/
export type type_state =
{
context_dom : (null | HTMLElement);
};
/**
*/
function set_condition
(
state : type_state,
condition : mod_model.app.enum_condition
) : void
{
state.context_dom.classList.toggle("empty", false);
state.context_dom.classList.toggle("logged_in", (condition === mod_model.app.enum_condition.logged_in));
state.context_dom.classList.toggle("logged_out", (condition === mod_model.app.enum_condition.logged_out));
}
/**
*/
export function make
(
) : type_state
{
return {
"context_dom": null,
};
}
/**
*/
export function setup
(
state : type_state,
platform : lib_mvc.type_model<mod_platform.app.web.type_state>,
) : Promise<void>
{
let context_dom : HTMLElement = platform.state.element_dom;
state.context_dom = context_dom;
// translate stuff
{
lib_loc.translate_item(".app_username > input", "username", {"context": context_dom, "kind": "attribute", "parameters": {"key": "placeholder"}});
lib_loc.translate_item(".app_password > input", "password", {"context": context_dom, "kind": "attribute", "parameters": {"key": "placeholder"}});
lib_loc.translate_item(".app_login > button", "login", {"context": context_dom});
lib_loc.translate_item(".app_logout > button", "logout", {"context": context_dom});
lib_loc.translate_item(".app_rent > button", "rent", {"context": context_dom});
}
// list rentals
{
/*
let rentals_dom : HTMLElement = document.querySelector("#rentals");
rentals_dom.innerHTML = "";
model.rentals.order.forEach
(
(rental_id) =>
{
}
);
*/
}
set_condition(state, platform.state.model.state.condition);
return Promise.resolve<void>(undefined);
}
/**
*/
function update
(
state : type_state,
platform : lib_mvc.type_model<mod_platform.app.web.type_state>,
event : lib_mvc.type_event<any>
) : Promise<void>
{
switch (event.type)
{
default:
{
// do nothing
break;
}
case "login":
case "logout":
{
set_condition(state, platform.state.model.state.condition);
break;
}
}
return Promise.resolve<void>(undefined);
}
/**
*/
export function implementation_view
(
) : lib_mvc.type_view<mod_platform.app.web.type_state>
{
let state : type_state = make();
return {
"setup": (model) => setup(state, model),
"update": (model, event) => update(state, model, event),
};
}
}

View file

@ -0,0 +1,151 @@
namespace mod_view.rental.web
{
/**
*/
export function setup
(
platform : lib_mvc.type_model<mod_platform.rental.web.type_state>
) : Promise<void>
{
let context_dom : HTMLElement = platform.state.element_dom;
// container
{
context_dom.setAttribute("rel", platform.state.model.state.id);
context_dom.classList.add
(
(
(rental_state) =>
{
switch (rental_state)
{
case mod_model.rental.enum_condition.prior: return "prior";
case mod_model.rental.enum_condition.running: return "running";
case mod_model.rental.enum_condition.paused: return "paused";
}
}
) (platform.state.model.state.condition),
);
}
// bike
{
let bike_dom : HTMLInputElement = (context_dom.querySelector(".rental_bike > input") as HTMLInputElement)
lib_loc.translate_item
(
".rental_bike > input",
"bikename",
{
"context": context_dom,
"kind": "attribute",
"parameters": {"key": "placeholder"},
}
);
bike_dom.value = (platform.state.model.state.bike_name ?? "");
const disabled : boolean = (
(
(rental_state) =>
{
switch (rental_state)
{
case mod_model.rental.enum_condition.prior: return false;
case mod_model.rental.enum_condition.running: return true;
case mod_model.rental.enum_condition.paused: return true;
}
}
) (platform.state.model.state.condition)
);
if (! disabled)
{
// do nothing
}
else
{
bike_dom.setAttribute("disabled", "disabled");
}
}
// start
{
lib_loc.translate_item(".rental_start > button", "start", {"context": context_dom});
}
// open
{
lib_loc.translate_item(".rental_open > button", "open", {"context": context_dom});
}
// pause
{
lib_loc.translate_item(".rental_pause > button", "pause", {"context": context_dom});
}
// finish
{
lib_loc.translate_item(".rental_finish > button", "finish", {"context": context_dom});
}
return Promise.resolve<void>(undefined);
}
/**
*/
function update
(
platform : lib_mvc.type_model<mod_platform.rental.web.type_state>,
event : lib_mvc.type_event<any>
) : Promise<void>
{
switch (event.type)
{
default:
{
// do nothing
break;
}
case "start":
{
let container_dom : HTMLElement = document.querySelector
(
lib_string.coin(".rental[rel=\"{{rel}}\"]", {"rel": event.data["id"]})
);
container_dom.classList.remove("prior");
container_dom.classList.remove("paused");
container_dom.classList.add("running");
break;
}
case "pause":
{
let container_dom : HTMLElement = document.querySelector
(
lib_string.coin(".rental[rel=\"{{rel}}\"]", {"rel": event.data["id"]})
);
container_dom.classList.remove("prior");
container_dom.classList.remove("running");
container_dom.classList.add("paused");
break;
}
case "open":
{
let container_dom : HTMLElement = document.querySelector
(
lib_string.coin(".rental[rel=\"{{rel}}\"]", {"rel": event.data["id"]})
);
container_dom.classList.remove("prior");
container_dom.classList.remove("paused");
container_dom.classList.add("running");
break;
}
}
return Promise.resolve<void>(undefined);
}
/**
*/
export function implementation_view
(
) : lib_mvc.type_view<mod_platform.rental.web.type_state>
{
return {
"setup": (model) => setup(model),
"update": (model, event) => update(model, event),
};
}
}

74
source/media/logo.svg Normal file
View file

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg:svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
height="56.408779"
width="100.46931"
sodipodi:docname="Nextbike_Logo.svg"
id="svg14"
enable-background="new 0 0 392.2 56.5"
viewBox="0 0 100.4693 56.408779"
version="1.1">
<svg:metadata
id="metadata20">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</svg:metadata>
<svg:defs
id="defs18" />
<sodipodi:namedview
inkscape:current-layer="svg14"
inkscape:window-maximized="1"
inkscape:window-y="0"
inkscape:window-x="0"
inkscape:cy="9.7905717"
inkscape:cx="58.217152"
inkscape:zoom="5.8898519"
fit-margin-bottom="0"
fit-margin-right="0"
fit-margin-left="0"
fit-margin-top="0"
showgrid="false"
id="namedview16"
inkscape:window-height="1056"
inkscape:window-width="1920"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff" />
<svg:g
transform="translate(-291.73069,-0.09122245)"
id="g12"
fill="#0154a6">
<svg:path
id="path10"
d="m 301.2,15.8 c 0.7,1.7 -1.1,1.4 0.1,4.1 0.8,1.7 5.1,9 19.6,11.8 1.4,0.3 9.2,1.2 14.8,-2 1.9,-1.1 -2.6,-1 -4.9,-9.6 C 330,17 329.8,12.4 324.9,8.2 319.8,3.8 311.8,1.8 303.7,2 c -3.9,0.1 -11.3,0.6 -11.9,1.6 -0.7,1.4 4.1,3.2 5.5,4.9 1.1,1.3 -0.4,2 -0.1,3 0.5,2 3.7,3.6 4,4.3 z m 71.1,0.3 c -11.2,0 -19.9,9.1 -19.9,20.3 0,11.1 8.7,20 19.9,20 11.3,0 19.9,-9 19.9,-20 0,-11.2 -8.6,-20.3 -19.9,-20.3 z m 0,34.5 C 365,50.6 359,44.2 359,36.4 c 0,-8 6,-14.5 13.3,-14.5 7.4,0 13.4,6.5 13.4,14.5 0,7.9 -6,14.2 -13.4,14.2 z M 358.7,16.7 c 4.6,-2.4 11.2,-3.9 11.2,-8.6 0,-4 -5.2,-6.8 -7.4,-7.7 -1.7,-0.7 -3.6,-0.2 -4.3,1.4 -0.7,1.4 0.4,3.1 2.3,3.8 2,0.7 2.9,1.7 2.6,2.5 -0.3,0.9 -2.7,1.2 -7.1,3.4 -2.3,1.1 -4.3,2.9 -6.2,5 -2,2.1 -4,5.9 -5.1,8.2 -4.2,8 -12.2,10.4 -13.5,10.6 v 1.2 c 0,7.8 -6,14.2 -13.4,14.2 -7.3,0 -13.3,-6.4 -13.3,-14.2 0,-2.4 0.6,-4.7 1.5,-6.7 -1.4,-1 -4,-3.3 -5.3,-4.7 -1.2,2 -2.9,6.1 -2.9,11.4 0,11.1 8.7,20 19.9,20 10,0 17.8,-6.9 19.6,-16.1 7.3,-3 10.3,-7.4 12.5,-12 1.9,-4.3 4.3,-9.3 8.9,-11.7 z" />
</svg:g>
<link
id="dark-mode-general-link"
rel="stylesheet"
type="text/css" />
<link
id="dark-mode-custom-link"
rel="stylesheet"
type="text/css" />
<style
id="dark-mode-custom-style"
type="text/css" />
</svg:svg>

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html>
<template id="app">
<div class="app">
<div class="formfield app_username">
<input type="text" placeholder="Telefon-Nummer"/>
</div>
<div class="formfield app_password">
<input type="password" placeholder="Passwort"/>
</div>
<div class="formfield app_login">
<button></button>
</div>
<ol class="app_rentals">
</ol>
<div class="formfield app_rent">
<button></button>
</div>
<div class="formfield app_logout">
<button></button>
</div>
<div class="app_logo">
<img src="logo.svg"/>
</div>
</div>
</template>
<template id="rental">
<li class="rental">
<div class="formfield rental_bike">
<input type="text" placeholder="Rad-Nummer" pattern="[0-9]{5,6}"/>
</div>
<div class="formfield rental_start">
<button></button>
</div>
<div class="formfield rental_open">
<button></button>
</div>
<div class="formfield rental_pause">
<button></button>
</div>
<div class="formfield rental_finish">
<button></button>
</div>
<hr/>
</li>
</template>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="text/javascript" src="logic.js"></script>
<script type="text/javascript">document.addEventListener("DOMContentLoaded", function (event) {main();})</script>
<link rel="stylesheet" type="text/css" href="style/base.css"/>
<title>ryde</title>
</head>
<body>
</body>
</html>

154
source/style/base.less Normal file
View file

@ -0,0 +1,154 @@
@font-face
{
font-family: 'strogg';
src: url('../fonts/strogg.ttf') format('ttf'), url('../fonts/strogg.woff') format('woff'), url('../fonts/strogg.woff2') format('woff2');
}
html
{
font-size: 2.5em;
padding: 0;
margin: 0;
}
body
{
margin: 8px;
padding: 8px;
}
.formfield
{
margin-bottom: 4px;
& > input,
& > button
{
margin: 8px;
padding: 8px;
width: 93.75%;
min-height: 64px;
font-weight: bold;
font-size: 24px;
}
& > button
{
cursor: pointer;
}
& > input
{
border: none;
}
& > button
{
border: none;
}
& > label
{
display: block;
font-weight: bold;
font-size: 0.75em;
}
}
.app
{
&.empty
{
& > .app_username {display: none;}
& > .app_password {display: none;}
& > .app_bikename {display: none;}
& > .app_login {display: none;}
& > .app_logout {display: none;}
& > .app_rentals {display: none;}
& > .app_rent {display: none;}
& > .app_pause {display: none;}
& > .app_finish {display: none;}
& > .app_open {display: none;}
}
&.logged_out
{
& > .app_username {}
& > .app_password {}
& > .app_bikename {display: none;}
& > .app_login {}
& > .app_logout {display: none;}
& > .app_rentals {display: none;}
& > .app_rent {display: none;}
& > .app_pause {display: none;}
& > .app_finish {display: none;}
& > .app_open {display: none;}
}
&.logged_in
{
& > .app_username {display: none;}
& > .app_password {display: none;}
& > .app_bikename {}
& > .app_login {display: none;}
& > .app_logout {}
& > .app_rentals {}
& > .app_rent {}
& > .app_pause {display: none;}
& > .app_finish {display: none;}
& > .app_open {display: none;}
}
}
.app_logo
{
width: 60%;
margin: 20%;
filter: saturate(0);
& > img
{
width: 100%;
}
}
.app_rentals
{
margin: 0;
padding: 0;
& > li
{
list-style-type: none;
}
}
.rental
{
&.prior
{
& > .rental_bike {}
& > .rental_start {}
& > .rental_pause {display: none;}
& > .rental_finish {display: none;}
& > .rental_open {display: none;}
}
&.running
{
& > .rental_bike {}
& > .rental_start {display: none;}
& > .rental_pause {}
& > .rental_finish {}
& > .rental_open {}
}
&.paused
{
& > .rental_bike {}
& > .rental_start {display: none;}
& > .rental_pause {display: none;}
& > .rental_finish {display: none;}
& > .rental_open {}
}
}

40
source/style/default.less Normal file
View file

@ -0,0 +1,40 @@
@hue: 150;
html
{
background-color: hsv(@hue, 0%, 0%);
color: hsv(@hue, 0%, 100%);
font-family: monospace;
}
body
{
/*
background-color: hsv(@hue, 0%, 25%);
color: hsv(@hue, 0%, 75%);
*/
}
.formfield
{
& > input,
& > button
{
font-family: monospace;
text-transform: uppercase;
}
& > input
{
background-color: hsv(@hue, 0%, 75%);
color: hsv(@hue, 0%, 0%);
}
& > button
{
background-color: hsv(@hue, 75%, 50%);
color: hsv(@hue, 0%, 100%);
}
}

40
source/style/strogg.less Normal file
View file

@ -0,0 +1,40 @@
@hue: 0;
html
{
background-color: hsv(@hue, 0%, 0%);
color: hsv(@hue, 0%, 100%);
font-family: strogg;
}
body
{
/*
background-color: hsv(@hue, 0%, 25%);
color: hsv(@hue, 0%, 75%);
*/
}
.formfield
{
& > input,
& > button
{
font-family: strogg;
text-transform: lowercase;
}
& > input
{
background-color: hsv(@hue, 0%, 75%);
color: hsv(@hue, 0%, 0%);
}
& > button
{
background-color: hsv(@hue, 75%, 50%);
color: hsv(@hue, 0%, 100%);
}
}

4
tools/build Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env sh
make --file=tools/makefile

11
tools/deploy Executable file
View file

@ -0,0 +1,11 @@
#!/usr/bin/env sh
if [ $# -ge 1 ] ; then target=$1 && shift ; else target="pv-mehl:~/websites/folksprak.org/htdocs/ryde" ; fi
rsync \
--recursive \
--update \
--verbose \
build/ \
${target}

146
tools/makefile Normal file
View file

@ -0,0 +1,146 @@
cmd_mkdir := mkdir -p
cmd_log := echo "--"
cmd_tsc := tsc --target es2020 --lib es2020,dom
cmd_copy := cp --recursive --update
cmd_lessc := lessc
all: \
structure \
logic \
style \
conf \
media \
localization \
fonts
.PHONY: all
structure: structure-log structure-exe
.PHONY: structure
structure-exe: \
build/index.html
.PHONY: structure-exe
structure-log:
@ ${cmd_log} "structure …"
.PHONY: structure-log
build/index.html: source/structure/index.html
@ ${cmd_mkdir} build
@ ${cmd_copy} $^ $@
logic: logic-log logic-exe
.PHONY: logic
logic-log:
@ ${cmd_log} "logic …"
.PHONY: logic-log
logic-exe: \
build/logic.js
.PHONY: logic-exe
build/logic.js: \
source/logic/base.ts \
source/logic/helpers/call.ts \
source/logic/helpers/string.ts \
source/logic/helpers/storage.ts \
source/logic/helpers/dom.ts \
source/logic/helpers/mvc.ts \
source/logic/helpers/loc.ts \
source/logic/helpers/conf.ts \
source/logic/helpers/nextbike.ts \
source/logic/model/rental.ts \
source/logic/model/app.ts \
source/logic/platform/web/app.ts \
source/logic/platform/web/rental.ts \
source/logic/view/console/rental.ts \
source/logic/view/console/app.ts \
source/logic/view/web/rental.ts \
source/logic/view/web/app.ts \
source/logic/control/rental.ts \
source/logic/control/app.ts \
source/logic/main.ts
@ ${cmd_mkdir} build
@ ${cmd_tsc} $^ --outFile $@
style: style-log style-exe
.PHONY: style
style-log:
@ ${cmd_log} "style …"
.PHONY: style-log
style-exe: \
build/style/base.css \
build/style/default.css \
build/style/strogg.css
.PHONY: style-exe
build/style/base.css: source/style/base.less
@ ${cmd_mkdir} $(dir $@)
@ ${cmd_lessc} $^ > $@
build/style/default.css: source/style/default.less
@ ${cmd_mkdir} $(dir $@)
@ ${cmd_lessc} $^ > $@
build/style/strogg.css: source/style/strogg.less
@ ${cmd_mkdir} $(dir $@)
@ ${cmd_lessc} $^ > $@
conf: conf-log conf-exe
.PHONY: conf
conf-log:
@ ${cmd_log} "conf …"
.PHONY: conf-log
conf-exe: \
build/conf.json
.PHONY: conf-exe
build/conf.json: source/data/conf.json
@ ${cmd_mkdir} $(dir $@)
@ ${cmd_copy} $^ $@
media: media-log media-exe
.PHONY: media
media-log:
@ ${cmd_log} "media …"
.PHONY: media-log
media-exe: \
build/logo.svg
.PHONY: media-exe
build/logo.svg: source/media/logo.svg
@ ${cmd_mkdir} $(dir $@)
@ ${cmd_copy} $^ $@
localization: localization-log localization-exe
.PHONY: localization
localization-log:
@ ${cmd_log} "localization …"
.PHONY: localization-log
localization-exe:
@ ${cmd_mkdir} build/localization
@ ${cmd_copy} source/data/localization/* build/localization/
.PHONY: localization-exe
fonts: fonts-log fonts-exe
.PHONY: fonts
fonts-log:
@ ${cmd_log} "fonts …"
.PHONY: fonts-log
fonts-exe:
@ ${cmd_mkdir} build/fonts
@ ${cmd_copy} source/fonts/* build/fonts/
.PHONY: fonts-exe

4
tools/run Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env sh
web-server build 8888