Landing: Implement registration success/failure flows

Change-Id: I0b48cfb0c0b35bfe7c71b13f8953951821fb3958
diff --git a/apps/landing/.gitignore b/apps/landing/.gitignore
index c75eecc..132a090 100644
--- a/apps/landing/.gitignore
+++ b/apps/landing/.gitignore
@@ -1 +1,2 @@
-/public
+public/
+resources/
\ No newline at end of file
diff --git a/apps/landing/app.cue b/apps/landing/app.cue
new file mode 100644
index 0000000..1d6e418
--- /dev/null
+++ b/apps/landing/app.cue
@@ -0,0 +1,8 @@
+app: {
+	type: "hugo:latest"
+	ingress: {
+		network: "Private"
+		subdomain: "landing"
+		auth: enabled: false
+	}
+}
diff --git a/apps/landing/hugo.toml b/apps/landing/hugo.toml
index b7cb8bc..a7a29d1 100644
--- a/apps/landing/hugo.toml
+++ b/apps/landing/hugo.toml
@@ -7,3 +7,6 @@
     Content-Security-Policy = 'connect-src app.v1.dodo.cloud'
     Referrer-Policy = 'strict-origin-when-cross-origin'
     X-Content-Type-Options = 'nosniff'
+
+[params]
+  apiBaseURL = "https://app.v1.dodo.cloud"
diff --git a/apps/landing/layouts/_default/baseof.html b/apps/landing/layouts/_default/baseof.html
index 153bd09..dd82ad4 100644
--- a/apps/landing/layouts/_default/baseof.html
+++ b/apps/landing/layouts/_default/baseof.html
@@ -3,9 +3,12 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <link rel="stylesheet" href="/styles/style.css?v=0.0.2">
+    <link rel="stylesheet" href="/styles/style.css?v=0.0.6">
     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/hack-font/3.3.0/web/hack.min.css">
     <title>dodo</title>
+    <script>
+        apiBaseURL = "{{ .Site.Params.apiBaseURL }}";
+    </script>
 </head>
 <body>
     <div class="container">
diff --git a/apps/landing/layouts/_default/list.html b/apps/landing/layouts/_default/list.html
index 471e61a..c92d5c5 100644
--- a/apps/landing/layouts/_default/list.html
+++ b/apps/landing/layouts/_default/list.html
@@ -30,5 +30,6 @@
 <div class="footer-form">
     {{ partial "register-form.html" . }}
 </div>
-<script src="/js/main.js?v=0.0.2"></script>
+<script src="/js/main.js?v=0.0.5"></script>
+<script src="/js/register.js?v=0.0.3"></script>
 {{ end }}
diff --git a/apps/landing/layouts/partials/register-form.html b/apps/landing/layouts/partials/register-form.html
index 3f35896..03e08c4 100644
--- a/apps/landing/layouts/partials/register-form.html
+++ b/apps/landing/layouts/partials/register-form.html
@@ -1,24 +1,24 @@
-<div class="form-container-footer">
-    <form id="register-form" method="POST" action="/register" class="form-group-footer" onsubmit="return register()">
-		<label>
-			domain
-			<select id="network" name="domain">
+<div id="form-container" class="form-container-footer">
+	<form id="register-form" method="POST" action="/register" class="form-group-footer" onsubmit="return register()">
+		<h3 id="error-message"></h3>
+		<div class="reg-inputs">
+			<select id="network" name="domain" required>
+				<option value="" disabled selected>domain</option>
 				<option value="dodoapp.xyz">dodoapp.xyz</option>
 			</select>
-		</label>
-		<label>
-			subdomain
-			<input id="subdomain" type="text" name="subdomain" />
-		</label>
-		<label>
-			application type
-			<select id="app-type" name="app-type">
+			<input id="subdomain" type="text" name="subdomain" placeholder="subdomain" required>
+			<select id="app-type" name="app-type" required>
+				<option value="" disabled selected>application type</option>
 			</select>
-		</label>
-        <label>
-			ssh public key
-			<textarea id="public-key" name="public-key" rows="2" required></textarea>
-		</label>
-        <button type="submit">create first app</button>
-    </form>
+		</div>
+		<textarea id="public-key" name="public-key" rows="2" placeholder="ssh public key"></textarea>
+		<button id="create-app-button" type="submit">
+			<svg id="spinner" class="animated-spinner" fill="none" height="18" width="18" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" style="display: none;">
+				<g>
+					<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="4" />
+				</g>
+			</svg>
+			create first app
+		</button>
+	</form>
 </div>
diff --git a/apps/landing/layouts/register/single.html b/apps/landing/layouts/register/single.html
index f576e41..7d872a5 100644
--- a/apps/landing/layouts/register/single.html
+++ b/apps/landing/layouts/register/single.html
@@ -4,4 +4,5 @@
         {{ partial "register-form.html" . }}
     </div>
 </div>
+<script src="/js/register.js?v=0.0.3"></script>
 {{ end }}
diff --git a/apps/landing/static/js/main.js b/apps/landing/static/js/main.js
index 24e651e..a068560 100644
--- a/apps/landing/static/js/main.js
+++ b/apps/landing/static/js/main.js
@@ -139,55 +139,3 @@
         fact.addEventListener("mouseover", () => handleMouseover(facts[index].params.image, index, facts[index].params.title));
     });
 });
-
-async function loadPublicData() {
-  let networkSelect = document.querySelector("select#network");
-  if (networkSelect === undefined) {
-	return;
-  }
-  networkSelect.innerHTML = "";
-  let appTypeSelect = document.querySelector("select#app-type");
-  if (appTypeSelect === undefined) {
-	return;
-  }
-  appTypeSelect.innerHTML = "";
-  let resp = await fetch("https://app.v1.dodo.cloud/api/public-data");
-  if (!resp.ok) {
-	return;
-  }
-  let data = await resp.json();
-  data.networks.forEach((network) => {
-	let opt = document.createElement("option");
-	opt.setAttribute("value", network.domain);
-	opt.innerHTML = network.domain;
-	networkSelect.appendChild(opt);
-  });
-  data.types.forEach((t) => {
-	let opt = document.createElement("option");
-	opt.setAttribute("value", t);
-	opt.innerHTML = t;
-	appTypeSelect.appendChild(opt);
-  });
-}
-
-function register() {
-  var data = {
-	type: document.getElementById("app-type").value,
-	adminPublicKey: document.getElementById("public-key").value,
-	network: document.getElementById("network").value,
-	subdomain: document.getElementById("subdomain").value,
-  };
-  fetch("https://app.v1.dodo.cloud/api/apps", {
-	method: "POST",
-	body: JSON.stringify(data),
-  }).then((resp) => {
-	resp.json().then((r) => {
-	  console.log(r);
-	});
-  });
-  return false;
-}
-
-document.addEventListener("DOMContentLoaded", () => {
-  loadPublicData();
-});
diff --git a/apps/landing/static/js/register.js b/apps/landing/static/js/register.js
new file mode 100644
index 0000000..ca66f0b
--- /dev/null
+++ b/apps/landing/static/js/register.js
@@ -0,0 +1,128 @@
+async function loadPublicData() {
+    let networkSelect = document.querySelector("select#network");
+    if (networkSelect === undefined) {
+        return;
+    }
+    let appTypeSelect = document.querySelector("select#app-type");
+    if (appTypeSelect === undefined) {
+        return;
+    }
+    networkSelect.innerHTML = `<option value="" disabled selected>domain</option>`;
+    appTypeSelect.innerHTML = `<option value="" disabled selected>application type</option>`;
+    let resp = await fetch(`${apiBaseURL}/api/public-data`);
+    if (!resp.ok) {
+        return;
+    }
+    let data = await resp.json();
+    data.networks.forEach((network) => {
+        let opt = document.createElement("option");
+        opt.setAttribute("value", network.domain);
+        opt.innerHTML = network.domain;
+        networkSelect.appendChild(opt);
+    });
+    data.types.forEach((t) => {
+        let opt = document.createElement("option");
+        opt.setAttribute("value", t);
+        opt.innerHTML = t;
+        appTypeSelect.appendChild(opt);
+    });
+}
+
+function errorRender(error) {
+    const errorMsg = document.getElementById("error-message");
+    errorMsg.innerHTML = error;
+    errorMsg.style.display = "block";
+}
+
+function triggerForm(status, errDisplay, buttonTxt, spinnerStatus) {
+    const form = document.getElementById('register-form');
+    const elements = form.querySelectorAll('input, select, textarea, button');
+    elements.forEach(element => {
+        element.disabled = status;
+    });
+    const errorMsg = document.getElementById("error-message");
+    errorMsg.style.display = errDisplay;
+    const button = document.getElementById("create-app-button");
+    button.removeChild(button.lastChild);
+    button.appendChild(document.createTextNode(buttonTxt));
+    const spinner = document.getElementById("spinner");
+    spinner.style.display = spinnerStatus;
+}
+
+async function register(event) {
+    event.preventDefault();
+    const data = {
+        type: document.getElementById("app-type").value,
+        adminPublicKey: document.getElementById("public-key").value,
+        network: document.getElementById("network").value,
+        subdomain: document.getElementById("subdomain").value,
+    };
+    triggerForm(true, "none", "\u00A0\u00A0\creating first app", "inline-block");
+    fetch(`${apiBaseURL}/api/apps`, {
+        method: "POST",
+        body: JSON.stringify(data)
+    })
+        .then(response => {
+            if (!response.ok) {
+                errorRender("Internal error, try again");
+                triggerForm(false, "block", "create first app", "none");
+            }
+            return response.json();
+        })
+        .then(result => {
+            const domain = document.getElementById("network").value;
+            const subdomain = document.getElementById("subdomain").value;
+            const appLink = `https://${subdomain}.${domain}`;
+            const appStatusLink = `https://status.${subdomain}.${domain}`;
+
+            const successHTML = `
+            <div class="registration-outcome">
+                <h3>Application has been successfully deployed, use information below to access it:</h3>
+                <button onclick="window.open('${appLink}', '_blank')">Application address: ${appLink}</button>
+                <br>
+                <button onclick="window.open('${appStatusLink}', '_blank')">Status page address: ${appStatusLink}</button>
+                <br>
+                <button class="pass" onclick="copyPassword('${result.password}')" id="copy-button">
+                    Status page password:&nbsp;<strong>${result.password}</strong> 
+                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 256 256">
+                        <path fill="currentColor" d="M216 32H88a8 8 0 0 0-8 8v40H40a8 8 0 0 0-8 8v128a8 8 0 0 0 8 8h128a8 8 0 0 0 8-8v-40h40a8 8 0 0 0 8-8V40a8 8 0 0 0-8-8m-56 176H48V96h112Zm48-48h-32V88a8 8 0 0 0-8-8H96V48h112Z" />
+                    </svg>
+                </button>
+                <div id="tooltip" class="tooltip">Password copied to clipboard</div>
+            </div>`;
+            document.getElementById("form-container").innerHTML = successHTML;
+        })
+        .catch(error => {
+            errorRender(`Failed to deploy application. Error: '${error.message}'`);
+            triggerForm(false, "block", "create first app", "none");
+        })
+        .finally(() => {
+            document.getElementById("spinner").style.display = "none";
+        });
+    return;
+}
+
+function copyPassword(password) {
+    navigator.clipboard.writeText(password).then(() => {
+        const button = document.getElementById("copy-button");
+        const tooltip = document.getElementById("tooltip");
+        const rect = button.getBoundingClientRect();
+        const tooltipWidth = tooltip.offsetWidth;
+        tooltip.style.top = `${rect.top - 30 + window.scrollY}px`;
+        tooltip.style.left = `${rect.left + (rect.width / 2) - (tooltipWidth / 2) + window.scrollX}px`;
+        tooltip.style.opacity = "1";
+        tooltip.style.visibility = "visible";
+        setTimeout(() => {
+            tooltip.style.opacity = "0";
+            tooltip.style.visibility = "hidden";
+        }, 1000);
+    });
+}
+
+document.addEventListener("DOMContentLoaded", () => {
+    loadPublicData();
+    const registerForm = document.getElementById("register-form");
+    if (registerForm) {
+        registerForm.onsubmit = register;
+    }
+});
diff --git a/apps/landing/static/styles/style.css b/apps/landing/static/styles/style.css
index 6537d58..8cd8e34 100644
--- a/apps/landing/static/styles/style.css
+++ b/apps/landing/static/styles/style.css
@@ -18,7 +18,9 @@
   font-size: var(--fontSize);
 }
 
-input, textarea, select {
+input,
+textarea,
+select {
   font-family: Hack, monospace;
   font-size: var(--fontSize);
 }
@@ -291,6 +293,8 @@
   padding: 20px;
   display: flex;
   justify-content: center;
+  flex-direction: column;
+  align-items: center;
 }
 
 .form-container-footer {
@@ -309,14 +313,12 @@
 .form-group-footer {
   display: flex;
   justify-content: space-between;
-  align-items: center;
+  align-items: flex-start;
   flex-direction: column;
 }
 
 .form-group-footer label {
   display: block;
-  margin-bottom: 5px;
-  padding-bottom: 10px;
   color: var(--formText);
   margin-right: auto;
   width: 100%;
@@ -325,8 +327,7 @@
 .form-group-footer input,
 .form-group-footer textarea {
   width: 100%;
-  padding: 10px;
-  margin-top: 5px;
+  padding: 10px 10px 10px 1rem;
   border: 1px solid var(--formText);
   box-sizing: border-box;
   color: var(--formText);
@@ -334,8 +335,32 @@
   resize: vertical;
 }
 
+.form-group-footer textarea {
+  margin-top: 10px;
+}
+
+.form-group-footer select {
+  padding: 10px 2.5rem 10px 1rem;
+  padding-inline-start: 1rem;
+  padding-inline-end: 2.5rem;
+  border: 1px solid var(--formText);
+  border-radius: 0;
+  outline: 0;
+  box-shadow: none;
+  background-color: var(--formBg);
+  -webkit-appearance: none;
+  -moz-appearance: none;
+  appearance: none;
+  box-sizing: border-box;
+  background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='rgb(136%2C%20145%2C%20164)'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpolyline%20points='6%209%2012%2015%2018%209'%20/%3E%3C/svg%3E");
+  background-repeat: no-repeat;
+  background-position: center right 0.75rem;
+  background-size: 1rem auto;
+}
+
 .form-group-footer input:focus,
-.form-group-footer textarea:focus {
+.form-group-footer textarea:focus,
+.form-group-footer select:focus {
   outline: none !important;
   border: 1px solid var(--button);
 }
@@ -355,6 +380,44 @@
   background-color: #d4888d;
 }
 
+input::placeholder,
+textarea::placeholder {
+  color: rgba(214, 214, 214, 0.6);
+  opacity: 1;
+}
+
+select option[disabled] {
+  color: rgba(214, 214, 214, 0.6);
+}
+
+select {
+  color: rgb(214, 214, 214);
+}
+
+select:invalid {
+  color: rgba(214, 214, 214, 0.6);
+}
+
+select:not(:invalid) {
+  color: rgb(214, 214, 214);
+}
+
+select option:not(:disabled) {
+  color: rgb(214, 214, 214);
+}
+
+input:disabled,
+select:disabled,
+textarea:disabled {
+  color: rgba(214, 214, 214, 0.6);
+  cursor: not-allowed;
+}
+
+button:disabled {
+  cursor: not-allowed;
+  opacity: 0.6;
+}
+
 /* FOOTER FORM END */
 
 /* APPS START */
@@ -457,3 +520,117 @@
 }
 
 /* ABOUT END */
+
+/* SECCESFULL REGISTRATION START */
+
+.registration-outcome {
+  display: flex;
+  flex-direction: column;
+  align-items: stretch;
+}
+
+.registration-outcome button {
+  font-size: 16px;
+  cursor: pointer;
+  background-color: var(--bg);
+  border-radius: 0;
+  border: 0;
+  height: 30px;
+  font-family: Hack, monospace;
+  color: #3a3a3a;
+  display: flex;
+  align-items: center;
+  justify-content: start;
+}
+
+.registration-outcome h3 {
+  margin: 0;
+  margin-bottom: 15px;
+}
+
+.registration-outcome .pass {
+  display: flex;
+}
+
+.registration-outcome svg {
+  color: var(--button);
+  margin-left: 5px;
+}
+
+.tooltip {
+  position: absolute;
+  background-color: #333;
+  color: #fff;
+  padding: 5px 10px;
+  border-radius: 4px;
+  white-space: nowrap;
+  opacity: 0;
+  visibility: hidden;
+  transition: opacity 0.3s;
+  font-size: 14px;
+}
+
+/* SECCESFULL REGISTRATION END */
+
+.reg-inputs {
+  display: grid;
+  grid-template-columns: 1fr 1fr 1fr;
+  gap: 10px;
+  margin-bottom: 10px;
+  width: 100%;
+}
+
+@media (max-width: 768px) {
+  .reg-inputs {
+    grid-template-columns: 1fr;
+    width: 100%;
+  }
+}
+
+#create-app-button {
+  display: flex;
+  align-items: center;
+  margin-top: 5px;
+}
+
+#error-message {
+  margin: 0 0 10px 0;
+  color: var(--logo);
+  display: none;
+}
+
+.animated-spinner g {
+  animation: rotate 2s linear infinite;
+  transform-origin: center center;
+}
+
+.animated-spinner circle {
+  stroke-dasharray: 75, 100;
+  stroke-dashoffset: -5;
+  animation: dash 1.5s ease-in-out infinite;
+  stroke-linecap: round;
+}
+
+@keyframes rotate {
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}
+
+@keyframes dash {
+  0% {
+    stroke-dasharray: 1, 100;
+    stroke-dashoffset: 0;
+  }
+  50% {
+    stroke-dasharray: 44.5, 100;
+    stroke-dashoffset: -17.5;
+  }
+  100% {
+    stroke-dasharray: 44.5, 100;
+    stroke-dashoffset: -62;
+  }
+}