diff --git a/api/newsletter/subscribe.php b/api/newsletter/subscribe.php index f138ff1..1b83767 100644 --- a/api/newsletter/subscribe.php +++ b/api/newsletter/subscribe.php @@ -6,7 +6,7 @@ // CHECK CSRF PROTECTION // $x_cookieless_csrf_protection = getallheaders()["x-cookieless-csrf-protection"] ?? null; - if($x_cookieless_csrf_protection !== "42"){ + if(\Flake\Env::IS_PRODUCTION and $x_cookieless_csrf_protection !== "42"){ // show an excuse page Excuse::show("invalid_csrf_token"); } diff --git a/api/newsletter/unsubscribe.php b/api/newsletter/unsubscribe.php index a67d1f3..42b5efd 100644 --- a/api/newsletter/unsubscribe.php +++ b/api/newsletter/unsubscribe.php @@ -6,7 +6,7 @@ // CHECK CSRF PROTECTION // $x_cookieless_csrf_protection = getallheaders()["x-cookieless-csrf-protection"] ?? null; - if($x_cookieless_csrf_protection !== "42"){ + if(\Flake\Env::IS_PRODUCTION and $x_cookieless_csrf_protection !== "42"){ // show an excuse page Excuse::show("invalid_csrf_token"); } diff --git a/api/newsletter/verify.php b/api/newsletter/verify.php index 6bab2d0..0675a71 100644 --- a/api/newsletter/verify.php +++ b/api/newsletter/verify.php @@ -5,7 +5,7 @@ // CHECK CSRF PROTECTION // $x_cookieless_csrf_protection = getallheaders()["x-cookieless-csrf-protection"] ?? null; - if($x_cookieless_csrf_protection !== "42"){ + if(\Flake\Env::IS_PRODUCTION and $x_cookieless_csrf_protection !== "42"){ // show an excuse page Excuse::show("invalid_csrf_token"); } diff --git a/init.php b/init.php index 427e1a0..7b789dc 100644 --- a/init.php +++ b/init.php @@ -4,4 +4,22 @@ // LOAD ENV CONFIG // require_once("./.env.php"); + + + // PREPARE CLASSES FOR STATE STORAGE // + // lang reference + class Lang_Ref { + public static object $dict; + } + + // nav + class Nav { + public static ?string $active = null; + } + + // footer + class Footer { + public static array $lang_href; + public static bool $cookieaccept_but_no_lang = false; + } ?> diff --git a/meta.php b/meta.php index 0133954..716a24f 100644 --- a/meta.php +++ b/meta.php @@ -1,6 +1,6 @@ "", "target" => "page/start"], ["path" => ":lang", "target" => "page/start"], + + ["path" => "timeline", "target" => "page/timeline"], + ["path" => ":lang/timeline", "target" => "page/timeline"], + ["path" => "impressum", "target" => "page/imprint"], ["path" => "imprint", "target" => "page/imprint"], ["path" => "datenschutz", "target" => "page/privacy"], diff --git a/newsletter/content/2024-07-29-sbggjetzt-1-3.data.php b/newsletter/content/2024-07-29-sbggjetzt-1-3.data.php new file mode 100644 index 0000000..233b29f --- /dev/null +++ b/newsletter/content/2024-07-29-sbggjetzt-1-3.data.php @@ -0,0 +1,37 @@ + [ + "de" => "SBGG.jetzt: Großes SBGG.jetzt Update!", + "en" => "SBGG.jetzt: Big SBGG.jetzt Update!" + ], + + "main" => [ + "de" => <<Großes SBGG.jetzt Update! + +

Bald ist es so weit: In drei Tagen können Anmeldungen für die Personenstandsänderung beim Standesamt abgegeben werden! Pünktlich dazu jetzt auf der Webseite:

+ + + +

Alles hier ansehen: www.SBGG.jetzt

+ HTML, + "en" => <<Big SBGG.jetzt Update! + +

It will soon be time: In three days, registrations for the change in civil status can be submitted to the registry office! Just on time on the website:

+ + + +

View it here: www.SBGG.jetzt

+ HTML + ] + ]; +?> diff --git a/newsletter/template/base.html b/newsletter/template/base.html index c8e3e76..85c09fc 100644 --- a/newsletter/template/base.html +++ b/newsletter/template/base.html @@ -33,6 +33,9 @@ br.gone { display: none; } + li { + margin-bottom: 8px; + } .gray { color: #828997; diff --git a/page/accordion.css b/page/accordion.css new file mode 100644 index 0000000..c8e0203 --- /dev/null +++ b/page/accordion.css @@ -0,0 +1,91 @@ +.accordion { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + gap: 2rem; +} +@media only screen and (max-width: 1000px) { + .accordion { + gap: 1rem; + } +} + +.accordion > .item.box, +.accordion > .wrapper > .item.box { + justify-content: flex-start; + align-items: stretch; + gap: 0; + + padding: 0; + + overflow: hidden; + text-align: left; +} + + + +.accordion > .item.box > .head, +.accordion > .wrapper > .item.box > .head { + display: flex; + flex-direction: row; + align-items: center; + flex-wrap: nowrap; + gap: 1.5rem; + + padding: 1rem 2rem; + + /* HACK: This is equal to an `outline-bottom` */ + border-bottom: 0.125rem solid var(--color-gray-dark-dark); + margin-bottom: -0.125rem; +} +.accordion > .item.box > .head:hover, +.accordion > .wrapper > .item.box > .head:hover { + cursor: pointer; +} + +.accordion > .item.box > .head > .icon, +.accordion > .wrapper > .item.box > .head > .icon { + color: var(--theme); +} +.accordion > .item.box > .head > .title, +.accordion > .wrapper > .item.box > .head > .title { + flex-grow: 1; + + justify-content: flex-start; +} + +@media only screen and (max-width: 1000px) { + .accordion > .item.box > .head > .icon, + .accordion > .wrapper > .item.box > .head > .icon, + .accordion > .item.box > .head > .title, + .accordion > .wrapper > .item.box > .head > .title { + font-size: 1rem; + } + + .accordion .head > .title .copylink { + /* HACK: Fix copylink positioning */ + top: -0.35rem; + } +} + + + +.accordion > .item.box > .body-container, +.accordion > .wrapper > .item.box > .body-container { + transition: max-height 0.5s ease-out; +} + +.accordion > .item.box:not(.open) > .body-container, +.accordion > .wrapper > .item.box:not(.open) > .body-container { + max-height: 0; +} + +.accordion > .item.box > .body-container > .body, +.accordion > .wrapper > .item.box > .body-container > .body { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + gap: 1rem; + + padding: 2rem; +} diff --git a/page/accordion.js b/page/accordion.js new file mode 100644 index 0000000..eb8c5a3 --- /dev/null +++ b/page/accordion.js @@ -0,0 +1,208 @@ +"use strict"; + +let accordion_view_list = []; +const accordion_resize_observer = new ResizeObserver((changed_item_list) => { + for(let one_changed_item of changed_item_list){ + accordion_body_resize(one_changed_item.target); + } +}); + + + +window.addEventListener("load", function(event){ + // COLLECT ACCORDION VIEWS // + let accordion_root_list = document.getElementsByClassName("accordion"); + for(let one_accordion_root of accordion_root_list){ + // collect this view + let accordion_view = accordion_collect_view(one_accordion_root); + accordion_view_list.push(accordion_view); + + // initialized this view + accordion_init(accordion_view); + } +}); + + + +/** +* HELPER: Collect data for one accordion view. +* +* @param root Root element. +* +* @return Accordion view object. +*/ +function accordion_collect_view(root){ + // COLLECT ITEMS // + let item_list = []; + for(let one_child of root.children){ + // maybe unwrap + if(one_child.classList.contains("wrapper")) one_child = one_child.children[0] ?? null; + + // validate child as item + if(!one_child.classList.contains("item")) continue; + + // find head element + let head; + for(let one_item_child of one_child.children){ + if(one_item_child.classList.contains("head")){ + head = one_item_child; + break; + } + } + if(head === undefined) throw "Unable to find accordion item head"; + + // find icon + let icon; + for(let one_head_child of head.children){ + if(one_head_child.classList.contains("icon")){ + icon = one_head_child; + break; + } + } + if(icon === undefined) throw "Unable to find accordion item icon"; + + // find title + let title; + for(let one_head_child of head.children){ + if(one_head_child.classList.contains("title")){ + title = one_head_child; + break; + } + } + if(title === undefined) throw "Unable to find accordion item title"; + + // find body container + let bodyContainer; + for(let one_item_child of one_child.children){ + if(one_item_child.classList.contains("body-container")){ + bodyContainer = one_item_child; + break; + } + } + if(bodyContainer === undefined) throw "Unable to find accordion body container"; + + // add to item list + item_list.push({ + element: one_child, + head: head, + icon: icon, + title: title, + bodyContainer: bodyContainer, + }); + } + + + // BUILD OBJECT // + return { + root: root, + item_list: item_list, + }; +} + + + +/** +* HELPER: Initialize accordion view. +* +* @param accordion Accordion view object. +*/ +function accordion_init(accordion){ + // REGISTER ONCLICK HANDLERS // + for(let one_item of accordion.item_list){ + // head + one_item.head.onclick = function(event){ + event.stopPropagation(); + accordion_click(one_item, accordion); + }; + + // title + one_item.title.tabIndex = 0; + one_item.title.onclick = function(event){ + event.stopPropagation(); + accordion_click(one_item, accordion); + }; + one_item.title.onkeypress = function(event){ + if(event.key === "Enter"){ + event.preventDefault(); + event.stopPropagation(); + accordion_click(one_item, accordion); + } + }; + } + + + // RESET OPENED STATE // + for(let one_item of accordion.item_list){ + accordion_state_set(one_item, accordion_state_get(one_item)); + } +} + + + +/** +* CALLBACK: Item head was clicked. +* +* @param item Accordion item. +* @param accordion This item's accordion view object. +*/ +function accordion_click(item, accordion){ + // get our old state + let old_state = accordion_state_get(item); + + // close all other items // + for(let one_item of accordion.item_list){ + accordion_state_set(one_item, false); + } + + // set our new state + accordion_state_set(item, !old_state); +} + + + +/** +* GETTER: Get state of one accordion item. +* +* @param item Accordion item. +*/ +function accordion_state_get(item){ + return item.element.classList.contains("open"); +} + + + +/** +* SETTER: Set state of one accordion item. +* +* @param item Accordion item. +* @param state New state. +*/ +function accordion_state_set(item, state){ + // set observe state + if(state) accordion_resize_observer.observe(item.bodyContainer); + else accordion_resize_observer.unobserve(item.bodyContainer); + + // update class + item.element.classList.remove("open"); + if(state) item.element.classList.add("open"); + + // update icon direction + item.icon.classList.remove("ti-chevron-up", "ti-chevron-right", "ti-chevron-down", "ti-chevron-left"); + if(state) item.icon.classList.add("ti-chevron-down"); + else item.icon.classList.add("ti-chevron-right"); + + // set body height + if(state) accordion_body_resize(item.bodyContainer); + else item.bodyContainer.style.maxHeight = 0; +} + + + +/** +* HELPER: Resize body height. +* +* @param bodyContainer Accordion body container. +*/ +function accordion_body_resize(bodyContainer){ + bodyContainer.style.maxHeight = bodyContainer.scrollHeight + "px"; +} diff --git a/page/admin/footer.php b/page/admin/footer.php deleted file mode 100644 index 01b1866..0000000 --- a/page/admin/footer.php +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - diff --git a/page/admin/login/index.php b/page/admin/login/index.php index 9347239..e85ff12 100644 --- a/page/admin/login/index.php +++ b/page/admin/login/index.php @@ -3,8 +3,6 @@ namespace Kimendisch\Sbgg_Jetzt; use Flake\Url_Redirect; use Flake\Request; - use Flake\Lang; - use Flake\Lang_Dict; use Flake\Page; use Flake\Cookieaccept; use Flake\Csrf; @@ -25,42 +23,30 @@ // LANGUAGE MANAGER // // hack: fake get param from constant $_GET["lang"] = "en"; - - // initialize - $lang = new Lang(list: ["de", "en"], default: "en"); - - // load dict - $dict = new Lang_Dict($lang); - require("./page/strings.php"); + require("./page/lang_base.php"); // PAGE INIT // Page::start(); Page::title("SBGG.jetzt - Admin Area"); - Page::icon("./asset/logo-256.png"); - - Page::lang($lang->get()); - Page::viewport(scale: 1, zoom: true); - Page::robots(index: false, follow: false); - Page::author("Kim Endisch"); + Page::$head["og_title"] = ''; - Page::$head["analytics"] = ''; - Page::css("./page/start/style.css"); - Page::css("./page/start/style.css.php", eval: true); + require("./page/page_base.php"); Page::css(__DIR__ . "/style.css"); - - Page::font("ubuntu"); - Page::font("tabler"); ?> + + + + -