This commit is contained in:
roydfalk 2024-04-22 10:02:43 +02:00
commit e73424e2f2
20 changed files with 9930 additions and 0 deletions

27
.editorconfig Normal file
View file

@ -0,0 +1,27 @@
# see https://EditorConfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = tab
indent_style = tab
tab_width = 4
insert_final_newline = true
max_line_length = 80
trim_trailing_whitespace = true
curly_bracket_next_line = false
indent_brace_style = K&R
spaces_around_operators = true
spaces_around_brackets = false
quote_type = double
[*.y{,a}ml{,lint}]
indent_style = space
indent_size = 2
[*.md]
indent_style = space
indent_size = 2

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/.geany
/temp/
/build/

2298
lib/plankton/plankton.d.ts vendored Normal file

File diff suppressed because it is too large Load diff

6303
lib/plankton/plankton.js Normal file

File diff suppressed because it is too large Load diff

0
readme.md Normal file
View file

323
source/logic/backend.ts Normal file
View file

@ -0,0 +1,323 @@
namespace _aum.backend
{
/**
*/
var _session_key_chest : lib_plankton.storage.type_chest<string, string, void, string, string>;
/**
* @todo use backend call
*/
export async function logged_in(
) : Promise<boolean>
{
let session_key : (null | string);
try {
session_key = (await _session_key_chest.read("session_key"));
}
catch (error) {
session_key = null;
}
return Promise.resolve<boolean>(session_key !== null);
}
/**
*/
export function init(
) : Promise<void>
{
_session_key_chest = lib_plankton.storage.localstorage.implementation_chest(
{
"corner": "aum",
}
);
return Promise.resolve<void>(undefined);
}
/**
*/
async function abstract_call<type_input, type_output>(
http_method : string,
path : string,
options : {
data ?: type_input;
} = {}
) : Promise<type_output>
{
options = Object.assign(
{
"data": null,
},
options
);
let session_key : (null | string);
try {
session_key = (await _session_key_chest.read("session_key"));
}
catch (error) {
session_key = null;
}
const without_content : boolean = ["HEAD","OPTIONS","GET"].includes(http_method);
return (
fetch(
lib_plankton.string.coin(
("{{scheme}}://{{host}}:{{port}}{{path_base}}{{path_action}}"),
{
"scheme": _aum.conf.get().backend.scheme,
"host": _aum.conf.get().backend.host,
"port": _aum.conf.get().backend.port.toFixed(0),
"path_base": _aum.conf.get().backend.path_base,
"path_action": path,
}
),
{
"method": http_method,
"headers": Object.assign(
{},
((! without_content) ? {"Content-Type": "application/json"} : {}),
((session_key !== null) ? {"X-Session-Key": session_key} : {})
),
"body": (
without_content
? null
: JSON.stringify(options.data)
),
}
)
.then(
x => {
if ((x.status >= 200) && (x.status < 300)) {
return x.json();
}
else {
return Promise.reject<type_output>(new Error("unexpected response status code"));
}
}
)
);
}
/**
*/
export async function login(
name : string,
password : string
) : Promise<void>
{
const session_key : string = await abstract_call<{name : string; password : string;}, string>(
"POST",
"/session/begin",
{
"data": {
"name": name,
"password": password,
}
}
);
await _session_key_chest.write("session_key", session_key);
}
/**
*/
export async function logout(
) : Promise<void>
{
try {
await abstract_call<void, null>(
"DELETE",
"/session/end"
);
}
catch (error) {
// do nothing
}
await _session_key_chest.delete("session_key");
}
/**
*/
export async function email(
receivers : Array<string>,
subject : string,
content : string
) : Promise<void>
{
return (
abstract_call(
"POST",
"/email",
{
"data": {
"receivers": receivers,
"subject": subject,
"content": content,
},
}
)
);
}
/**
*/
export async function verification_get(
data : any
) : Promise<string>
{
return (
abstract_call(
"POST",
"/verification/get",
{
"data": {
"data": data,
},
}
)
);
}
/**
*/
export async function verification_check(
data : any,
verification : string
) : Promise<boolean>
{
return (
abstract_call(
"POST",
"/verification/check",
{
"data": {
"data": data,
"verification": verification,
}
}
)
);
}
/**
*/
export async function member_list(
) : Promise<Array<any>>
{
return (
abstract_call(
"GET",
"/member"
)
);
}
/**
*/
export async function member_get(
id : int
) : Promise<any>
{
return abstract_call(
"GET",
"/member/" + id.toFixed(0)
);
}
/**
*/
export async function member_add(
data : Record<string, any>
) : Promise<int>
{
return abstract_call(
"POST",
"/member",
{
"data": {
"membership_number": data["membership_number"],
"enabled": data["enabled"],
"name_real_value": data["name_real_value"],
"name_real_extension": data["name_real_extension"],
"name_display": data["name_display"],
"name_login": data["name_login"],
"password_image": null,
"email_address_private_value": data["email_address_private_value"],
"email_address_numberbased_use": data["email_address_numberbased_use"],
"email_address_namebased_use": data["email_address_namebased_use"],
"email_redirect_to_private": data["email_redirect_to_private"],
"salutation": data["salutation"],
}
}
);
}
/**
*/
export async function member_modify(
id : int,
data : Record<string, any>
) : Promise<void>
{
return abstract_call(
"PATCH",
"/member/" + id.toFixed(0),
{
"data": {
"membership_number": data["membership_number"],
"enabled": data["enabled"],
"name_real_value": data["name_real_value"],
"name_real_extension": data["name_real_extension"],
"name_display": data["name_display"],
"name_login": data["name_login"],
"password_image": null,
"email_address_private_value": data["email_address_private_value"],
"email_address_numberbased_use": data["email_address_numberbased_use"],
"email_address_namebased_use": data["email_address_namebased_use"],
"email_redirect_to_private": data["email_redirect_to_private"],
"salutation": data["salutation"],
}
}
);
}
/**
*/
export async function member_register(
id : int,
verification : string,
data : {
name_login : string;
name_display : string;
salutation : string;
email_mode : ("none" | "number" | "number_and_name");
email_redirect : boolean;
password : string;
}
) : Promise<void>
{
return (
abstract_call(
"POST",
"/member/register/" + id.toFixed(0) + "?verification=" + verification,
{
"data": data,
}
)
);
}
}

43
source/logic/conf.ts Normal file
View file

@ -0,0 +1,43 @@
namespace _aum.conf
{
/**
*/
export type type_data = {
backend : {
scheme : string;
host : string;
port : int;
path_base : string;
};
};
/**
*/
var _data : (null | type_data) = null;
/**
*/
export async function load(
) : Promise<void>
{
_data = lib_plankton.json.decode(await lib_plankton.file.read("conf.json"));
}
/**
*/
export function get(
) : type_data
{
if (_data === null) {
throw (new Error("conf not loaded yet"));
}
else {
return _data;
}
}
}

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

@ -0,0 +1,105 @@
/**
*/
function template_request(
id : string
) : DocumentFragment
{
let dom_template = document.querySelector("template#" + id);
// return template["content"].cloneNode(true);
return (document.importNode(dom_template["content"], true) as DocumentFragment)
}
/**
*/
async function update_nav(
options : {
mode ?: string;
} = {}
) : Promise<void>
{
options = Object.assign(
{
"mode": ((await _aum.backend.logged_in()) ? "logged_in" : "logged_out"),
},
options
);
let dom_body = document.querySelector("body");
if (options.mode === null) {
dom_body.removeAttribute("rel");
}
else {
dom_body.setAttribute("rel", options.mode);
}
}
/**
*/
function setup_nav(
) : void
{
const entries : Array<
{
location : lib_plankton.zoo_page.type_location;
label : string;
classes : Array<string>;
}
> = [
{
"location": {"name": "login", "parameters": {}},
"label": "Anmelden",
"classes": ["logged_out"],
},
{
"location": {"name": "logout", "parameters": {}},
"label": "Abmelden",
"classes": ["logged_in"],
},
{
"location": {"name": "create", "parameters": {}},
"label": "Mitglied anlegen",
"classes": ["logged_in"],
},
{
"location": {"name": "members", "parameters": {}},
"label": "Mitglieder-Übersicht",
"classes": ["logged_in"],
},
];
let dom_ul : HTMLElement = document.querySelector("nav > ul");
entries.forEach(
entry => {
let dom_li : HTMLElement = document.createElement("li");
{
let dom_a : HTMLElement = document.createElement("a");
dom_a.textContent = entry.label;
dom_a.setAttribute("href", lib_plankton.zoo_page.encode(entry.location));
entry.classes.forEach(class_ => {dom_a.classList.add(class_);});
dom_li.appendChild(dom_a);
}
dom_ul.appendChild(dom_li);
}
);
update_nav();
}
/**
*/
async function main(
) : Promise<void>
{
await _aum.conf.load();
await _aum.backend.init();
await lib_plankton.zoo_page.init(
document.querySelector("main"),
{
"fallback": {"name": "index", "parameters": {}},
}
);
setup_nav();
lib_plankton.zoo_page.start();
}

View file

@ -0,0 +1,72 @@
lib_plankton.zoo_page.register(
"create",
(parameters, target_element) => {
const form = new lib_plankton.zoo_form.class_form<
{
membership_number : string;
name_real_value : string;
email_address_private_value : (null | string);
},
{
membership_number : string;
name_real_value : string;
email_address_private_value : string;
}
>(
value => ({
"membership_number": value.membership_number,
"name_real_value": value.name_real_value,
"email_address_private_value": (value.email_address_private_value ?? ""),
}),
representation => ({
"membership_number": representation.membership_number,
"name_real_value": representation.name_real_value,
"email_address_private_value": representation.email_address_private_value,
}),
new lib_plankton.zoo_input.class_input_group(
[
{
"name": "membership_number",
"input": new lib_plankton.zoo_input.class_input_text(),
"label": "Mitgliedsnummer",
},
{
"name": "name_real_value",
"input": new lib_plankton.zoo_input.class_input_text(),
"label": "Echter Name",
},
{
"name": "email_address_private_value",
"input": new lib_plankton.zoo_input.class_input_text(),
"label": "Private E-Mail-Adresse",
},
]
),
[
{
"label": "Senden",
"procedure": async (get_value, get_representation) => {
const value = await get_value();
const id = await _aum.backend.member_add(
{
"membership_number": value.membership_number,
"name_real_value": value.name_real_value,
"email_address_private_value": value.email_address_private_value,
"name_real_extension": null,
"name_display": null,
"name_login": null,
"salutation": null,
"password_image": null,
"email_address_numberbased_use": false,
"email_address_namebased_use": false,
"email_redirect_to_private": false,
}
);
lib_plankton.zoo_page.set({"name": "view", "parameters": {"id": id}});
},
}
]
);
form.setup(target_element as HTMLElement);
}
);

View file

@ -0,0 +1,5 @@
lib_plankton.zoo_page.register(
"index",
(parameters, target_element) => {
}
);

View file

@ -0,0 +1,62 @@
lib_plankton.zoo_page.register(
"login",
(parameters, target_element) => {
target_element.appendChild(template_request("login"));
const form = new lib_plankton.zoo_form.class_form<
{
name : string;
password : string;
},
{
name : string;
password : string;
}
>(
x => x,
x => x,
new lib_plankton.zoo_input.class_input_group(
[
{
"name": "name",
"input": new lib_plankton.zoo_input.class_input_text(),
"label": "Name",
},
{
"name": "password",
"input": new lib_plankton.zoo_input.class_input_password(),
"label": "Passwort",
},
]
),
[
{
"label": "Anmelden",
"procedure": async (get_value, get_representation) => {
const value = await get_value();
(
_aum.backend.login(value.name, value.password)
.then(
() => {
lib_plankton.zoo_page.set({"name": "index", "parameters": {}});
update_nav({"mode": "logged_in"});
}
)
.catch(
(error) => {
form.input_write(
{
"name": value.name,
"password": ""
}
);
}
)
);
},
}
]
);
form.setup(target_element.querySelector(".login-form") as HTMLElement);
}
);

View file

@ -0,0 +1,8 @@
lib_plankton.zoo_page.register(
"logout",
async (parameters, target_element) => {
await _aum.backend.logout();
lib_plankton.zoo_page.set({"name": "index", "parameters": {}});
update_nav({"mode": "logged_in"});
}
);

View file

@ -0,0 +1,219 @@
lib_plankton.zoo_page.register(
"members",
(parameters, target_element) => {
const state : (null | any) = (
("state" in parameters)
? lib_plankton.json.decode(lib_plankton.base64.decode(parameters["state"]))
: null
);
const set_state = function (state) {
target_element.querySelector(".members").setAttribute("rel", state);
};
let _id : (null | int) = null;
const editor : lib_plankton.zoo_editor.type_editor<int, any> = lib_plankton.zoo_editor.make<int, any>(
{
"setup": (parameters) => Promise.resolve<void>(undefined),
"search": async (term) => lib_plankton.call.convey(
await _aum.backend.member_list(),
[
// (raw as Array<{id : int; preview : any;}>)
x => x.filter(
(
(term === null)
||
(term === "")
)
? (entry => true)
: (
(term.length < 2)
? (entry => false)
: (
entry => (
entry.preview["membership_number"].includes(term)
||
entry.preview["name_real_value"].toLowerCase().includes(term)
)
)
)
),
x => x.map(
entry => ({
"key": entry.id,
"preview": entry.preview,
})
),
]
),
"read": (key) => _aum.backend.member_get(key),
"create": (value) => /*_aum.backend.member_add(value)*/Promise.reject(new Error("missing")),
"update": (key, value) => _aum.backend.member_modify(key, value),
"delete": (key) => /*_aum.backend.member_remove(key)*/Promise.reject(new Error("missing")),
},
lib_plankton.zoo_form.make<any>(
"GET",
[
{
"name": "membership_number",
"type": "text",
"label": "Mitgliedsnummer",
},
{
"name": "enabled",
"type": "checkbox",
"label": "Für Online-Dienste freischalten",
},
{
"name": "name_real_value",
"type": "text",
"label": "Echter Name",
},
{
"name": "name_real_extension",
"type": "text",
"label": "Zusatz für echten Name (für mögliche Dopplung)",
},
{
"name": "name_display",
"type": "text",
"label": "Anzeigename",
},
{
"name": "name_login",
"type": "text",
"label": "Anmeldename",
},
{
"name": "email_address_private_value",
"type": "text",
"label": "Private E-Mail-Adresse",
},
{
"name": "email_address_numberbased_use",
"type": "checkbox",
"label": "Nummernbasierte E-Mail-Adresse verwenden",
},
{
"name": "email_address_namebased_use",
"type": "checkbox",
"label": "Namensbasierte E-Mail-Adresse verwenden",
},
{
"name": "email_redirect_to_private",
"type": "checkbox",
"label": "E-Mails an private Adresse umleiten",
},
{
"name": "salutation",
"type": "text",
"label": "Anrede/Pronomen",
},
],
value => ({
"enabled": (value.enabled ? "on" : ""),
"membership_number": value.membership_number,
"name_real_value": value.name_real_value,
"name_real_extension": value.name_real_extension,
"name_display": value.name_display,
"name_login": value.name_login,
"email_address_private_value": value.email_address_private_value,
"email_address_numberbased_use": (value.email_address_numberbased_use ? "on" : ""),
"email_address_namebased_use": (value.email_address_namebased_use ? "on" : ""),
"email_redirect_to_private": (value.email_redirect_to_private ? "on" : ""),
"salutation": value.salutation,
}),
raw => ({
"enabled": (raw["enabled"] === "on"),
"membership_number": raw["membership_number"],
"name_real_value": raw["name_real_value"],
"name_real_extension": raw["name_real_extension"],
"name_display": raw["name_display"],
"name_login": raw["name_login"],
"email_address_private_value": raw["email_address_private_value"],
"email_address_numberbased_use": (raw["email_address_numberbased_use"] === "on"),
"email_address_namebased_use": (raw["email_address_namebased_use"] === "on"),
"email_redirect_to_private": (raw["email_redirect_to_private"] === "on"),
"salutation": raw["salutation"],
}),
{
}
),
{
"encode_hit": hit => lib_plankton.string.coin(
"{{number}} | {{name}}",
{
"number": hit.preview["membership_number"],
"name": hit.preview["name_real_value"],
}
),
"hook_switch": (state) => {
_id = state.key;
set_state((_id === null) ? "poor" : "rich");
const state_encoded : string = lib_plankton.base64.encode(lib_plankton.json.encode(state));
lib_plankton.zoo_page.set({"name": "members", "parameters": {"state": state_encoded}});
},
}
);
target_element.appendChild(template_request("members"));
lib_plankton.zoo_editor.render<int, any>(
editor,
target_element.querySelector(".members-editor"),
(
Object.assign(
{
},
((state !== null) ? {"state": state} : {})
)
)
);
target_element.querySelector(".members-urge_for_registration").addEventListener(
"click",
async () => {
const verification : string = await _aum.backend.verification_get(_id);
const location_encoded : string = lib_plankton.zoo_page.encode(
{
"name": "register",
"parameters": {
"id": _id,
"verification": verification,
}
}
);
const url_base : string = window.location.href.split("#")[0];
const url : string = (url_base + location_encoded);
const data : Record<string, any> = await lib_plankton.zoo_form.read(editor.form);
const text_paragraphs : Array<string> = [
lib_plankton.string.coin(
"Hi, {{name}}!",
{
"name": "data.name_real_value",
}
),
"Willkommen bei der Linken!",
"Wir als Landesverband Sachsen stellen für unsere Mitglieder verschiedene Online-Dienste zur Verfügung. Dazu gehört eine E-Mail-Adresse bei der Partei, ein Instant Messenger, eine Cloud, ein Wiki und noch einiges mehr.",
"Wenn du die Dienste nutzen möchtest, rufe bitte folgende Adresse auf:",
url,
"Solidarische Grüße, dein Landesverband Sachsen",
];
_aum.backend.email(
[
data.email_address_private_value,
],
"DIE LINKE. | Landesverband Sachsen | Registierung für Online-Dienste",
text_paragraphs.join("\n\n"),
);
// TODO: statt dessen eine E-Mail an die private Adresse des Neumitglieds senden
// target_element.querySelector(".members-result").setAttribute("href", url);
alert(url);
}
);
}
);

View file

@ -0,0 +1,132 @@
lib_plankton.zoo_page.register(
"register",
async (parameters, target_element) => {
const set_state = function (state) {
target_element.querySelector(".register").setAttribute("rel", state);
};
const set_message = function (message) {
target_element.querySelector(".register-message").textContent = message;
};
const id : int = parseInt(parameters["id"]);
const verification : string = parameters["verification"];
update_nav({"mode": null});
const form = new lib_plankton.zoo_form.class_form<any, any>(
x => x,
x => x,
new lib_plankton.zoo_input.class_input_group(
[
{
"name": "id",
"input": new lib_plankton.zoo_input.class_input_hidden(),
},
{
"name": "verification",
"input": new lib_plankton.zoo_input.class_input_hidden(),
},
{
"name": "email_mode",
"input": new lib_plankton.zoo_input.class_input_enumeration(
[
{"value": "none", "label": "keine"},
{"value": "number", "label": "nur numerische"},
{"value": "number_and_name", "label": "numerische und namensbasierte"},
]
),
"label": "Partei-E-Mail-Adresse einrichten",
"help": "Die nummernbasierte Partei-E-Mail-Adresse hat folgenden schematischen Aufbau: \"mitglied-<mitglieds-nummer>@dielinke-sachsen.de\", z.B. \"mitglied-11223344@dielinke-sachsen.de\".\n\nDie namensbasierte Partei-E-Mail-Adresse hat folgenden schematischen Aufbau: \"<name-in-kleinbuchstaben-mit-punkten-getrennt>@dielinke-sachsen.de\". Beispiel: \"Karl Liebknecht\" würde die Adresse \"karl.liebknecht@dielinke-sachsen.de\" bekommen.\n\nDie Partei-E-Mail-Adressen können zum Empfangen von E-Mails verwendet werden. Falls es nötig werden sollte, dass du auch E-Mails mit über die Partei-Adresse verschicken kannst, wende dich bitte an den/die Mitgliederbeauftragte:n!"
},
{
"name": "email_redirect",
"input": new lib_plankton.zoo_input.class_input_checkbox(),
"label": "eingehende E-Mails zu privater Adresse umleiten",
"help": "Um die bei der Partei-Adresse eingegangenen E-Mails zu lesen, gibt es zwei Wege: Entweder du hinterlegst das zugehörige Konto im E-Mail-Client-Programm deiner Wahl und kümmerst dich selbst darum die E-Mails regelmäßig abzurufen oder die E-Mails werden an deine private Adresse weitergeleitet, sodass sie bei deinen gewöhnlichen E-Mails mit auftauchen.\n\nWenn du dir unsicher bist, empfehlen wir dir die Umleitung anzuschalten.",
},
{
"name": "name_display",
"input": new lib_plankton.zoo_input.class_input_text(),
"label": "Anzeigename",
"help": "So wirst du bei Online-Diensten anderen angezeigt.",
},
{
"name": "salutation",
"input": new lib_plankton.zoo_input.class_input_text(),
"label": "Anrede/Pronomen (z.B. 'er/ihn')",
},
{
"name": "name_login",
"input": new lib_plankton.zoo_input.class_input_text(
{
"pattern": "^[0-9a-zA-Z_]+$",
}
),
"label": "Anmeldename",
"help": "Dieser Wert ist der Nutzername für die Anmeldung bei den Online-Diensten. Hierfür solltest du etwas kurzes und prägnantes wählen. Diesen Namen bekommt für gewöhnlich niemand zu sehen. Bitte beachte, dass der Name noch verfügbar sein muss!",
},
{
"name": "password",
"input": new lib_plankton.zoo_input.class_input_password(),
"label": "Passwort",
"help": "Das Passwort für die Anmeldung bei den Online-Diensten",
},
{
"name": "password_confirmation",
"input": new lib_plankton.zoo_input.class_input_password(),
"label": "Passwort wiederholen",
},
]
),
[
{
"label": "Senden",
"procedure": async (get_value, get_representation) => {
set_message("wird verarbeitet …");
set_state("wait");
const value : any = await get_value();
if (value.password !== value.password_confirmation) {
set_message("Die Passwörter stimmen nicht überein.");
set_state("fill");
}
else {
try {
await _aum.backend.member_register(
id,
verification,
value
);
set_message("Danke!");
set_state("done");
}
catch (error) {
set_message("Da ist etwas schief gelaufen :/");
set_state("fill");
}
}
},
},
]
);
target_element.appendChild(template_request("register"));
await form.setup(target_element.querySelector(".register-form") as HTMLElement);
await form.input_write(
{
"id": id,
"verification": verification,
"name_login": "",
"name_display": "",
"salutation": "",
"email_mode": "number_and_name",
"email_redirect": true,
"password": "",
"password_confirmation": "",
}
);
set_state("fill");
},
);

View file

@ -0,0 +1,62 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="stylesheet" type="text/css" href="style.css"/>
<script type="text/javascript" src="logic.js"></script>
<script type="text/javascript">document.addEventListener("DOMContentLoaded", () => {main();});</script>
<template id="login">
<section class="login">
<h2>Login</h2>
<div class="login-form">
</div>
</section>
</template>
<template id="view">
<section class="view">
<h2>Mitglied</h2>
<pre class="view-data">
</pre>
<hr/>
<div>
<button class="view-urge_for_registration">Zur Registrierung auffordern</button>
</div>
<a class="view-result">Registrierung</a>
</section>
</template>
<template id="members">
<section class="members">
<h2>Mitglied</h2>
<div class="members-editor">
</div>
<hr/>
<div class="members-extras">
<button class="members-urge_for_registration">Zur Registrierung auffordern</button>
<a class="members-result">Registrierung</a>
</div>
</section>
</template>
<template id="register">
<section class="register">
<h2>Registrieren</h2>
<div class="register-message">
</div>
<div class="register-form">
</div>
</section>
</template>
</head>
<body>
<header>
<h1>DIE LINKE. Landesverband Sachsen</h1>
</header>
<hr/>
<nav>
<ul>
</ul>
</nav>
<main>
</main>
</body>
</html>

183
source/style/style.css Normal file
View file

@ -0,0 +1,183 @@
:root
{
--hue: 0;
}
html
{
font-family: monospace;
font-size: 1.5em;
background-color: hsl(var(--hue), 0%, 0%);
color: hsl(var(--hue), 0%, 100%);
}
body
{
max-width: 960px;
margin: auto;
padding: 24px;
background-color: hsl(var(--hue), 0%, 12.5%);
color: hsl(var(--hue), 0%, 87.5%);
}
body:not([rel="logged_out"]) nav .logged_out {display: none;}
body:not([rel="logged_in"]) nav .logged_in {display: none;}
a
{
background-color: hsl(var(--hue), 0%, 12.5%);
text-decoration: none;
}
a:not(:hover)
{
/*
color: hsl(var(--hue), 75%, 50%);
*/
color: hsl(var(--hue), 72.3%, 54.7%);
}
a:hover
{
/*
color: hsl(var(--hue), 75%, 75%);
*/
color: hsl(var(--hue), 72.8%, 79.8%)
}
input[type="text"]
,
input[type="number"]
,
input[type="password"]
{
background-color: hsl(var(--hue), 0%, 87.5%);
color: hsl(var(--hue), 0%, 12.5%);
padding: 8px;
}
nav
{
border-bottom: 1px solid hsl(var(--hue), 0%, 100%);
margin-bottom: 16px;
}
nav > ul
{
list-style-type: none;
margin: 0;
padding: 0;
}
nav > ul > li
{
display: inline-block;
margin-right: 16px;
padding: 8px;
/*
text-transform: uppercase;
*/
}
/*
nav > ul > li:hover::before
{
content: "[";
}
nav > ul > li:hover::after
{
content: "]";
}
*/
.plankton_form_field
{
margin-bottom: 16px;
}
.plankton_form_label
{
display: block;
font-size: 0.75em;
font-weight: bold;
}
.plankton_input_group_field
{
margin-bottom: 16px;
}
.plankton_input_group_field_label
{
font-size: 0.75em;
font-weight: bold;
/*
text-transform: capitalize;
*/
}
.plankton_input_group_field_label + :not(.plankton_input_group_field_help)
{
display: block;
}
.plankton_input_group_field_help
{
margin-left: 8px;
font-size: 0.75em;
font-weight: bold;
cursor: help;
}
.plankton_input_group_field_help + *
{
display: block;
}
.plankton_input_enumeration > *
{
display: block;
}
.plankton_editor_actions
{
margin-top: 16px;
}
.plankton_editor_action
{
text-transform: uppercase;
margin: 8px;
}
.members-result:not([href])
{
display: none;
}
.register:not([rel]) .register-message {display: none;}
.register:not([rel]) .register-form {display: none;}
.register[rel="fill"] .register-message {}
.register[rel="fill"] .register-form {}
.register[rel="wait"] .register-message {}
.register[rel="wait"] .register-form {display: none;}
.register[rel="done"] .register-message {}
.register[rel="done"] .register-form {display: none;}
.members:not([rel]) .members-extras {display: none;}
.members[rel="poor"] .members-extras {display: none;}
.members[rel="rich"] .members-extras {}

5
tools/build Executable file
View file

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

49
tools/makefile Normal file
View file

@ -0,0 +1,49 @@
## consts
dir_lib := lib
dir_source := source
dir_temp := temp
dir_build := build
cmd_log := echo "--"
cmd_cat := cat
cmd_chmod := chmod
cmd_mkdir := mkdir -p
cmd_cp := cp
## rules
.PHONY: default
default: ${dir_build}/index.html ${dir_build}/logic.js ${dir_build}/style.css
${dir_build}/index.html: ${dir_source}/structure/index.html.tpl
@ ${cmd_log} "structure …"
@ ${cmd_mkdir} $(dir $@)
@ ${cmd_cat} $^ > $@
${dir_temp}/logic-unlinked.js: \
${dir_lib}/plankton/plankton.d.ts \
${dir_source}/logic/backend.ts \
${dir_source}/logic/conf.ts \
${dir_source}/logic/pages/index.ts \
${dir_source}/logic/pages/login.ts \
${dir_source}/logic/pages/logout.ts \
${dir_source}/logic/pages/members.ts \
${dir_source}/logic/pages/create.ts \
${dir_source}/logic/pages/register.ts \
${dir_source}/logic/main.ts
@ ${cmd_log} "logic | compile …"
@ ${cmd_mkdir} $(dir $@)
@ tsc --lib es2020,dom $^ --outFile $@
${dir_build}/logic.js: ${dir_lib}/plankton/plankton.js ${dir_temp}/logic-unlinked.js
@ ${cmd_log} "logic | link …"
@ ${cmd_mkdir} $(dir $@)
@ ${cmd_cat} $^ > $@
@ ${cmd_chmod} +x $@
${dir_build}/style.css: ${dir_source}/style/style.css
@ ${cmd_log} "style …"
@ ${cmd_mkdir} $(dir $@)
@ ${cmd_cat} $^ > $@

5
tools/run Executable file
View file

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

26
tools/update-plankton Executable file
View file

@ -0,0 +1,26 @@
#!/usr/bin/env sh
## consts
dir=lib/plankton
modules=""
modules="${modules} base"
modules="${modules} file"
modules="${modules} json"
modules="${modules} base64"
modules="${modules} string"
modules="${modules} storage"
modules="${modules} zoo-input"
modules="${modules} zoo-form"
modules="${modules} zoo-search"
modules="${modules} zoo-editor"
modules="${modules} zoo-page"
## exec
mkdir -p ${dir}
cd ${dir}
ptk bundle web ${modules}
cd - > /dev/null