diff --git a/public/index.html b/public/index.html
index 29a124dc..358423a7 100644
--- a/public/index.html
+++ b/public/index.html
@@ -25,7 +25,7 @@
"/logout": "/pages/ctrl_logout.js",
- "/login": "/pages/connectpage/connectpage.js",
+ "/login": "/pages/connectpage/ctrl_connectpage.js",
//"/": "/pages/home/index.js",
"": "/pages/ctrl_notfound.js",
diff --git a/public/pages/adminpage/ctrl_settings.js b/public/pages/adminpage/ctrl_settings.js
index 67f5e187..4096b4bd 100644
--- a/public/pages/adminpage/ctrl_settings.js
+++ b/public/pages/adminpage/ctrl_settings.js
@@ -39,8 +39,7 @@ export default AdminOnly(WithShell(function(render) {
);
effect(config$.pipe(
- rxjs.map((formSpec) => createForm(formSpec, formTmpl({ autocomplete: false }))),
- rxjs.mergeMap((promise) => rxjs.from(promise)),
+ rxjs.mergeMap((formSpec) => createForm(formSpec, formTmpl(false))),
rxjs.map(($form) => [$form]),
applyMutation(qs($container, `[data-bind="form"]`), "appendChild"),
));
diff --git a/public/pages/connectpage/component_forkme.js b/public/pages/connectpage/component_forkme.js
new file mode 100644
index 00000000..512c8352
--- /dev/null
+++ b/public/pages/connectpage/component_forkme.js
@@ -0,0 +1,25 @@
+import { createElement } from "../../lib/skeleton/index.js";
+
+export default createElement(`
+
+
+
+
+`);
diff --git a/public/pages/connectpage/component_poweredby.js b/public/pages/connectpage/component_poweredby.js
new file mode 100644
index 00000000..0aa4fd30
--- /dev/null
+++ b/public/pages/connectpage/component_poweredby.js
@@ -0,0 +1,29 @@
+import { createElement } from "../../lib/skeleton/index.js";
+import t from "../../lib/locales.js";
+
+export default createElement(`
+
+`);
diff --git a/public/pages/connectpage/connectpage.js b/public/pages/connectpage/connectpage.js
deleted file mode 100644
index 88239e3e..00000000
--- a/public/pages/connectpage/connectpage.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import { createElement } from "../../lib/skeleton/index.js";
-
-export default function(render) {
- render(createElement(` LOGIN
`));
-}
diff --git a/public/pages/connectpage/ctrl_connectpage.js b/public/pages/connectpage/ctrl_connectpage.js
new file mode 100644
index 00000000..52480abd
--- /dev/null
+++ b/public/pages/connectpage/ctrl_connectpage.js
@@ -0,0 +1,56 @@
+import { createElement, createRender } from "../../lib/skeleton/index.js";
+import rxjs, { effect, applyMutation } from "../../lib/rx.js";
+import { qs } from "../../lib/dom.js";
+
+import ctrlForm from "./ctrl_form.js";
+import config$ from "./model_config.js";
+
+import $fork from "./component_forkme.js";
+import $poweredby from "./component_poweredby.js";
+
+export default function(render) {
+ const $page = createElement(`
+
+ `);
+ render($page);
+
+ // feature1: connection form
+ ctrlForm(createRender(qs($page, `[data-bind="component_form"]`)));
+
+ // feature2: forkme button
+ effect(config$.pipe(
+ rxjs.filter(({ fork_button }) => fork_button !== false),
+ rxjs.mapTo([$fork]),
+ applyMutation(qs($page, `[data-bind="component_forkme"]`), "appendChild"),
+ ));
+
+ // feature3: poweredby button
+ effect(config$.pipe(
+ rxjs.filter(({ fork_button }) => fork_button !== false),
+ rxjs.mapTo([$poweredby]),
+ applyMutation(qs($page, `[data-bind="component_poweredby"]`), "appendChild"),
+ ));
+
+ // feature4: center the form
+ effect(rxjs.fromEvent(window, "resize").pipe(
+ rxjs.startWith(null),
+ rxjs.map(() => {
+ let size = 300;
+ const $screen = document.querySelector(".login-form");
+ if ($screen) size = $screen.offsetHeight;
+
+ size = Math.round((document.body.offsetHeight - size) / 2);
+ if (size < 0) return 0;
+ if (size > 150) return 150;
+ return size;
+ }),
+ rxjs.map((size) => ["padding-top", `${size}px`]),
+ applyMutation(qs($page, `[data-bind="centerthis"]`), "style", "setProperty"),
+ ));
+}
diff --git a/public/pages/connectpage/ctrl_form.css b/public/pages/connectpage/ctrl_form.css
new file mode 100644
index 00000000..73c7ea90
--- /dev/null
+++ b/public/pages/connectpage/ctrl_form.css
@@ -0,0 +1,103 @@
+.component_page_connection_form .box {
+ margin-bottom: 7px;
+ border-radius: 3px;
+ box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.2);
+ background: white;
+}
+.component_page_connection_form div.buttons {
+ display: flex;
+ margin: 0px 0px 15px 0px;
+ padding: 5px;
+ overflow-x: auto;
+}
+.component_page_connection_form div.buttons button {
+ min-width: 110px;
+ padding: 8px 5px;
+ box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2);
+}
+.component_page_connection_form div.buttons button:not(.active):hover {
+ transition: background 0.2s ease;
+ background: rgba(0, 0, 0, 0.03);
+}
+.component_page_connection_form div.buttons button.active {
+ transition: box-shadow 0.2s;
+ box-shadow: 0px 2px 20px rgba(0, 0, 0, 0.2);
+}
+.component_page_connection_form .formBody form .formbuilder {
+ animation-name: PageConnectionFormBody;
+ opacity: 0;
+ animation-duration: 0.25s;
+ animation-fill-mode: forwards;
+}
+.component_page_connection_form form {
+ padding-top: 5px;
+}
+.component_page_connection_form form label {
+ font-style: italic;
+ font-size: 0.9em;
+ display: block;
+ text-transform: capitalize;
+}
+.component_page_connection_form form .advanced_form {
+ max-height: 156px;
+ overflow-y: auto;
+ margin-top: 5px;
+ padding-right: 10px;
+}
+.component_page_connection_form form button.emphasis {
+ margin-top: 10px;
+ color: white;
+ text-transform: uppercase;
+}
+.component_page_connection_form form .third-party {
+ text-align: center;
+}
+.component_page_connection_form form .third-party img {
+ height: 115px;
+ margin: 0;
+}
+.component_page_connection_form form .component_checkbox .indicator {
+ top: 2px;
+}
+.component_page_connection_form form input.component_input[name="oauth2"] {
+ display: none;
+}
+.component_page_connection_form .component_loader {
+ margin: 45px 0;
+}
+
+.component_page_connection_form.form-appear {
+ opacity: 0;
+ transform: translateX(5px);
+}
+
+.component_page_connection_form.form-appear.form-appear-active {
+ opacity: 1;
+ transform: translateX(0);
+ transition: transform 0.25s ease-out, opacity 0.5s ease-out;
+}
+
+.scroll-x {
+ overflow-x: auto !important;
+ overflow-y: hidden !important;
+ -webkit-overflow-scrolling: touch;
+}
+.scroll-x::-webkit-scrollbar {
+ height: 4px;
+}
+.scroll-x::-webkit-scrollbar-track {
+ background: white;
+}
+.scroll-x::-webkit-scrollbar-thumb {
+ -webkit-border-radius: 2px;
+ -moz-border-radius: 2px;
+ -ms-border-radius: 2px;
+ -o-border-radius: 2px;
+ border-radius: 2px;
+ background: rgba(0, 0, 0, 0.1);
+}
+
+.dark-mode div.buttons button.active {
+ background: var(--bg-color);
+ color: var(--super-light);
+}
diff --git a/public/pages/connectpage/ctrl_form.js b/public/pages/connectpage/ctrl_form.js
new file mode 100644
index 00000000..0d7dbbfb
--- /dev/null
+++ b/public/pages/connectpage/ctrl_form.js
@@ -0,0 +1,88 @@
+import { createElement } from "../../lib/skeleton/index.js";
+import rxjs, { effect, applyMutation, preventDefault } from "../../lib/rx.js";
+import { qs, qsa, safe } from "../../lib/dom.js";
+import { createForm, mutateForm } from "../../lib/form.js";
+import { formTmpl } from "../../components/form.js";
+
+import CSSLoader from "../../helpers/css.js";
+
+import config$ from "./model_config.js";
+import backend$ from "./model_backend.js";
+import { setCurrentBackend, getCurrentBackend } from "./state.js";
+
+export default function(render) {
+ const $page = createElement(`
+
+ `);
+
+ // feature1: backend selector
+ effect(config$.pipe(
+ // dom creation
+ rxjs.map(({ connections }) => connections),
+ rxjs.mergeMap((conns) => conns.map((conn, i) => ({...conn, n: i }))),
+ rxjs.map(({ label, n }) => createElement(``)),
+ rxjs.map(($button) => [$button]), applyMutation(qs($page, `[role="navigation"]`), "appendChild"),
+ // initialise selection
+ rxjs.toArray(),
+ rxjs.map((conns) => Math.max(0, conns.length / 2 - 1)),
+ rxjs.tap((current) => setCurrentBackend(current)),
+ ));
+
+ // feature2: interaction with the buttons
+ effect(getCurrentBackend().pipe(
+ rxjs.first(),
+ rxjs.map(() => qsa($page, `[role="navigation"] button`)),
+ rxjs.mergeMap((els) => els),
+ rxjs.mergeMap(($button) => rxjs.fromEvent($button, "click")),
+ rxjs.map((e) => parseInt(e.target.getAttribute("data-current"))),
+ rxjs.tap((current) => setCurrentBackend(current)),
+ ));
+
+ // feature3: highlight the selected button
+ effect(getCurrentBackend().pipe(
+ rxjs.map((n) => ({ $buttons: qsa($page, `[role="navigation"] button`), n })),
+ rxjs.tap(({ $buttons }) => $buttons.forEach(($node) => $node.classList.remove("active"))),
+ rxjs.map(({ $buttons, n }) => $buttons[n]),
+ rxjs.filter(($button) => !!$button),
+ rxjs.tap(($button) => $button.classList.add("active")),
+ ));
+
+ // feature4: insert all the connection form
+ effect(rxjs.combineLatest(
+ config$.pipe(
+ rxjs.first(),
+ rxjs.mergeMap(({ connections }) => connections),
+ rxjs.mergeMap(({ type }) => backend$.pipe(rxjs.map((spec) => spec[type]))),
+ rxjs.mergeMap((formSpec) => createForm(formSpec, formTmpl(true))),
+ rxjs.toArray(),
+ rxjs.share(),
+ ),
+ getCurrentBackend(),
+ ).pipe(
+ rxjs.map(([$forms, n]) => [$forms[n]]),
+ applyMutation(qs($page, "form"), "replaceChildren"),
+ rxjs.tap(() => qs($page, "form").appendChild(createElement(``))),
+ ));
+
+ // feature5: form submission
+ effect(rxjs.fromEvent(qs($page, "form"), "submit").pipe(
+ preventDefault(),
+ rxjs.map((e) => new FormData(e.target)),
+ rxjs.map((formData) => {
+ const json = {};
+ for (const pair of formData.entries()) {
+ json[pair[0]] = pair[1] === "" ? null : pair[1];
+ }
+ return json;
+ }),
+ dbg("SUBMIT"),
+ ));
+
+ render($page);
+}
+
+const css = await CSSLoader(import.meta, "ctrl_form.css");
diff --git a/public/pages/connectpage/model_backend.js b/public/pages/connectpage/model_backend.js
new file mode 100644
index 00000000..a4c19c33
--- /dev/null
+++ b/public/pages/connectpage/model_backend.js
@@ -0,0 +1,7 @@
+import rxjs from "../../lib/rx.js";
+import ajax from "../../lib/ajax.js";
+
+export default ajax({
+ url: "/api/backend",
+ responseType: "json",
+}).pipe(rxjs.map(({ responseJSON }) => responseJSON.result));
diff --git a/public/pages/connectpage/model_config.js b/public/pages/connectpage/model_config.js
new file mode 100644
index 00000000..3c090217
--- /dev/null
+++ b/public/pages/connectpage/model_config.js
@@ -0,0 +1,10 @@
+import rxjs from "../../lib/rx.js";
+import ajax from "../../lib/ajax.js";
+
+export default ajax({
+ url: "/api/config",
+ responseType: "json",
+}).pipe(
+ rxjs.map(({ responseJSON }) => responseJSON.result),
+ rxjs.share(),
+);
diff --git a/public/pages/connectpage/state.js b/public/pages/connectpage/state.js
new file mode 100644
index 00000000..025c753f
--- /dev/null
+++ b/public/pages/connectpage/state.js
@@ -0,0 +1,11 @@
+import rxjs from "../../lib/rx.js";
+
+const currentBackend$ = new rxjs.Subject(1);
+
+export function setCurrentBackend(n) {
+ currentBackend$.next(n);
+}
+
+export function getCurrentBackend() {
+ return currentBackend$.asObservable();
+}