commit 2ec442c086cc8d949842f82ec7e749da2c2ce791 Author: Fenris Wolf Date: Fri Mar 6 08:37:53 2026 +0100 [ini] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5e8147f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.geany +build/ diff --git a/source/data/conf.json b/source/data/conf.json new file mode 100644 index 0000000..87f9904 --- /dev/null +++ b/source/data/conf.json @@ -0,0 +1,5 @@ +{ + "baseurl": "https://api.nextbike.net/api/api.php", + "apikey": "xEsFhYZR5jpO1Ug1" +} + diff --git a/source/data/localization/de.json b/source/data/localization/de.json new file mode 100644 index 0000000..1946021 --- /dev/null +++ b/source/data/localization/de.json @@ -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" +} + diff --git a/source/data/localization/en.json b/source/data/localization/en.json new file mode 100644 index 0000000..cf50ee8 --- /dev/null +++ b/source/data/localization/en.json @@ -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" +} + diff --git a/source/data/localization/eo.json b/source/data/localization/eo.json new file mode 100644 index 0000000..5a70c5b --- /dev/null +++ b/source/data/localization/eo.json @@ -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" +} + diff --git a/source/fonts/strogg.ttf b/source/fonts/strogg.ttf new file mode 100644 index 0000000..c28590b Binary files /dev/null and b/source/fonts/strogg.ttf differ diff --git a/source/fonts/strogg.woff b/source/fonts/strogg.woff new file mode 100644 index 0000000..2c26a40 Binary files /dev/null and b/source/fonts/strogg.woff differ diff --git a/source/fonts/strogg.woff2 b/source/fonts/strogg.woff2 new file mode 100644 index 0000000..a1f3cab Binary files /dev/null and b/source/fonts/strogg.woff2 differ diff --git a/source/logic/base.ts b/source/logic/base.ts new file mode 100644 index 0000000..61b835c --- /dev/null +++ b/source/logic/base.ts @@ -0,0 +1 @@ +type int = number; diff --git a/source/logic/control/app.ts b/source/logic/control/app.ts new file mode 100644 index 0000000..eadbf39 --- /dev/null +++ b/source/logic/control/app.ts @@ -0,0 +1,53 @@ +namespace mod_control.app +{ + + /** + */ + export function setup + ( + platform : lib_mvc.type_model + ) : Promise + { + 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(undefined); + } + + + /** + */ + export function implementation_control + ( + ) : lib_mvc.type_control + { + return { + "setup": (model) => setup(model), + }; + } + +} diff --git a/source/logic/control/rental.ts b/source/logic/control/rental.ts new file mode 100644 index 0000000..5103572 --- /dev/null +++ b/source/logic/control/rental.ts @@ -0,0 +1,68 @@ +namespace mod_control.rental +{ + + /** + */ + export function setup + ( + platform : lib_mvc.type_model + ) : Promise + { + 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(undefined); + } + + + /** + */ + export function implementation_control + ( + ) : lib_mvc.type_control + { + return { + "setup": (model) => setup(model), + }; + } + +} diff --git a/source/logic/helpers/call.ts b/source/logic/helpers/call.ts new file mode 100644 index 0000000..bdf8e88 --- /dev/null +++ b/source/logic/helpers/call.ts @@ -0,0 +1,24 @@ +namespace lib_call +{ + + /** + */ + export function convey + ( + value : any, + functions : Array + ) : any + { + let result : any = value; + functions.forEach + ( + function_ => + { + result = function_(result); + } + ); + return result; + } + +} + diff --git a/source/logic/helpers/conf.ts b/source/logic/helpers/conf.ts new file mode 100644 index 0000000..71764d1 --- /dev/null +++ b/source/logic/helpers/conf.ts @@ -0,0 +1,32 @@ + +namespace lib_conf +{ + + /** + */ + var _data : any; + + + /** + */ + export async function load + ( + path : string + ) : Promise + { + _data = await fetch(path, {"method": "GET"}).then(x => x.json()) + } + + + /** + */ + export function get + ( + key : string + ) : any + { + return _data[key]; + } + +} + diff --git a/source/logic/helpers/dom.ts b/source/logic/helpers/dom.ts new file mode 100644 index 0000000..65a1b23 --- /dev/null +++ b/source/logic/helpers/dom.ts @@ -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); + } = {} + ) : 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; + } + } + +} diff --git a/source/logic/helpers/loc.ts b/source/logic/helpers/loc.ts new file mode 100644 index 0000000..7a06e60 --- /dev/null +++ b/source/logic/helpers/loc.ts @@ -0,0 +1,168 @@ +namespace lib_loc +{ + + /** + */ + var _data : Record> = {}; + + + /** + */ + var _language_order : Array = []; + + + /** + */ + var _resolve_path : ((language : string) => string); + + + /** + */ + async function load + ( + language : string + ) : Promise + { + if (_data.hasOwnProperty(language)) + { + // do nothing + } + else + { + try + { + _data[language] = ((await fetch(_resolve_path(language), {"method": "GET"}).then(x => x.json())) as Record); + } + catch (error) + { + console.warn + ( + lib_string.coin + ( + "could not load localization data for language '{{language}}': {{error}}", + { + "language": language, + "error": String(error), + } + ) + ); + } + } + // return Promise.resolve(undefined); + } + + + /** + */ + export function get + ( + key : string, + options : + { + language ?: (null | string); + } = {} + ) : string + { + options = Object.assign + ( + { + "language": null, + }, + options + ); + const language_list : Array = ( + [] + .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; + 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, + options : + { + resolve_path ?: ((language : string) => string), + } = {} + ) : Promise + { + 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); + } + } + +} + diff --git a/source/logic/helpers/mvc.ts b/source/logic/helpers/mvc.ts new file mode 100644 index 0000000..b3723e3 --- /dev/null +++ b/source/logic/helpers/mvc.ts @@ -0,0 +1,166 @@ +namespace lib_mvc +{ + + /** + */ + export type type_event = { + type : string; + data : type_data; + }; + + + /** + */ + export type type_model = + { + listeners : Array<((event : type_event) => Promise)>; + state : type_state; + }; + + + /** + */ + export type type_view = + { + setup : ((model : type_model) => Promise); + update : ((model : type_model, event : type_event) => Promise); + }; + + + /** + */ + export type type_control = + { + setup : ((model : type_model) => Promise); + }; + + + /** + */ + export type type_complex = + { + model : type_model; + views : Array>; + controls : Array>; + }; + + + /** + */ + export function model_make + ( + state : type_state + ) : type_model + { + return { + "listeners": [], + "state": state, + }; + } + + + /** + */ + export function model_listen + ( + model : type_model, + action : ((event : type_event) => Promise) + ) : void + { + model.listeners.push(action); + } + + + /** + */ + export async function model_notify + ( + model : type_model, + event : type_event + ) : Promise + { + for await (const action of model.listeners) + { + action(event); + } + } + + + /** + */ + export async function view_setup + ( + view : type_view, + model : type_model, + options : + { + blocking ?: boolean; + } = {} + ) : Promise + { + 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(undefined); + } + } + ); + } + + + /** + */ + export async function control_setup + ( + control : type_control, + model : type_model, + options : + { + } = {} + ) : Promise + { + options = Object.assign + ( + { + }, + options + ); + await control.setup(model); + } + + + /** + */ + export async function complex_setup + ( + complex : type_complex + ) : Promise + { + for await (const view of complex.views) + { + await view_setup(view, complex.model); + } + for await (const control of complex.controls) + { + await control_setup(control, complex.model); + } + } + +} diff --git a/source/logic/helpers/nextbike.ts b/source/logic/helpers/nextbike.ts new file mode 100644 index 0000000..4c9f9c7 --- /dev/null +++ b/source/logic/helpers/nextbike.ts @@ -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 + ) : Promise + { + 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 + { + return api_call + ( + "login", + null, + {"mobile": mobile, "pin": pin} + ); + } + + + /** + */ + export async function logout + ( + loginkey : string + ) : Promise + { + return api_call + ( + "logout", + loginkey, + {} + ); + } + + + /** + */ + export async function open_lock + ( + loginkey : string, + bike : string + ) : Promise + { + return api_call + ( + "openLock", + loginkey, + {"bike": bike} + ); + } + + + /** + */ + export async function rental_begin + ( + loginkey : string, + bike : string + ) : Promise + { + return api_call + ( + "rent", + loginkey, + {"bike": bike} + ); + } + + + /** + */ + export async function rental_break + ( + loginkey : string, + bike : string + ) : Promise + { + return api_call + ( + "rentalBreak", + loginkey, + {"bike": bike} + ); + } + + + /** + */ + export async function rental_end + ( + loginkey : string, + bike : string + ) : Promise + { + return api_call + ( + "return", + loginkey, + {"bike": bike} + ); + } + +} + diff --git a/source/logic/helpers/storage.ts b/source/logic/helpers/storage.ts new file mode 100644 index 0000000..526c532 --- /dev/null +++ b/source/logic/helpers/storage.ts @@ -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); + } + +} + diff --git a/source/logic/helpers/string.ts b/source/logic/helpers/string.ts new file mode 100644 index 0000000..f45637c --- /dev/null +++ b/source/logic/helpers/string.ts @@ -0,0 +1,29 @@ + +namespace lib_string +{ + + /** + */ + export function coin + ( + template : string, + arguments_ : Record + ) : string + { + let result : string = template; + Object.entries(arguments_).forEach + ( + ([key, value]) => + { + result = result.replace + ( + new RegExp("{{" + key + "}}", "g"), + value + ); + } + ); + return result; + } + +} + diff --git a/source/logic/main.ts b/source/logic/main.ts new file mode 100644 index 0000000..397f141 --- /dev/null +++ b/source/logic/main.ts @@ -0,0 +1,179 @@ +/** + */ +async function do_rental +( + rental_model : lib_mvc.type_model, + target_dom : HTMLElement +) : Promise<{complex : lib_mvc.type_complex; element : HTMLElement;}> +{ + const platform : lib_mvc.type_model = lib_mvc.model_make + ( + mod_platform.rental.web.make(rental_model) + ); + + const rental_complex : lib_mvc.type_complex = + { + "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(rental_complex); + + return Promise.resolve<{complex : lib_mvc.type_complex; element : HTMLElement;}> + ( + { + "complex": rental_complex, + "element": platform.state.element_dom, + } + ); +} + + +/** + */ +async function do_app +( + app_model : lib_mvc.type_model, + target_dom : HTMLElement +) : Promise<{complex : lib_mvc.type_complex; element : HTMLElement;}> +{ + const platform : lib_mvc.type_model = lib_mvc.model_make + ( + mod_platform.app.web.make(app_model) + ); + const app_complex : lib_mvc.type_complex = + { + "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(app_complex); + + return Promise.resolve<{complex : lib_mvc.type_complex; element : HTMLElement;}> + ( + { + "complex": app_complex, + "element": platform.state.element_dom, + } + ); +} + + +/** + */ +async function main +( +) : Promise +{ + // consts + const styles : Record = + { + "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 = {}; + 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 = lib_mvc.model_make(mod_model.app.inital(loginkey)); + const app_deed = await do_app(app_model, document.querySelector("body")); + + // sub:rentals + { + lib_mvc.model_listen + ( + app_model, + async (event) => + { + switch (event.type) + { + default: + { + // do nothing + break; + } + case "new_rental": + { + let rental_model : lib_mvc.type_model = 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(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(undefined); + break; + } + } + } + ); + } +} diff --git a/source/logic/model/app.ts b/source/logic/model/app.ts new file mode 100644 index 0000000..42666f4 --- /dev/null +++ b/source/logic/model/app.ts @@ -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>; + order : Array; + }; + }; + + + /** + */ + 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, + username : string, + password : string + ) : Promise + { + 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 + ( + model, + { + "type": "login", + "data": {} + } + ); + } + + + /** + * @todo end rentals? + */ + export async function logout + ( + model : lib_mvc.type_model + ) : Promise + { + 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 + ( + model, + { + "type": "logout", + "data": {} + } + ); + } + + + /** + */ + export async function prepare_rental + ( + model : lib_mvc.type_model + ) : Promise + { + 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 = lib_mvc.model_make(rental_subject); + lib_mvc.model_listen + ( + 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(rental_subject); + model.state.rentals.order.push(rental_id); + lib_mvc.model_notify}> + ( + model, + { + "type": "new_rental", + "data": {"rental_model": rental_model} + } + ); + } + + + /** + */ + async function start_rental + ( + model : lib_mvc.type_model, + bike_name : string + ) : Promise + { + const result : any = await lib_nextbike.rental_begin(model.state.loginkey, bike_name); + lib_mvc.model_notify + ( + model, + { + "type": "start_rental", + "data": {} + } + ); + } + + + /** + */ + async function pause_rental + ( + model : lib_mvc.type_model, + bike_name : string + ) : Promise + { + const result : any = await lib_nextbike.rental_break(model.state.loginkey, bike_name); + lib_mvc.model_notify + ( + model, + { + "type": "pause_rental", + "data": {} + } + ); + } + + + /** + */ + async function open_lock + ( + model : lib_mvc.type_model, + bike_name : string + ) : Promise + { + const result : any = await lib_nextbike.open_lock(model.state.loginkey, bike_name); + lib_mvc.model_notify + ( + model, + { + "type": "open_lock", + "data": {} + } + ); + } + + + /** + */ + function end_rental + ( + model : lib_mvc.type_model, + rental_id : mod_model.rental.type_id + ) : Promise + { + lib_mvc.model_notify + ( + 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(undefined); + } + +} diff --git a/source/logic/model/rental.ts b/source/logic/model/rental.ts new file mode 100644 index 0000000..11c2360 --- /dev/null +++ b/source/logic/model/rental.ts @@ -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, + bike_name : string + ) : Promise + { + model.state.condition = enum_condition.running; + model.state.bike_name = bike_name; + await lib_mvc.model_notify + ( + model, + { + "type": "start", + "data": {"id": model.state.id} + } + ); + } + + + /** + */ + export async function pause + ( + model : lib_mvc.type_model + ) : Promise + { + model.state.condition = enum_condition.paused; + await lib_mvc.model_notify + ( + model, + { + "type": "pause", + "data": {"id": model.state.id} + } + ); + } + + + /** + */ + export async function open + ( + model : lib_mvc.type_model + ) : Promise + { + model.state.condition = enum_condition.running; + await lib_mvc.model_notify + ( + model, + { + "type": "open", + "data": {"id": model.state.id} + } + ); + } + + + /** + */ + export async function end + ( + model : lib_mvc.type_model + ) : Promise + { + await lib_mvc.model_notify + ( + model, + { + "type": "end", + "data": {"id": model.state.id} + } + ); + } + +} diff --git a/source/logic/platform/web/app.ts b/source/logic/platform/web/app.ts new file mode 100644 index 0000000..1b06d09 --- /dev/null +++ b/source/logic/platform/web/app.ts @@ -0,0 +1,66 @@ +namespace mod_platform.app.web +{ + + /** + */ + export type type_state = + { + model : lib_mvc.type_model; + element_dom : (null | HTMLElement); + } + + + /** + */ + export function make + ( + model : lib_mvc.type_model + ) : type_state + { + return { + "model": model, + "element_dom": null, + }; + } + + + /** + */ + export function setup + ( + platform : lib_mvc.type_model, + target_dom : HTMLElement + ) : Promise + { + 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 + ( + platform.state.model, + (event) => + { + return lib_mvc.model_notify(platform, event); + } + ); + + return Promise.resolve(undefined); + } + + + // imitate interface of model + // export function login(platform : lib_mvc.type_model, username : string, password : string) : Promise {return mod_model.app.login(platform.state.model, username, password);} + // export function logout(platform : lib_mvc.type_model) : Promise {return mod_model.app.logout(platform.state.model);} + // export function prepare_rental(platform : lib_mvc.type_model) : Promise {return mod_model.app.prepare_rental(platform.state.model);} + // function start_rental(platform : lib_mvc.type_model, bike_name : string) : Promise {return mod_model.app.start_rental(platform.model, bike_name);} + // function pause_rental(platform : lib_mvc.type_model, bike_name : string) : Promise {return mod_model.app.pause_rental(platform.model, bike_name);} + // function open_lock(platform : lib_mvc.type_model, bike_name : string) : Promise {return mod_model.app.open_lock(platform.model, bike_name);} + // function end_rental(platform : lib_mvc.type_model, rental_id : mod_model.rental.type_id) : Promise {return mod_model.app.end_rental(platform.model, rental_id);} + +} diff --git a/source/logic/platform/web/rental.ts b/source/logic/platform/web/rental.ts new file mode 100644 index 0000000..ec7c429 --- /dev/null +++ b/source/logic/platform/web/rental.ts @@ -0,0 +1,54 @@ +namespace mod_platform.rental.web +{ + + /** + */ + export type type_state = + { + model : lib_mvc.type_model; + element_dom : (null | HTMLElement); + } + + + /** + */ + export function make + ( + model : lib_mvc.type_model + ) : type_state + { + return { + "model": model, + "element_dom": null, + }; + } + + + /** + */ + export function setup + ( + platform : lib_mvc.type_model, + target_dom : HTMLElement + ) : Promise + { + 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 + ( + platform.state.model, + (event) => + { + return lib_mvc.model_notify(platform, event); + } + ); + + return Promise.resolve(undefined); + } + +} diff --git a/source/logic/view/console/app.ts b/source/logic/view/console/app.ts new file mode 100644 index 0000000..a3d9283 --- /dev/null +++ b/source/logic/view/console/app.ts @@ -0,0 +1,40 @@ +namespace mod_view.app.console_ +{ + + /** + */ + export function setup + ( + platform : lib_mvc.type_model, + ) : Promise + { + return Promise.resolve(undefined); + } + + + /** + */ + function update + ( + platform : lib_mvc.type_model, + event : lib_mvc.type_event + ) : Promise + { + console.info("app", event.type, platform.state.model, event.data); + return Promise.resolve(undefined); + } + + + /** + */ + export function implementation_view + ( + ) : lib_mvc.type_view + { + return { + "setup": (model) => setup(model), + "update": (model, event) => update(model, event), + }; + } + +} diff --git a/source/logic/view/console/rental.ts b/source/logic/view/console/rental.ts new file mode 100644 index 0000000..15a89d2 --- /dev/null +++ b/source/logic/view/console/rental.ts @@ -0,0 +1,40 @@ +namespace mod_view.rental.console_ +{ + + /** + */ + export function setup + ( + platform : lib_mvc.type_model + ) : Promise + { + return Promise.resolve(undefined); + } + + + /** + */ + function update + ( + platform : lib_mvc.type_model, + event : lib_mvc.type_event + ) : Promise + { + console.info("rental", event.type, platform.state.model, event.data); + return Promise.resolve(undefined); + } + + + /** + */ + export function implementation_view + ( + ) : lib_mvc.type_view + { + return { + "setup": (model) => setup(model), + "update": (model, event) => update(model, event), + }; + } + +} diff --git a/source/logic/view/web/app.ts b/source/logic/view/web/app.ts new file mode 100644 index 0000000..831950b --- /dev/null +++ b/source/logic/view/web/app.ts @@ -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, + ) : Promise + { + 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(undefined); + } + + + /** + */ + function update + ( + state : type_state, + platform : lib_mvc.type_model, + event : lib_mvc.type_event + ) : Promise + { + switch (event.type) + { + default: + { + // do nothing + break; + } + case "login": + case "logout": + { + set_condition(state, platform.state.model.state.condition); + break; + } + } + return Promise.resolve(undefined); + } + + + /** + */ + export function implementation_view + ( + ) : lib_mvc.type_view + { + let state : type_state = make(); + return { + "setup": (model) => setup(state, model), + "update": (model, event) => update(state, model, event), + }; + } + +} diff --git a/source/logic/view/web/rental.ts b/source/logic/view/web/rental.ts new file mode 100644 index 0000000..6b44f57 --- /dev/null +++ b/source/logic/view/web/rental.ts @@ -0,0 +1,151 @@ +namespace mod_view.rental.web +{ + + /** + */ + export function setup + ( + platform : lib_mvc.type_model + ) : Promise + { + 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(undefined); + } + + + /** + */ + function update + ( + platform : lib_mvc.type_model, + event : lib_mvc.type_event + ) : Promise + { + 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(undefined); + } + + + /** + */ + export function implementation_view + ( + ) : lib_mvc.type_view + { + return { + "setup": (model) => setup(model), + "update": (model, event) => update(model, event), + }; + } + +} diff --git a/source/media/logo.svg b/source/media/logo.svg new file mode 100644 index 0000000..77b1564 --- /dev/null +++ b/source/media/logo.svg @@ -0,0 +1,74 @@ + + + + + + image/svg+xml + + + + + + + + + + + + +