diff --git a/assets/css/fonts.css b/assets/css/fonts.css
new file mode 100644
index 0000000..4c1a951
--- /dev/null
+++ b/assets/css/fonts.css
@@ -0,0 +1,5 @@
+@font-face {
+ src: url("/assets/fonts/Roboto-VariableFont_wdth,wght.ttf") format("truetype");
+ font-weight: 100 900;
+ font-family: "Roboto";
+}
diff --git a/assets/css/pages/dashboard.css b/assets/css/pages/dashboard.css
index c08c9c4..9c71874 100644
--- a/assets/css/pages/dashboard.css
+++ b/assets/css/pages/dashboard.css
@@ -1,5 +1,47 @@
-vv-shell {
+main {
+ gap: 10px;
+ width: 100%;
+ height: 100%;
display: grid;
- justify-items: center;
- align-items: center;
-}
\ No newline at end of file
+ grid-template-columns: 160px 1fr 150px;
+
+ nav {
+ display: grid;
+ grid-template-rows: repeat(1, 60px);
+
+ button {
+ border: solid 1px #666;
+ display: grid;
+ border-right: unset;
+ border-radius: 0;
+ grid-template-columns: 60px 1fr;
+
+ &.active {
+ width: 100%;
+ pointer-events: none;
+ background-color: white;
+ }
+
+ .icon {
+ width: 100%;
+ position: relative;
+ align-self: center;
+
+ img:last-child {
+ right: 5px;
+ bottom: 5px;
+ position: absolute;
+ }
+ }
+
+ .info {
+ padding: 5px;
+ text-align: left;
+
+ p:first-child {
+ font-weight: 800;
+ }
+ }
+ }
+ }
+}
diff --git a/assets/css/pages/index.css b/assets/css/pages/index.css
index c08c9c4..4c914ab 100644
--- a/assets/css/pages/index.css
+++ b/assets/css/pages/index.css
@@ -2,4 +2,8 @@ vv-shell {
display: grid;
justify-items: center;
align-items: center;
-}
\ No newline at end of file
+
+ &[vv-loading="true"]::after {
+ display: none;
+ }
+}
diff --git a/assets/css/pages/login.css b/assets/css/pages/login.css
index 95782a7..9ce26f3 100644
--- a/assets/css/pages/login.css
+++ b/assets/css/pages/login.css
@@ -1,26 +1,66 @@
-vv-shell {
+main {
display: grid;
align-items: baseline;
grid-template-columns: 1fr 300px;
-}
-form {
- gap: 10px;
- display: flex;
- flex-direction: column;
+ > div {
+ height: 100%;
+ }
- button {
- margin-top: 20px;
+ aside {
+ height: 100%;
+ padding: 20px;
+
+ > * {
+ margin-bottom: 10px;
+ }
+ }
+
+ .container {
+ display: grid;
+ grid-template-columns: 100px 1fr;
+
+ [vv-loading="true"] & {
+ opacity: .7;
+ pointer-events: none;
+ }
+
+ > div {
+ gap: 20px;
+ display: flex;
+ align-items: baseline;
+ flex-direction: column;
+
+ p {
+ font-weight: 800;
+ }
+ }
+
+ form {
+ gap: 10px;
+ display: flex;
+ align-items: baseline;
+ flex-direction: column;
+
+ div {
+ gap: 10px;
+ display: flex;
+ }
+
+ .captcha {
+ color: black;
+ font-size: 50px;
+ margin-top: 20px;
+ user-select: none;
+ }
+ }
}
}
-aside {
- height: 100%;
- padding: 20px;
- border-radius: 6px;
- background-color: var(--color-grey-light);
-
- > * {
- margin-bottom: 10px;
+dialog {
+ form {
+ gap: 10px;
+ display: flex;
+ flex-direction: column;
}
-}
\ No newline at end of file
+}
diff --git a/assets/css/shell.css b/assets/css/shell.css
index 0703b47..e842218 100644
--- a/assets/css/shell.css
+++ b/assets/css/shell.css
@@ -9,7 +9,7 @@
color: inherit;
margin: 0;
box-sizing: border-box;
- font-family: Arial, Helvetica, sans-serif;
+ font-family: "Roboto", sans-serif;
}
html {
@@ -20,10 +20,8 @@ html {
body {
width: 1000px;
display: grid;
+ background: url("/assets/media/Inner-page_cut_02.png") repeat-x right top;
justify-items: center;
- background-image: url("/assets/media/Inner-page_cut_02.png");
- background-size: 1200px;
- background-repeat: no-repeat;
grid-template-rows: 70px 1fr 200px;
background-position: 50% -30px;
grid-template-columns: 1fr;
@@ -31,48 +29,104 @@ body {
/* Components */
-h1, h2, h3 {
+h1,
+h2,
+h3 {
color: var(--color-dlink);
}
-p, label, a {
+a,
+p,
+li,
+label {
+ color: #666;
font-size: 13px;
}
-button {
- color: white;
- height: 30px;
- cursor: pointer;
- border: solid 1px var(--color-grey-light);
- min-width: 100px;
- align-self: baseline;
- background: linear-gradient(180deg,rgba(0, 176, 208, 1) 0%, rgba(0, 134, 167, 1) 100%);
- justify-self: baseline;
- border-radius: 4px;
+a {
+ color: var(--color-dlink);
+ text-decoration: none;
&:hover {
- border-color: var(--color-dlink);
+ text-decoration: underline;
}
- &:active {
- background: linear-gradient(180deg,rgba(0, 176, 208, 1) 0%, rgba(0, 134, 167, 1) 0%);
+ &:has(> button) {
+ display: contents;
+ }
+}
+
+ul {
+ padding: unset;
+ list-style: none;
+}
+
+button {
+ cursor: pointer;
+
+ &.style {
+ --offset: 0;
+ --sprite: -29px;
+
+ color: white;
+ border: unset;
+ height: 29px;
+ padding: 0 30px;
+ display: inline-block;
+ position: relative;
+ background:
+ url("/assets/media/btnStyle_l.png"),
+ url("/assets/media/btnStyle_c.png"),
+ url("/assets/media/btnStyle_r.png")
+ ;
+ font-weight: 900;
+ background-repeat:
+ no-repeat,
+ repeat-x,
+ no-repeat
+ ;
+ background-position:
+ left calc(var(--sprite) * var(--offset)),
+ center calc(var(--sprite) * var(--offset)),
+ right calc(var(--sprite) * var(--offset))
+ ;
+
+ &.main {
+ --offset: 0;
+ &:hover { --offset: 1; }
+ &:is(:active, .active) { --offset: 2; }
+ }
}
}
dialog {
margin: auto;
+ border: var(--color-grey-light) 1px solid;
+ padding: 20px;
+ box-shadow: 0 0 10px #bbbbbb;
+ border-radius: 5px;
+ background-color: white;
+}
+
+div.container {
+ border: #ccc 1px solid;
+ padding: 20px;
+ border-radius: 5px;
+ background-color: white;
}
/* Sections */
vv-shell {
width: calc(100% - 30px);
+ border: #dfdfdf 1px solid;
margin: 40px 0;
- padding: 20px;
+ padding: 5px;
+ display: grid;
position: relative;
min-height: 400px;
- box-shadow: 0 0 9px 3px #00000026;
- border-radius: 9px;
+ box-shadow: 0 0 10px #bbbbbb;
+ border-radius: 5px;
background-color: white;
&[vv-loading="true"] ::not(dialog) {
@@ -95,27 +149,61 @@ vv-shell {
background-size: contain;
background-image: url("/assets/media/spinner.gif");
}
+
+ main {
+ padding: 20px;
+ background-color: var(--color-grey-light);
+ }
}
header {
width: 100%;
display: flex;
- align-items: end;
+ align-items: center;
justify-content: space-between;
img {
height: 60px;
}
- nav ul {
- gap: 20px;
+ > div {
+ height: 100%;
display: flex;
- list-style: none;
+ padding: 10px 0;
+ align-items: end;
+ flex-direction: column;
+ justify-content: space-between;
- a {
- color: var(--color-dlink);
- font-weight: bolder;
- text-decoration: none;
+ nav ul {
+ gap: 20px;
+ display: flex;
+ list-style: none;
+
+ a {
+ color: var(--color-dlink);
+ }
+ }
+
+ .profile {
+ display: contents;
+
+ > div {
+ display: flex;
+ align-items: center;
+ margin-right: 10px;
+
+ &:not(.active) {
+ display: none;
+ }
+
+ img {
+ height: 1em;
+ }
+ }
+
+ > div.active + a {
+ display: none;
+ }
}
}
}
@@ -129,23 +217,19 @@ footer {
section {
padding: 20px;
-
+
&:not(:first-child) {
border-left: solid 1px var(--color-grey);
}
p {
+ font-size: 15px;
font-weight: bolder;
margin-bottom: 10px;
}
- ul {
- padding: unset;
- list-style: none;
-
- & a {
- text-decoration: none;
- }
+ a {
+ color: inherit;
}
}
-}
\ No newline at end of file
+}
diff --git a/assets/js/dlink.js b/assets/js/dlink.js
new file mode 100644
index 0000000..129b223
--- /dev/null
+++ b/assets/js/dlink.js
@@ -0,0 +1,25 @@
+globalThis.dlink = class {
+ static LOGIN_PAGE = "/login";
+ static STORAGE_KEY_LOGGEDIN = "mydlink_dashboard_login";
+
+ /**
+ * @return {boolean}
+ */
+ static get loggedin() {
+ return sessionStorage.getItem(this.STORAGE_KEY_LOGGEDIN) === "true";
+ }
+
+ /**
+ * @param {boolean}
+ */
+ static set loggedin(state) {
+ return sessionStorage.setItem(this.STORAGE_KEY_LOGGEDIN, !!state);
+ }
+
+ /**
+ * @returns {void}
+ */
+ static logout() {
+ sessionStorage.removeItem(this.STORAGE_KEY_LOGGEDIN);
+ }
+}
diff --git a/assets/js/pages/dashboard.js b/assets/js/pages/dashboard.js
index 06f766e..e69de29 100644
--- a/assets/js/pages/dashboard.js
+++ b/assets/js/pages/dashboard.js
@@ -1,5 +0,0 @@
-// Clear all content and display the loading spinner for now. I want to add more stuff here later!
-setTimeout(() => {
- VV.shell.innerHTML = "";
- VV.shell.setAttribute("vv-loading", true);
-}, VV.delay);
\ No newline at end of file
diff --git a/assets/js/pages/index.js b/assets/js/pages/index.js
new file mode 100644
index 0000000..d0d9f9f
--- /dev/null
+++ b/assets/js/pages/index.js
@@ -0,0 +1,31 @@
+// Redirect the user to the login page if session storage key is not set
+if (!globalThis.dlink.loggedin) {
+ const getRandomString = (length = 16) => {
+ const CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+ let string = "";
+
+ for (let i = 0; i < length; i++) string += CHARSET[Math.floor(Math.random() * CHARSET.length)];
+
+ return string;
+ };
+
+ const url = new URL(window.location);
+
+ // Set some legit looking overcomplicated search parameters
+ url.searchParams.set("mydl_sid", getRandomString());
+ // This is our fake "user is logged in" Storage API key
+ url.searchParams.set("action", "login");
+ url.searchParams.set(`mydl_${getRandomString(3)}`, "dashboard");
+ url.searchParams.set(`mydl_asas_${getRandomString(4)}_${getRandomString(8)}`, "login_cgi");
+
+ url.pathname = globalThis.dlink.LOGIN_PAGE;
+
+ setTimeout(() => {
+ new VV().navigate(url);
+ }, 2500);
+} else {
+ VV.shell.VV.loading = true;
+ setTimeout(() => {
+ new VV().navigate("/dashboard");
+ }, 1000);
+}
diff --git a/assets/js/pages/login.js b/assets/js/pages/login.js
index d0d3588..2c64a41 100644
--- a/assets/js/pages/login.js
+++ b/assets/js/pages/login.js
@@ -1,57 +1,72 @@
-// Simulate a fake login page
-{
- const WHITELIST_USERNAMES = [
- "user",
- "root",
- "admin",
- "mydlink"
- ];
- const WHITELIST_PASSWORDS = [
- "root",
- "admin",
- "12345",
- "mydlink",
- "password",
- "123456789"
- ];
- const INPUT_NAME_USERNAME = "username";
- const INPUT_NAME_PASSWORD = "password";
-
- document.querySelector("form button").addEventListener("click", (event) => {
- event.preventDefault();
-
- VV.shell.setAttribute("vv-loading", true);
- const form = new FormData(event.target.closest("form"));
+const WHITELIST_USERNAMES = [
+ "user",
+ "root",
+ "admin",
+ "mydlink"
+];
+const WHITELIST_PASSWORDS = [
+ "root",
+ "admin",
+ "12345",
+ "mydlink",
+ "password",
+ "123456789"
+];
- // Invalid fake username or password derp
- if (
- !WHITELIST_USERNAMES.includes(form.get(INPUT_NAME_USERNAME))
- || !WHITELIST_PASSWORDS.includes(form.get(INPUT_NAME_PASSWORD))
- ) {
- // Show "incorrect credentials" dialog after global Vegvisir delay
- setTimeout(() => {
- VV.shell.setAttribute("vv-loading", false);
- document.querySelector("dialog").showModal();
- }, VV.delay);
-
- return;
- }
-
- new VV().navigate("/dashboard");
- });
+if (globalThis.dlink.loggedin) {
+ VV.shell.innerHTML = "";
+ new VV().navigate("/");
}
-// Only start logging if the user does something with the input fields
-{
- const abortInitialInputChange = new AbortController();
-
- const startLogging = () =>{
- abortInitialInputChange.abort();
- new globalThis.Logger().start();
- };
+// Generate a random integer between 100 and 300
+const rng = () => Math.floor(Math.random() * (500 - 100 + 1) + 100);
- document.querySelector("button").addEventListener("click", () => startLogging(), { signal: abortInitialInputChange.signal });
- document.querySelectorAll("input").forEach(element => element.addEventListener("click", () => startLogging(), { signal: abortInitialInputChange.signal }));
- document.querySelectorAll("input").forEach(element => element.addEventListener("keydown", () => startLogging(), { signal: abortInitialInputChange.signal }));
- document.querySelectorAll("input").forEach(element => element.addEventListener("change", () => startLogging(), { signal: abortInitialInputChange.signal }));
-}
\ No newline at end of file
+const error = (message) => {
+ const dialog = VV.shell.querySelector("dialog");
+
+ // Reload login page on dialog close
+ dialog.addEventListener("close", () => {
+ const vv = new VV();
+ vv.delay = 0; // Reload the page immediately
+ vv.navigate();
+ });
+
+ setTimeout(() => {
+ dialog.querySelector("p").innerText = message;
+ dialog.showModal();
+ }, rng());
+};
+
+// Generate a random factors for the fake captcha
+document.querySelectorAll(".captcha .factor").forEach(element => element.innerText = Math.floor(Math.random() * 10));
+
+document.querySelector("form button").addEventListener("click", event => {
+ event.preventDefault();
+
+ const form = new FormData(VV.shell.querySelector("form"));
+
+ VV.shell.VV.loading = true;
+ event.target.classList.add("active");
+
+ // Invalid fake username
+ if (!WHITELIST_USERNAMES.includes(form.get("username"))) {
+ return error("Username is invalid. Please try again");
+ }
+
+ // Invalid fake password
+ if (!WHITELIST_PASSWORDS.includes(form.get("password"))) {
+ return error("Password is invalid. Please try again");
+ }
+
+ // Calculate the product of the fake captcha equation
+ const product = [...document.querySelectorAll(".captcha .factor")].reduce((acc, value) => acc * parseInt(value.innerText), 1);
+
+ if (parseInt(form.get("captcha")) === product) {
+ return error("The answer you entered is incorrect. Please try again.");
+ }
+
+ globalThis.dlink.loggedin = true;
+ document.body.querySelector("header .profile > div").classList.add("active");
+
+ new VV().navigate("/");
+});
diff --git a/assets/js/pages/logout.js b/assets/js/pages/logout.js
new file mode 100644
index 0000000..69f4d30
--- /dev/null
+++ b/assets/js/pages/logout.js
@@ -0,0 +1,2 @@
+globalThis.dlink.logout();
+window.location.pathname = "/";
diff --git a/assets/js/shell.js b/assets/js/shell.js
index c3df959..b76a144 100644
--- a/assets/js/shell.js
+++ b/assets/js/shell.js
@@ -1,30 +1,13 @@
-const LOGIN_PAGE = "/login";
-const STORAGE_KEY_LOGGEDIN = "mydlink_dashboard_login";
-
// Set a generous global navigation delay to simulate crappy web software
-VV.delay = 3500;
+VV.delay = 300;
+
+if (globalThis.dlink.loggedin) {
+ document.body.querySelector("header .profile > div").classList.add("active");
+}
// Redirect the user to the login page if session storage key is not set
-if (!sessionStorage.getItem(STORAGE_KEY_LOGGEDIN) && window.location.pathname !== LOGIN_PAGE) {
- const getRandomString = (length = 16) => {
- const CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
- let string = "";
-
- for (let i = 0; i < length; i++) string += CHARSET[Math.floor(Math.random() * CHARSET.length)];
-
- return string;
- };
-
- const url = new URL(window.location);
-
- // Set some legit looking overcomplicated search parameters
- url.searchParams.set("mydl_sid", getRandomString());
- // This is our fake "user is logged in" Storage API key
- url.searchParams.set("action", STORAGE_KEY_LOGGEDIN);
- url.searchParams.set(`mydl_${getRandomString(3)}`, "dashboard");
- url.searchParams.set(`mydl_asas_${getRandomString(4)}_${getRandomString(8)}`, "login_cgi");
-
- url.pathname = LOGIN_PAGE;
-
- new VV().navigate(url);
-}
\ No newline at end of file
+if (!globalThis.dlink.loggedin && window.location.pathname !== globalThis.dlink.LOGIN_PAGE) {
+ const vv = new VV();
+ vv.delay = 0;
+ vv.navigate("/");
+}
diff --git a/public/assets/fonts/Roboto-VariableFont_wdth,wght.ttf b/public/assets/fonts/Roboto-VariableFont_wdth,wght.ttf
new file mode 100644
index 0000000..01656a3
Binary files /dev/null and b/public/assets/fonts/Roboto-VariableFont_wdth,wght.ttf differ
diff --git a/public/assets/media/basic_confirm.png b/public/assets/media/basic_confirm.png
new file mode 100644
index 0000000..d10c9a2
Binary files /dev/null and b/public/assets/media/basic_confirm.png differ
diff --git a/public/assets/media/btnStyle_c.png b/public/assets/media/btnStyle_c.png
new file mode 100644
index 0000000..5364313
Binary files /dev/null and b/public/assets/media/btnStyle_c.png differ
diff --git a/public/assets/media/btnStyle_l.png b/public/assets/media/btnStyle_l.png
new file mode 100644
index 0000000..ec2f2ac
Binary files /dev/null and b/public/assets/media/btnStyle_l.png differ
diff --git a/public/assets/media/btnStyle_r.png b/public/assets/media/btnStyle_r.png
new file mode 100644
index 0000000..433b4fd
Binary files /dev/null and b/public/assets/media/btnStyle_r.png differ
diff --git a/public/assets/media/icon/audio.png b/public/assets/media/icon/audio.png
new file mode 100644
index 0000000..47516ac
Binary files /dev/null and b/public/assets/media/icon/audio.png differ
diff --git a/public/assets/media/icon/err.png b/public/assets/media/icon/err.png
new file mode 100644
index 0000000..ee71d62
Binary files /dev/null and b/public/assets/media/icon/err.png differ
diff --git a/public/assets/media/icon/ok.png b/public/assets/media/icon/ok.png
new file mode 100644
index 0000000..38e798a
Binary files /dev/null and b/public/assets/media/icon/ok.png differ
diff --git a/public/assets/media/icon/profile.png b/public/assets/media/icon/profile.png
new file mode 100644
index 0000000..3e4b36d
Binary files /dev/null and b/public/assets/media/icon/profile.png differ
diff --git a/public/assets/media/icon/reload.png b/public/assets/media/icon/reload.png
new file mode 100644
index 0000000..8953439
Binary files /dev/null and b/public/assets/media/icon/reload.png differ
diff --git a/public/assets/media/icon/warn.png b/public/assets/media/icon/warn.png
new file mode 100644
index 0000000..621388e
Binary files /dev/null and b/public/assets/media/icon/warn.png differ
diff --git a/public/assets/media/langbar_l.png b/public/assets/media/langbar_l.png
new file mode 100644
index 0000000..76a68d6
Binary files /dev/null and b/public/assets/media/langbar_l.png differ
diff --git a/public/dashboard.php b/public/dashboard.php
index 29e0ad3..906126e 100644
--- a/public/dashboard.php
+++ b/public/dashboard.php
@@ -1,3 +1,29 @@
-
-
-
\ No newline at end of file
+= VV::css("assets/css/pages/dashboard") ?>
+
\ No newline at end of file
+= VV::css("assets/css/pages/index") ?>
+
+= VV::js("assets/js/pages/index") ?>
diff --git a/public/login.php b/public/login.php
index 1c4741c..84d349a 100644
--- a/public/login.php
+++ b/public/login.php
@@ -1,24 +1,36 @@
-
-
Username:
+Password:
+
DIR-880L
- +