summaryrefslogtreecommitdiff
path: root/app/public
diff options
context:
space:
mode:
authorAlexis Hovorka <[email protected]>2024-02-16 23:48:02 -0700
committerAlexis Hovorka <[email protected]>2024-02-16 23:48:02 -0700
commitc5aa364e0e372d0e29063a27bd17811446db8b6a (patch)
tree5c5b10471f7356b66b74277071e61e5e0447edc5 /app/public
parent773e7f747fa096031be427b22257137cf894d587 (diff)
[feat] Flesh out basic UI
Diffstat (limited to 'app/public')
-rw-r--r--app/public/icons.svg1
-rw-r--r--app/public/index.html71
-rw-r--r--app/public/logo-icon.svg96
-rw-r--r--app/public/main.js119
-rw-r--r--app/public/style.css414
5 files changed, 653 insertions, 48 deletions
diff --git a/app/public/icons.svg b/app/public/icons.svg
new file mode 100644
index 0000000..47e13fb
--- /dev/null
+++ b/app/public/icons.svg
@@ -0,0 +1 @@
+<svg width="96" height="72" viewBox="0 0 25.4 19.05" xmlns="http://www.w3.org/2000/svg"><g style="fill:#000;stroke-width:2"><g style="stroke-width:2"><path fill="none" style="stroke-width:4" d="M0 0h24v24H0z" transform="matrix(.26458 0 0 .26458 6.35 0)"/><path style="fill:#1ab21a;fill-opacity:1;stroke-width:2" d="M7.333 6h9.333C17.4 6 18 6.6 18 7.334v9.333C18 17.4 17.4 18 16.666 18H7.333C6.6 18 6 17.4 6 16.666V7.333c0-.733.6-1.332 1.333-1.332zM5 3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z" transform="matrix(.26458 0 0 .26458 6.35 0)"/></g></g><g style="fill:#000;stroke-width:2"><path d="M0 0h24v24H0Z" fill="none" style="stroke-width:4" transform="matrix(.26458 0 0 .26458 12.7 0)"/><path d="M15.824 13.676 14.71 14l-.28-.27a6.5 6.5 0 0 0 1.48-5.34c-.47-2.78-2.79-5-5.59-5.34a6.505 6.505 0 0 0-7.27 7.27c.34 2.8 2.56 5.12 5.34 5.59a6.5 6.5 0 0 0 5.34-1.48l.27.28-.324 1.114 4.905 4.906c.41.41 1.225.265 1.812-.322s.732-1.402.322-1.812zM9.5 12.989A3.484 3.484 0 0 1 6.011 9.5 3.484 3.484 0 0 1 9.5 6.011 3.484 3.484 0 0 1 12.989 9.5 3.484 3.484 0 0 1 9.5 12.989z" style="fill:#1565c0;fill-opacity:1;stroke-width:2" transform="matrix(.26458 0 0 .26458 12.7 0)"/></g><g style="fill:#000;stroke-width:2"><path d="M0 0h24v24H0Z" fill="none" style="stroke-width:4" transform="matrix(.26458 0 0 .26458 19.05 0)"/><g style="opacity:1"><path style="color:#000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000;solid-opacity:1;vector-effect:none;fill:#fcc11c;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000;stop-opacity:1" d="M12 3c-1.095 0-2 .905-2 2v5H5c-1.095 0-2 .905-2 2s.905 2 2 2h5v5c0 1.095.905 2 2 2s2-.905 2-2v-5h5c1.095 0 2-.905 2-2s-.905-2-2-2h-5V5c0-1.095-.905-2-2-2Z" transform="matrix(.26458 0 0 .26458 19.05 0)"/></g></g><g style="fill:#000;stroke-width:2"><path d="M0 0h24v24H0Z" fill="none" style="stroke-width:4" transform="matrix(.26458 0 0 .26458 0 6.35)"/><path d="M3 17.46v3.04c0 .28.22.5.5.5h3.04c.13 0 .26-.05.35-.15L17.81 9.94l-3.75-3.75L3.15 17.1c-.1.1-.15.22-.15.36ZM20.71 7.04a.996.996 0 0 0 0-1.41l-2.34-2.34a.996.996 0 0 0-1.41 0l-1.83 1.83 3.75 3.75z" style="fill:red;stroke-width:2" transform="matrix(.26458 0 0 .26458 0 6.35)"/></g><g style="fill:#000;stroke-width:2"><path d="M0 0h24v24H0Z" fill="none" style="stroke-width:4" transform="matrix(.26458 0 0 .26458 19.05 6.35)"/><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" style="fill:red;stroke-width:2" transform="matrix(.26458 0 0 .26458 19.05 6.35)"/></g><g style="fill:#000;stroke-width:2"><path d="M0 0h24v24H0Z" fill="none" style="stroke-width:4" transform="matrix(.26458 0 0 .26458 6.35 6.35)"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z" style="fill:red;stroke-width:2" transform="matrix(.26458 0 0 .26458 6.35 6.35)"/></g><g style="fill:#000;stroke-width:2"><g style="stroke-width:2" fill="none"><path style="stroke-width:4" d="M0 0h24v24H0z" transform="matrix(.26458 0 0 .26458 12.7 6.35)"/><path style="stroke-width:4" d="M0 0h24v24H0z" transform="matrix(.26458 0 0 .26458 12.7 6.35)"/></g><g style="stroke-width:2"><path d="M19 12.87c0-.47-.34-.85-.8-.98A2.997 2.997 0 0 1 16 9V4h1c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1 .45-1 1s.45 1 1 1h1v5c0 1.38-.93 2.54-2.2 2.89-.46.13-.8.51-.8.98V13c0 .55.45 1 1 1h4.98l.02 7c0 .55.45 1 1 1s1-.45 1-1l-.02-7H18c.55 0 1-.45 1-1z" fill-rule="evenodd" style="fill:red;stroke-width:4" transform="matrix(.26458 0 0 .26458 12.7 6.35)"/></g></g><path style="fill:#fff;fill-opacity:1;stroke:none;stroke-width:.264999;stroke-linecap:round;stroke-linejoin:round" d="M12.196 179.652a.53.53 0 0 0-.517.53.53.53 0 0 0 .53.528h3.704a.53.53 0 0 0 .529-.529.53.53 0 0 0-.53-.529H12.21a.53.53 0 0 0-.013 0zm0 1.852a.53.53 0 0 0-.517.53.53.53 0 0 0 .53.529h3.704a.53.53 0 0 0 .529-.53.53.53 0 0 0-.53-.529H12.21a.53.53 0 0 0-.013 0zm0 1.852a.53.53 0 0 0-.517.53.53.53 0 0 0 .53.529h3.704a.53.53 0 0 0 .529-.53.53.53 0 0 0-.53-.529H12.21a.53.53 0 0 0-.013 0z" transform="translate(-10.886 -178.858)"/><g style="fill:#000;stroke-width:2"><path d="M0 0h24v24H0Z" fill="none" style="stroke-width:2" transform="matrix(.26458 0 0 .26458 0 12.7)"/><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2Zm-8.29 13.29a.996.996 0 0 1-1.41 0L5.71 12.7a.996.996 0 1 1 1.41-1.41L10 14.17l6.88-6.88a.996.996 0 1 1 1.41 1.41z" style="stroke-width:2;fill:#1565c0;fill-opacity:1" transform="matrix(.26458 0 0 .26458 0 12.7)"/></g><g style="fill:#000;stroke-width:2"><path d="M0 0h24v24H0Z" fill="none" style="stroke-width:2" transform="matrix(.26458 0 0 .26458 6.35 12.7)"/><path d="M18 19H6c-.55 0-1-.45-1-1V6c0-.55.45-1 1-1h12c.55 0 1 .45 1 1v12c0 .55-.45 1-1 1zm1-16H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2Z" style="stroke-width:2;fill:red" transform="matrix(.26458 0 0 .26458 6.35 12.7)"/></g></svg> \ No newline at end of file
diff --git a/app/public/index.html b/app/public/index.html
index c7b44c7..593e6b3 100644
--- a/app/public/index.html
+++ b/app/public/index.html
@@ -56,26 +56,59 @@
<link rel="stylesheet" href="style.css">
</head>
<body>
- <h1>Notes</h1>
+ <dialog id="sign-in-dialog">
+ <form id="sign-in-form" class="card" method="dialog">
+ <h1 class="logo"><img src="logo-icon.svg" alt="" class="logo-icon"> Notes</h1>
+ <p id="sign-in-error" class="error-message"></p>
+ <div>
+ <input type="text" id="sign-in-username" name="username" placeholder="Username" autocomplete="username" autocorrect="off" autocapitalize="off" spellcheck="false" required autofocus>
+ <input type="password" id="sign-in-password" name="password" placeholder="Password" autocomplete="current-password" autocorrect="off" autocapitalize="off" spellcheck="false" required>
+ <label>
+ <input type="checkbox" id="sign-in-keep-session" name="keepSession">
+ <span class="checkbox-icon"></span> Keep me signed in
+ </label>
+ <button id="sign-in-basic-go"><span class="loading-spinner"></span>Sign in</button>
+ </div>
+ <!--div>
+ <input type="text" inputmode="numeric" id="sign-in-otp" name="otp" placeholder="OTP Code" autocomplete="one-time-code" autocorrect="off" autocapitalize="off" spellcheck="false" pattern="\d{6,6}" required>
+ <button id="sign-in-otp-go">Sign in</button>
+ </div>
+ <div>
+ <input type="password" id="sign-in-new-password" name="newPassword" placeholder="New password" autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" minlength=8 maxlength=512 required>
+ <input type="password" id="sign-in-new-password-confirm" name="newPasswordConfirm" placeholder="Confirm new password" autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" minlength=8 maxlength=512 required>
+ <button id="sign-in-change-password-go">Change password</button>
+ </div-->
+ </form>
+ </dialog>
- <form>
- <input type="text" id="username" name="username">
- <input type="password" id="password" name="password">
- <input type="checkbox" id="keep-session" name="keepSession">
- <button id="login">Log In</button>
- </form>
- <br>
- <input type="password" id="new-password" name="newPassword">
- <button id="change-password">Change Password</button>
- <br>
- <input type="text" id="new-username" name="newUsername">
- <button id="change-username">Change Username</button>
- <br>
- <input type="text" id="uid" name="uid">
- <button id="logout">Log Out</button>
- <button id="logout-everywhere">Log Out Everywhere</button>
- <div id="notes"></div>
- <button id="new-note">New Note</button>
+ <div id="menu" class="card-shadow">
+ <div class="card">
+ <input type="password" id="new-password" name="newPassword" placeholder="New password" autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false">
+ <button id="change-password">Change password</button>
+ <br>
+ <input type="text" id="new-username" name="newUsername" placeholder="New username" autocomplete="none" autocorrect="off" autocapitalize="off" spellcheck="false">
+ <button id="change-username">Change username</button>
+ <br>
+ <button id="log-out">Log out</button>
+ <button id="log-out-everywhere">Log out everywhere</button>
+ <br>
+ <!--input type="text" inputmode="search" id="search" name="search" placeholder="Search…" list="search-hints"-->
+ <input type="search" id="search" name="search" placeholder="Search…" list="search-hints" incremental>
+ <datalist id="search-hints">
+ <option value="#hash"></option>
+ <option value="#tag"></option>
+ </datalist>
+ </div>
+ </div>
+
+ <div id="notes" class="notes-column"></div>
+ <div id="toolbar">
+ <button class="toolbar-btn" id="menu-btn"></button>
+ <button class="toolbar-btn" id="window-btn"></button>
+ <button class="toolbar-btn" id="search-btn"></button>
+ <button class="toolbar-btn" id="conflict-btn"></button>
+ <button class="toolbar-fab" id="add-btn"></button>
+ </div>
<!--script src="sock.js"></script-->
<script src="main.js"></script>
diff --git a/app/public/logo-icon.svg b/app/public/logo-icon.svg
new file mode 100644
index 0000000..d66678f
--- /dev/null
+++ b/app/public/logo-icon.svg
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ width="96"
+ height="96"
+ viewBox="0 0 25.4 25.4"
+ version="1.1"
+ id="svg1753"
+ inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
+ sodipodi:docname="logo-icon.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <defs
+ id="defs1747" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="6.2441455"
+ inkscape:cx="123.67585"
+ inkscape:cy="47.999999"
+ inkscape:document-units="mm"
+ inkscape:current-layer="text902"
+ inkscape:document-rotation="0"
+ showgrid="false"
+ units="px"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:showpageshadow="2"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#d1d1d1"
+ inkscape:window-width="1368"
+ inkscape:window-height="890"
+ inkscape:window-x="-6"
+ inkscape:window-y="-6"
+ inkscape:window-maximized="1" />
+ <metadata
+ id="metadata1750">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(645.044,-144.5381)">
+ <rect
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round"
+ id="rect2383"
+ width="16.933332"
+ height="16.933332"
+ x="-640.81067"
+ y="148.77144" />
+ <g
+ style="fill:#000000;stroke-width:0.5"
+ id="g898"
+ transform="matrix(1.0583333,0,0,1.0583333,-645.044,144.5381)">
+ <g
+ id="g896"
+ style="stroke-width:0.5">
+ <rect
+ fill="none"
+ height="24"
+ width="24"
+ id="rect892"
+ x="0"
+ y="0"
+ style="stroke-width:0.25" />
+ <path
+ d="M 19,3 H 5 C 3.9,3 3,3.9 3,5 v 14 c 0,1.1 0.9,2 2,2 h 14 c 1.1,0 2,-0.9 2,-2 V 5 C 21,3.9 20.1,3 19,3 Z M 13,17 H 8 C 7.45,17 7,16.55 7,16 7,15.45 7.45,15 8,15 h 5 c 0.55,0 1,0.45 1,1 0,0.55 -0.45,1 -1,1 z m 3,-4 H 8 C 7.45,13 7,12.55 7,12 7,11.45 7.45,11 8,11 h 8 c 0.55,0 1,0.45 1,1 0,0.55 -0.45,1 -1,1 z M 16,9 H 8 C 7.45,9 7,8.55 7,8 7,7.45 7.45,7 8,7 h 8 c 0.55,0 1,0.45 1,1 0,0.55 -0.45,1 -1,1 z"
+ id="path894"
+ style="fill:#fbbc04;fill-opacity:1;stroke-width:0.5" />
+ </g>
+ </g>
+ <g
+ aria-label="Notes"
+ id="text902"
+ style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:14.9352px;line-height:1.25;font-family:Roboto;-inkscape-font-specification:'Roboto, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:0.87;stroke:none;stroke-width:0.264583"
+ transform="translate(-0.91684937)" />
+ </g>
+</svg>
diff --git a/app/public/main.js b/app/public/main.js
index 5dc9531..5746142 100644
--- a/app/public/main.js
+++ b/app/public/main.js
@@ -28,13 +28,25 @@ function debounce(fn, delay) {
//}
function drawNote(note) {
- const el = m(`<div>
- <h3>${note.id}</h3>
- <textarea>${note.content}</textarea>
- </div>`);
+ const el = m(`<div class="card-shadow"><div class="card">
+ <!--h3>${note.id}</h3-->
+ <textarea>${note.content.trim()}</textarea>
+ </div></div>`);
- $("textarea", el).addEventListener("input",
- debounce(() => { saveNote(note.id, $("textarea", el).value); }, 500));
+ const ta = $("textarea", el)
+
+ ta.addEventListener("input",
+ debounce(() => { saveNote(note.id, ta.value); }, 500));
+
+ function resizeTextarea() { // TODO simplify
+ el.style.height = (el.scrollHeight) + "px";
+ ta.style.height = ""; ta.style.height = (ta.scrollHeight) + "px";
+ el.style.height = "";
+ }
+ ta.addEventListener("input", resizeTextarea);
+ ta.addEventListener("focus", resizeTextarea);
+ window.addEventListener("resize", resizeTextarea);
+ setTimeout(resizeTextarea);
$("#notes").appendChild(el);
}
@@ -51,9 +63,7 @@ async function getList() {
const list = await res.json();
console.log(list);
- for (let i=0;i<list.length;i++) {
- await getNote(list[i]);
- }
+ return Promise.all(list.map(id => getNote(id)));
}
async function saveNote(id, content) {
@@ -77,13 +87,12 @@ async function newNote() {
drawNote(note);
}
-$("#new-note").addEventListener("click", newNote);
+$("#add-btn").addEventListener("click", newNote);
async function getUserData() {
const res = await fetch(`/user`);
const user = await res.json();
console.log(user);
- if (user.uid) $("#uid").value = user.uid;
}
async function getUserSessions() {
@@ -92,26 +101,75 @@ async function getUserSessions() {
console.log(sessions);
}
-$("#login").addEventListener("click", async () => {
+function showSignIn() {
+ const hash = location.hash.slice(2).replace(/^sign-in(\/to\/)?/,"");
+ history.replaceState("sign-in","","#/sign-in"+(hash?"/to/"+hash:""));
+ $("#sign-in-dialog").showModal();
+}
+
+$("#sign-in-dialog").addEventListener("cancel", e => e.preventDefault());
+$("#sign-in-username").addEventListener("input", e => e.target.classList.remove("error"));
+$("#sign-in-password").addEventListener("input", e => e.target.classList.remove("error"));
+$("#sign-in-basic-go").addEventListener("click", async e => {
+ e.preventDefault();
+ $("#sign-in-error").textContent = "";
+
+ const inputs = [
+ $("#sign-in-username"),
+ $("#sign-in-password"),
+ $("#sign-in-keep-session"),
+ $("#sign-in-basic-go"),
+ ];
+ const validatedInputs = inputs.slice(0,2);
+
+ if (validatedInputs.map(e => {
+ if (!e.checkValidity()) {
+ e.classList.add("error");
+ return true; }
+ }).reduce((a,e) => a||e, false)) return;
+
+ inputs.forEach(e => e.disabled = true);
+ $("#sign-in-form").classList.add("loading");
+
const res = await fetch("/login", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
- username: $("#username").value,
- password: $("#password").value,
- keepSession: $("#keep-session").checked,
+ username: $("#sign-in-username").value,
+ password: $("#sign-in-password").value,
+ keepSession: $("#sign-in-keep-session").checked,
})
});
- // TODO check return code
+ inputs.forEach(e => e.disabled = false);
+ $("#sign-in-form").classList.remove("loading");
+ if (!res.ok) {
+ $("#sign-in-error").textContent = res.status+" "+res.statusText;
+ return;
+ }
const json = await res.json();
console.log(json);
- // TODO "Must Change Password" flow
+ if (!json.success) {
+ $("#sign-in-error").textContent = json.msg;
+ return;
+ }
+
+ if (json.mustChangePassword) {
+ alert("Must change password (TODO)");
+ // TODO
+ return;
+ }
+
+ const hash = location.hash.slice(9);
+ history.replaceState("","","#/"+hash);
+ // TODO load location
getUserData();
getList();
+
+ $("#sign-in-dialog").close();
});
$("#change-password").addEventListener("click", async () => {
@@ -119,8 +177,8 @@ $("#change-password").addEventListener("click", async () => {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
- username: $("#username").value,
- password: $("#password").value,
+ username: $("#username").value, // TODO
+ password: $("#password").value, // TODO
newPassword: $("#new-password").value,
keepSession: $("#keep-session").checked,
})
@@ -138,7 +196,7 @@ $("#change-username").addEventListener("click", async () => {
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
newUsername: $("#new-username").value,
- password: $("#password").value,
+ password: $("#password").value, // TODO
})
});
@@ -148,7 +206,7 @@ $("#change-username").addEventListener("click", async () => {
getUserData();
});
-$("#logout").addEventListener("click", async () => {
+$("#log-out").addEventListener("click", async () => {
const res = await fetch("/logout", {
method: "POST",
});
@@ -156,19 +214,28 @@ $("#logout").addEventListener("click", async () => {
//const json = await res.json();
//console.log(json);
console.log("Logged out");
- $("#uid").value = "";
+ $("#sign-in-dialog").showModal();
});
-$("#logout-everywhere").addEventListener("click", async () => {
+$("#log-out-everywhere").addEventListener("click", async () => {
const res = await fetch("/deauth-all", {
method: "POST",
});
console.log("Logged out everywhere");
- $("#uid").value = "";
+ showSignIn();
});
-try { getUserData(); getList(); getUserSessions(); } catch(e) {}
+try { await getUserData(); await getList(); await getUserSessions(); } catch(e) { showSignIn(); }
+
+(() => {
+ let prevScroll = 0;
+ window.addEventListener("scroll", () => {
+ document.querySelector("#toolbar").classList[
+ (window.scrollY && window.scrollY > prevScroll)?"add":"remove"]("hide");
+ prevScroll = window.scrollY;
+ });
+})();
// TODO make sure to save unsynced delta even if token has expired, then sync when logged in
// TODO Cookie consent on signup; "only necessary for maintaining user session and cached data"
@@ -190,4 +257,6 @@ try { getUserData(); getList(); getUserSessions(); } catch(e) {}
// https://github.com/cure53/DOMPurify/blob/main/src/attrs.js
// https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
+// TODO note kebab menu could be a <dialog> element?
+
});
diff --git a/app/public/style.css b/app/public/style.css
index 655e181..e7dd049 100644
--- a/app/public/style.css
+++ b/app/public/style.css
@@ -3,12 +3,418 @@
padding: 0;
box-sizing: border-box;
font-family: "Roboto", "Noto Sans", sans-serif;
- transition-timing-function: ease-in-out;
+ transition-timing-function: cubic-bezier(.4,0,.2,1);
}
-body {
- user-select: none;
- -webkit-user-select: none;
+/* TODO
+ - normalize text size
+ - put all colors in variables
+ - checklists using ::marker?
+ - links
+ - CSS scroll snap for columns
+ - https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_scroll_snap
+ - box-decoration-break: clone; -webkit-box-decoration-break: clone;
+ - overscroll-behavior: contain;
+ - user-select: text;
+*/
+
+:root, ::backdrop {
+ --bg-color: #FFF;
+ --page-bg-color: #EEE;
+ --text-color: rgba(0,0,0,.87);
+ --error-triple: 176, 0, 32;
+ --error-color: rgb(var(--error-triple));
+ --icon-opacity-active: .87;
+ --icon-opacity-inactive: .6;
+ --icon-opacity-disabled: .38;
+ --icon-inactive-filter: brightness(0);
+ --icon-checkbox-active-filter: brightness(1);
+ --icon-add-hover-filter: brightness(1);
+ --icon-menu-hover-filter: brightness(1);
+ --icon-search-hover-filter: brightness(1);
+ --icon-window-hover-filter: brightness(1);
+ --menu-button-hover-bg-color: #FBBC04;
+ --button-hover-bg-color: #FFF;
+ --button-hover-shadow: 0 2px 16px rgba(0,0,0,.1), 0 2px 4px rgba(0,0,0,.2);
+ --fab-bg: #FFF;
+ --fab-shadow: 0 2px 12px rgba(0,0,0,.15), 0 2px 6px rgba(0,0,0,.2);
+ --fab-shadow-hover: 0 4px 16px rgba(0,0,0,.2), 0 3px 6px rgba(0,0,0,.3);
+ --toolbar-shadow-mobile: 0 8px 8px 8px rgba(0,0,0,.2);
+ --toolbar-hide-distance-mobile: calc(-56px - 8px);
+ --toolbar-button-hover-bg-mobile: rgba(0,0,0,.1);
+}
+
+/*@media (prefers-color-scheme: dark) { :root, ::backdrop {
+ --bg-color: #000;
+ --page-bg-color: #000;
+ --text-color: rgba(255,255,255,.87);
+ --error-triple: 245, 0, 45;
+ --error-color: rgb(var(--error-triple));
+ --icon-opacity-active: .87;
+ --icon-opacity-inactive: .6;
+ --icon-opacity-disabled: .38;
+ --icon-inactive-filter: brightness(0) invert(1) hue-rotate(180deg);
+ --icon-add-hover-filter: brightness(1.4) invert(.1) hue-rotate(0);
+ --icon-menu-hover-filter: brightness(1) invert(1) hue-rotate(180deg);
+ --icon-search-hover-filter: brightness(1) invert(1) hue-rotate(180deg);
+ --icon-window-hover-filter: brightness(.75) invert(1) hue-rotate(180deg);
+ --menu-button-hover-bg-color: #FBBC04;
+ --button-hover-bg-color: #181818;
+ --button-hover-shadow: 0 0 0 transparent;
+ --fab-bg: #333;
+ --fab-shadow: 0 2px 12px rgba(0,0,0,.2), 0 2px 6px rgba(0,0,0,.24);
+ --fab-shadow-hover: 0 2px 12px rgba(0,0,0,.2), 0 2px 6px rgba(0,0,0,.24);
+ --toolbar-shadow-mobile: 0 8px 8px 8px #000, 0 56px 56px -56px rgba(255,255,255,.25) inset;
+ --toolbar-hide-distance-mobile: calc(-56px - 8px);
+ --toolbar-button-hover-bg-mobile: rgba(255,255,255,.1);
+}}*/
+
+html {
+ background: var(--page-bg-color);
+ color: var(--text-color);
+ overscroll-behavior: contain;
+}
+
+body, dialog { /* TODO */
+ user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
}
+
+#toolbar { position: fixed; font-size: 0; }
+#toolbar > * { font-size: 16px; text-align: center; }
+.toolbar-btn { height: 48px; width: 48px; }
+.toolbar-btn, .toolbar-fab {
+ position: relative;
+ border-radius: 100%;
+ background: transparent;
+ border: none;
+ outline: none;
+}
+
+.toolbar-btn:not(#conflict-btn)::after, .toolbar-fab::after { content: ""; display: block; height: 24px; width: 24px; background-image: url("icons.svg"); filter: var(--icon-inactive-filter); opacity: var(--icon-opacity-active); position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); }
+#menu-btn::after { background-position: 0 0; }
+#window-btn::after { background-position: -24px 0; }
+#search-btn::after { background-position: -48px 0; }
+#add-btn::after { background-position: -72px 0; }
+
+#conflict-btn { display: none; vertical-align: bottom; }
+#conflict-btn::after { content: ""; position: absolute; top: 50%; left: 50%; display: block; height: 8px; width: 8px; border-radius: 4px; background: rgba(var(--error-triple), .8); transform: translate(-50%,-50%); }
+
+@media (hover:hover) {
+ .toolbar-btn, .toolbar-fab { cursor: pointer; transition: background-color .2s, box-shadow .2s; }
+ .toolbar-btn:not(#conflict-btn)::after, .toolbar-fab::after { transition: opacity .1s, filter .1s; filter: var(--icon-inactive-filter); }
+ .toolbar-btn:not(#conflict-btn):hover::after, .toolbar-fab:hover::after { opacity: 1; }
+
+ #menu-btn:hover::after { filter: var(--icon-menu-hover-filter); }
+ #window-btn:hover::after { filter: var(--icon-window-hover-filter); }
+ #search-btn:hover::after { filter: var(--icon-search-hover-filter); }
+ #add-btn:hover::after { filter: var(--icon-add-hover-filter); }
+
+ #conflict-btn::after { transition: box-shadow .1s; }
+ #conflict-btn:hover::after { box-shadow: 0 0 0 4px rgba(var(--error-triple),.25); }
+}
+
+@media (hover:hover) and (min-width:601px) {
+ .notes-column { padding: 8px 80px 16px; }
+ #toolbar { top: 16px; left: 50%; transform: translate(-50%,0); width: 100%; max-width: calc(7in + 16px); pointer-events: none; }
+ #toolbar > * { pointer-events: auto; }
+ .toolbar-fab { height: 48px; width: 48px; }
+ .toolbar-btn, .toolbar-fab { display: block; margin: 0 16px; }
+ .toolbar-btn:not(#conflict-btn):hover, .toolbar-fab:hover { background-color: var(--button-hover-bg-color); box-shadow: var(--button-hover-shadow); }
+ .toolbar-btn:not(#conflict-btn)::after, .toolbar-fab::after { opacity: var(--icon-opacity-inactive) }
+ .toolbar-btn:not(#conflict-btn):hover::after, .toolbar-fab:hover::after { opacity: var(--icon-opacity-active) }
+
+ #menu-btn:hover { background-color: var(--menu-button-hover-bg-color) !important; }
+ #search-btn { position: absolute; top: 0; right: 0; }
+ #add-btn { position: absolute; top: 48px; right: 0; }
+ #conflict-btn { height: 42px; }
+ #conflict-btn.show { display: block; }
+}
+
+@media (hover:none), (max-width:600px) {
+ .notes-column { padding: 0 16px 16px; }
+ #toolbar { bottom: 0; left: 0; right: 0; width: 100%; box-shadow: var(--toolbar-shadow-mobile); padding: 0 4px; background: var(--bg-color); transition: bottom .25s; /* reentrance speed */ }
+ .toolbar-btn { display: inline-block; }
+ .toolbar-fab { position: absolute; display: block; right: 16px; top: -28px; width: 56px; height: 56px; box-shadow: var(--fab-shadow); background: var(--fab-bg); transition: top .25s; }
+ #conflict-btn { width: 36px; }
+ #conflict-btn.show { display: inline-block; }
+
+ #toolbar.hide { bottom: var(--toolbar-hide-distance-mobile); transition: bottom .2s; /* exit speed */ }
+ #toolbar.hide .toolbar-fab { top: calc(var(--toolbar-hide-distance-mobile) - 16px); transition: top .2s; }
+
+ @media (orientation:portrait) {
+ .toolbar-btn { margin: 4px 0; }
+ }
+
+ @media (orientation:landscape) {
+ .toolbar-btn { margin: 0; }
+ }
+}
+
+@media (hover:hover) and (max-width:600px) {
+ .toolbar-btn:not(#conflict-btn)::before { content: ""; display: block; position: absolute; top: 50%; left: 50%; height: 40px; width: 40px; border-radius: 20px; transform: translate(-50%,-50%); transition: background-color .2s; }
+ .toolbar-btn:not(#conflict-btn):hover::before { background-color: var(--toolbar-button-hover-bg-mobile); }
+ #menu-btn:hover::before { background-color: var(--menu-button-hover-bg-color) !important; }
+ .toolbar-fab { transition: top .25s, box-shadow .2s; }
+ #toolbar.hide .toolbar-fab { transition: top .2s, box-shadow .2s; }
+ .toolbar-fab:hover { box-shadow: var(--fab-shadow-hover); }
+ #add-btn::after { transition: opacity .15s, filter .15s, transform .2s; }
+ #add-btn:hover::after { filter: var(--icon-add-hover-filter); transform: translate(-50%,-50%) scale(1.25); }
+}
+
+.logo {
+ font-weight: 500;
+ word-spacing: -.15em;
+}
+.logo-icon {
+ vertical-align: -.45em;
+ height: 1.6em;
+}
+
+#sign-in-dialog {
+ margin: auto;
+ width: auto;
+ max-width: min(5.5in, 100vw - 32px);
+ outline: none;
+ border: none;
+ border-radius: 24px;
+ box-shadow: 0 2px 16px rgba(0,0,0,.05), 0 2px 4px rgba(0,0,0,.1);
+ transition: box-shadow .2s;
+}
+#sign-in-dialog::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ box-shadow: 0 0 0 1px rgba(255,255,255,.2) inset;
+ border-radius: 24px;
+ pointer-events: none;
+ transition: box-shadow .2s;
+}
+#sign-in-dialog::backdrop {
+ background: var(--page-bg-color);
+}
+#sign-in-form {
+ display: block;
+ padding: 16px 16px 40px;
+
+ & .logo {
+ margin: 16px 0;
+ padding: .1em .3em .1em 0;
+ font-size: min(48px, 12vw);
+ text-align: center;
+ }
+
+ & .error-message {
+ text-align: center;
+ color: var(--error-color);
+ }
+
+ & input[type=text],
+ & input[type=password],
+ & label,
+ & button {
+ display: block;
+ width: 100%;
+ max-width: 3.5in;
+ margin: 12px auto;
+ font-size: inherit;
+ letter-spacing: .3px;
+ color: var(--text-color);
+ border-radius: 16px;
+ }
+
+ & input[type=text],
+ & input[type=password],
+ & button {
+ padding: 12px 18px;
+ outline: 0;
+ }
+ & input[type=text],
+ & input[type=password] {
+ position: relative;
+ background-color: #FFF;
+ border: 1px solid #FFF;
+ border-bottom: 1px solid rgba(0,0,0,.15);
+ box-shadow: 0 2px 1px -1px rgba(0,0,0,.1);
+ transition: border .2s, box-shadow .2s, opacity .2s;
+ }
+ & input[type=text]:hover:not(:disabled),
+ & input[type=password]:hover:not(:disabled),
+ & input[type=text]:focus,
+ & input[type=password]:focus {
+ border: 1px solid rgba(0,0,0,.07);
+ border-bottom: 1px solid rgba(0,0,0,.18);
+ box-shadow: 0 2px 16px rgba(0,0,0,.05), 0 2px 4px rgba(0,0,0,.15);
+ }
+ & input[type=text]::placeholder,
+ & input[type=password]::placeholder {
+ color: rgba(0,0,0,.6);
+ }
+ & input[type=text]:autofill,
+ & input[type=password]:autofill {
+ background-color: rgb(232,240,254);
+ border: 1px solid rgb(179,207,255);
+ }
+ & input[type=text].error,
+ & input[type=password].error {
+ background-color: rgb(254,232,232);
+ border: 1px solid rgb(255,191,191);
+ }
+ & input[type=text].error:focus,
+ & input[type=password].error:focus {
+ box-shadow: 0 2px 16px rgba(0,0,0,.05), 0 2px 4px rgba(0,0,0,.15), 0 0 1px 2px rgb(255,191,191) inset;
+ }
+
+ & label {
+ width: fit-content;
+ padding: 8px 12px 8px 8px;
+ text-align: center;
+ cursor: pointer;
+ transition: background-color .2s, opacity .2s;
+ }
+ & label:focus-within:has(:focus-visible) {
+ background-color: rgb(232,240,254);
+ text-align: center;
+ cursor: pointer;
+ }
+ & input[type=checkbox] {
+ display: inline-block;
+ height: 0;
+ width: 0;
+ opacity: 0;
+ }
+ .checkbox-icon {
+ display: inline-block;
+ height: 24px;
+ width: 24px;
+ vertical-align: -.36em;
+ background-image: url("icons.svg");
+ background-position: -24px -48px;
+ filter: var(--icon-inactive-filter);
+ opacity: var(--icon-opacity-inactive);
+ }
+ input[type=checkbox]:checked + .checkbox-icon {
+ background-position: 0 -48px;
+ filter: var(--icon-checkbox-active-filter);
+ opacity: var(--icon-opacity-active);
+ }
+
+ & button {
+ position: relative;
+ font-weight: 500;
+ background-color: rgba(0,0,0,.06);
+ border: none;
+ cursor: pointer;
+ transition: background-color .2s, box-shadow .2s;
+ }
+ & button:hover,
+ & button:focus,
+ &.loading button {
+ background-color: #FBBC04;
+ }
+ & button:focus-visible {
+ box-shadow: 0 0 0 4px rgba(251,188,4,.4);
+ }
+ & input + button {
+ margin-top: 32px;
+ }
+ &.loading button {
+ color: transparent;
+ }
+
+ & button .loading-spinner {
+ inset: 50%;
+ transform: translate(-50%,calc(-50% - 1em));
+ display: none;
+ }
+ &.loading button .loading-spinner {
+ display: inline-block;
+ }
+ & .loading-spinner,
+ & .loading-spinner::before,
+ & .loading-spinner::after {
+ position: absolute;
+ border-radius: 50%;
+ width: .7em;
+ height: .7em;
+ animation-fill-mode: both;
+ animation: loading-spinner 1.8s infinite ease-in-out;
+ }
+ & .loading-spinner {
+ animation-delay: -0.16s;
+ }
+ & .loading-spinner::before,
+ & .loading-spinner::after {
+ content: '';
+ top: 0;
+ }
+ & .loading-spinner::before {
+ left: -1.3em;
+ animation-delay: -0.32s;
+ }
+ & .loading-spinner::after {
+ left: 1.3em;
+ }
+
+ &.loading input,
+ &.loading label {
+ opacity: .5;
+ }
+}
+@keyframes loading-spinner {
+ 0%, 80%, 100% { box-shadow: 0 1em 0 -1.3em #fff; }
+ 40% { box-shadow: 0 1em 0 0 #fff; }
+}
+
+.notes-column {
+ overflow-y: auto;
+}
+
+/* TODO :target? */
+.card-shadow {
+ position: relative;
+ margin: 16px auto;
+ max-width: 5.5in;
+ border-radius: 24px;
+ box-shadow: 0 2px 16px rgba(0,0,0,.05), 0 2px 4px rgba(0,0,0,.1);
+ transition: box-shadow .2s;
+}
+.card-shadow:hover,
+.card-shadow:focus-within {
+ box-shadow: 0 2px 16px rgba(0,0,0,.15), 0 2px 4px rgba(0,0,0,.15);
+}
+.card-shadow::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ box-shadow: 0 0 0 1px rgba(255,255,255,.2) inset;
+ border-radius: 24px;
+ pointer-events: none;
+ transition: box-shadow .2s;
+}
+.card-shadow:hover::after,
+.card-shadow:focus-within::after {
+ box-shadow: 0 0 0 2px rgba(255,255,255,.25) inset;
+}
+.card {
+ /*padding-bottom: 16px;*/
+ border-radius: 24px;
+ background-color: var(--bg-color);
+ color: var(--text-color);
+ overflow: hidden;
+}
+
+.card > textarea { /* TODO */
+ display: block;
+ width: 100%;
+ padding: 16px 24px;
+ border: none;
+ resize: none;
+ outline: none;
+ font-size: 16px;
+ line-height: 24px;
+ letter-spacing: .3px;
+ background: transparent;
+ color: inherit;
+}