aboutsummaryrefslogtreecommitdiff
path: root/static/pages
diff options
context:
space:
mode:
authorNathan Reiner <nathan@nathanreiner.xyz>2025-11-13 14:56:02 +0100
committerNathan Reiner <nathan@nathanreiner.xyz>2025-11-13 14:56:02 +0100
commitc7b02f02ad0a7e2888f2d7d3599719e59bbd1ee2 (patch)
tree9f782daf2e2ff78559958f15e0b9ffe5ece78334 /static/pages
parent7ee9d320e6ba9a84542d838892c43cf98b268552 (diff)
frontend: design prototype
Diffstat (limited to 'static/pages')
-rw-r--r--static/pages/image-viewer/index.css17
-rw-r--r--static/pages/image-viewer/index.js20
-rw-r--r--static/pages/login/index.css129
-rw-r--r--static/pages/login/index.js48
-rw-r--r--static/pages/main/index.css69
-rw-r--r--static/pages/main/index.js97
-rw-r--r--static/pages/settings/index.css83
-rw-r--r--static/pages/settings/index.js62
-rw-r--r--static/pages/shuffle/index.css6
-rw-r--r--static/pages/shuffle/index.js17
10 files changed, 548 insertions, 0 deletions
diff --git a/static/pages/image-viewer/index.css b/static/pages/image-viewer/index.css
new file mode 100644
index 0000000..6f53333
--- /dev/null
+++ b/static/pages/image-viewer/index.css
@@ -0,0 +1,17 @@
+
+#container {
+ width: 100%;
+ height: 100vh;
+ overflow-y: auto;
+ display: grid;
+ gap: 10px;
+ padding: 10px;
+}
+
+#container img {
+ margin: auto;
+ max-width: 700px;
+ width: 100%;
+ border-radius: var(--border-radius);
+ box-shadow: #223223aa 1px 1px 4px;
+}
diff --git a/static/pages/image-viewer/index.js b/static/pages/image-viewer/index.js
new file mode 100644
index 0000000..76d720f
--- /dev/null
+++ b/static/pages/image-viewer/index.js
@@ -0,0 +1,20 @@
+import * as sfw from 'sfw';
+const { Div, Img } = sfw.element.native;
+
+const css = await sfw.css(import.meta.url, './index.css')
+
+export default class ImageViewer extends sfw.element.Container {
+ #container
+
+ constructor() {
+ super({ css });
+
+ this.body.append(
+ this.#container = Div.new({ id: 'container' })
+ );
+ }
+
+ add(url) {
+ this.#container.append(Img.new({ src: url }));
+ }
+}
diff --git a/static/pages/login/index.css b/static/pages/login/index.css
new file mode 100644
index 0000000..8a59d83
--- /dev/null
+++ b/static/pages/login/index.css
@@ -0,0 +1,129 @@
+#container {
+ background: var(--page-background);
+ overflow: hidden;
+ width: 100%;
+ height: 100%;
+}
+
+#box {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ max-width: 300px;
+ width: 100%;
+ padding: 20px;
+ box-shadow: #223223aa 1px 1px 4px;
+ border-radius: var(--border-radius);
+ background: #ffffff99;
+ backdrop-filter: blur(10px);
+ z-index: 2000;
+}
+
+#title {
+ font-family: 'Pacifico';
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ text-align: center;
+ top: 0px;
+ padding-top: 50px;
+ font-size: 2.5em;
+ z-index: 1000;
+ text-shadow: -1px -1px 0 var(--page-background),
+ 1px 1px 0 var(--page-background),
+ 1px -1px 0 var(--page-background),
+ -1px 1px 0 var(--page-background);
+ overflow: hidden;
+}
+
+@keyframes left-bubble {
+ from {
+ width: 0px;
+ height: 0px;
+ }
+ to {
+ width: 100vw;
+ height: 100vw;
+ }
+}
+
+#title:before {
+ content: '';
+ display: block;
+ position: absolute;
+ top: -30px;
+ left: 0px;
+ background: var(--primary);
+ transform: translate(-50%, -50%);
+ border-radius: 100%;
+ z-index: -1;
+ animation: left-bubble 1s normal forwards ease;
+ width: 100vw;
+ height: 100vw;
+}
+
+@keyframes right-bubble {
+ from {
+ width: 0px;
+ height: 0px;
+ }
+ to {
+ width: 200vw;
+ height: 200vw;
+ }
+}
+
+#title:after {
+ content: '';
+ display: block;
+ position: absolute;
+ bottom: 55%;
+ right: 0px;
+ width: 200vw;
+ height: 200vw;
+ transform: translate(50%, 50%);
+ background: var(--fg);
+ border-radius: 100%;
+ z-index: -2;
+ animation: right-bubble 1s normal forwards ease;
+}
+
+#form {
+ display: grid;
+ grid-template-columns: 100px auto;
+ max-width: 300px;
+ width: 100%;
+ margin: 0;
+ margin-bottom: 20px;
+ border-radius: 3px;
+ gap: 10px;
+}
+
+#form input {
+ width: 100%;
+}
+
+#form label {
+ margin: auto 0px;
+ font-weight: bold;
+}
+
+button {
+ width: 100%;
+}
+
+#subtitle {
+ position: absolute;
+ bottom: 50px;
+ left: 50%;
+ transform: translate(-50%, 0);
+ white-space: nowrap;
+ font-family: 'Pacifico';
+ color: #69717d;
+ z-index: 1000;
+ text-shadow: -1px -1px 0 var(--page-background),
+ 1px 1px 0 var(--page-background),
+ 1px -1px 0 var(--page-background),
+ -1px 1px 0 var(--page-background);
+}
diff --git a/static/pages/login/index.js b/static/pages/login/index.js
new file mode 100644
index 0000000..fc14dbf
--- /dev/null
+++ b/static/pages/login/index.js
@@ -0,0 +1,48 @@
+import * as sfw from 'sfw';
+const { Div, Label, H1: Title, Input, Button } = sfw.element.native;
+
+const css = await sfw.css(import.meta.url, './index.css');
+
+export default class LoginView extends sfw.element.Container {
+ #user
+ #password
+
+ constructor() {
+ super({ css });
+
+ this.onlogin = () => {};
+
+ this.body.append(
+ Div.new({
+ id: 'container',
+ children: [
+ Div.new({ id: 'title', innerText: 'Memora' }),
+ Div.new({
+ id: 'box',
+ children: [
+
+ Div.new({
+ id: 'form',
+ children: [
+ Label.new({ innerText: 'User' }),
+ this.#user = Input.new({ }),
+
+ Label.new({ innerText: 'Password' }),
+ this.#password = Input.new({ type: 'password' }),
+ ]
+ }),
+ Button.new({
+ innerText: 'Login',
+ onclick: () => this.onlogin(this.#user.value, this.#password.value),
+ }),
+ ]
+ }),
+ Div.new({
+ id: 'subtitle',
+ innerText: 'Where nostalgia is home.',
+ }),
+ ]
+ })
+ );
+ }
+}
diff --git a/static/pages/main/index.css b/static/pages/main/index.css
new file mode 100644
index 0000000..421e689
--- /dev/null
+++ b/static/pages/main/index.css
@@ -0,0 +1,69 @@
+
+:host {
+ display: grid;
+ height: 100%;
+ width: 100%;
+ background: var(--page-background);
+}
+
+#bar {
+ position: fixed;
+ bottom: 5px;
+ left: 5px;
+ right: 5px;
+ background: #fff9;
+ box-shadow: #223223aa 1px 1px 4px;
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr 1fr 1fr ;
+ height: 50px;
+ backdrop-filter: blur(10px);
+ border-radius: var(--border-radius);
+ transition: bottom 0.1s ease;
+}
+
+#bar.hidden {
+ bottom: -200px;
+}
+
+.menu-button {
+ color: var(--fg-disabled);
+ width: 18px;
+ height: 18px;
+ border-radius: 100%;
+ margin: auto;
+ cursor: pointer;
+ user-select: none;
+ align-content: center;
+ text-align: center;
+}
+
+.menu-button.add {
+ width: 60px;
+ height: 60px;
+ margin-top: -20px;
+ background: var(--primary);
+ transition: width 0.1s ease, height 0.1s ease, margin 0.1s ease;
+ color: var(--card-background);
+ font-weight: bold;
+ font-size: 2em;
+}
+
+.menu-button.add .icon {
+ width: 30px;
+ height: 30px;
+ display: grid;
+ margin: auto;
+}
+
+.menu-button.add .icon .bi-house-fill {
+ width: 20px;
+ height: 20px;
+ margin: auto;
+}
+
+.menu-button .icon {
+ width: 100%;
+ height: 100%;
+ display: grid;
+ margin: auto;
+}
diff --git a/static/pages/main/index.js b/static/pages/main/index.js
new file mode 100644
index 0000000..9bf0aae
--- /dev/null
+++ b/static/pages/main/index.js
@@ -0,0 +1,97 @@
+import * as sfw from 'sfw';
+const { Div, Input } = sfw.element.native;
+
+import icons from '/icons/index.js';
+
+const css = await sfw.css(import.meta.url, './index.css');
+
+export default class MainView extends sfw.element.Container {
+ #active_kind
+ #active_button
+ #active_view
+ #bar
+
+ static Kind = Object.freeze({
+ 'upload': 0,
+ 'home': 1,
+ });
+
+ constructor() {
+ super({ css });
+
+ this.onsearch = () => {}
+ this.onmonth = () => {}
+ this.onupload = () => {}
+ this.onshuffle = () => {}
+ this.onhome = () => {}
+
+ this.body.append(
+ this.#active_view = Div.new({ id: 'main' }),
+
+ this.#bar = Div.new({
+ id: 'bar',
+ children: [
+ Div.new({
+ className: 'menu-button',
+ children: [ icons.search ],
+ onclick: (e) => {
+ e.stopPropagation();
+ this.onsearch();
+ }
+ }),
+
+ Div.new({
+ className: 'menu-button',
+ children: [ icons.calendar ],
+ onclick: () => this.onmonth(),
+ }),
+
+ this.#active_button = Div.new({
+ className: 'menu-button add',
+ children: [ icons.add ],
+ onclick: () => this.onupload()
+ }),
+
+ Div.new({
+ className: 'menu-button',
+ children: [ icons.shuffle ],
+ onclick: () => this.onshuffle(),
+ }),
+
+ Div.new({
+ className: 'menu-button',
+ children: [ icons.settings ],
+ onclick: () => this.onsettings(),
+ }),
+ ],
+ }),
+ );
+ }
+
+ hide() {
+ this.#bar.classList.add('hidden');
+ }
+
+ show() {
+ this.#bar.classList.remove('hidden');
+ }
+
+ set active_view(element) {
+ this.#active_view.innerHTML = '';
+ this.#active_view.append(element);
+ }
+
+ set active_kind(kind) {
+ if (kind == MainView.Kind.home) {
+ this.#active_button.innerHTML = '';
+ this.#active_button.append(icons.home);
+ this.#active_button.onclick = () => this.onhome();
+ } else if (kind == MainView.Kind.upload) {
+ this.#active_button.innerHTML = '';
+ this.#active_button.append(icons.add);
+ this.#active_button.onclick = () => this.onupload();
+ } else {
+ console.error(`invalid kind ${kind}`);
+ }
+ }
+}
diff --git a/static/pages/settings/index.css b/static/pages/settings/index.css
new file mode 100644
index 0000000..5128c18
--- /dev/null
+++ b/static/pages/settings/index.css
@@ -0,0 +1,83 @@
+
+#container {
+ width: 100%;
+ height: 100%;
+ background: var(--page-background);
+ padding: 40px;
+ display: flex;
+ flex-flow: column;
+ gap: 20px;
+}
+
+#profile-image {
+ width: 200px;
+ height: 200px;
+ position: relative;
+ margin: auto;
+ margin-top: 50px;
+ margin-bottom: 30px;
+}
+
+#image-container {
+ position: relative;
+ border-radius: 100%;
+ width: 100%;
+ height: 100%;
+ top: 0px;
+ left: 0px;
+ overflow: hidden;
+}
+
+#image-container img {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ min-width: 100%;
+ min-height: 100%;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ user-select: none;
+}
+
+#profile-image #edit-container {
+ overflow: unset;
+}
+
+#profile-image #edit {
+ position: absolute;
+ top: 15px;
+ right: 15px;
+ background: var(--card-background);
+ width: 30px;
+ height: 30px;
+ padding: 6px;
+ border-radius: 100%;
+ box-shadow: var(--shadow);
+ cursor: pointer;
+}
+
+#info-box {
+ width: 100%;
+ text-align: center;
+ color: var(--fg-disabled);
+ font-weight: lighter;
+ user-select: none;
+}
+
+#info-box #name {
+ font-family: 'Pacifico';
+}
+
+#logout {
+ padding: 10px;
+ width: 100%;
+ text-align: center;
+ background: #ee5151;
+ box-shadow: var(--shadow);
+ color: #fff;
+ font-weight: bold;
+ border-radius: var(--border-radius);
+ cursor: pointer;
+}
diff --git a/static/pages/settings/index.js b/static/pages/settings/index.js
new file mode 100644
index 0000000..da30ba7
--- /dev/null
+++ b/static/pages/settings/index.js
@@ -0,0 +1,62 @@
+import * as sfw from 'sfw';
+const { Div, Img } = sfw.element.native;
+
+import Editable from '../../widgets/editable/index.js';
+
+import icons from '../../icons/index.js';
+
+const css = await sfw.css(import.meta.url, './index.css');
+
+export default class SettingsView extends sfw.element.Container {
+ constructor() {
+ super({ css });
+
+ this.onlogout = () => {};
+
+ this.body.append(
+ Div.new({
+ id: 'container',
+ children: [
+ Div.new({
+ id: 'profile-image',
+ children: [
+ Div.new({
+ id: 'image-container',
+ children: [
+ Img.new({
+ src: '/images/0010.jpg',
+ }),
+ ]
+ }),
+ Div.new({
+ id: 'edit',
+ children: [ icons.edit ]
+ }),
+ ]
+ }),
+ Editable.new({
+ title: 'Name',
+ value: 'Nathan Reiner'
+ }),
+ Editable.new({
+ title: 'Birthday',
+ type: 'date',
+ value: '2002-08-06',
+ }),
+ Div.new({
+ id: 'logout',
+ innerText: 'Log-out',
+ onclick: () => this.onlogout(),
+ }),
+ Div.new({
+ id: 'info-box',
+ children: [
+ Div.new({ id: 'name', innerText: 'Memora' }),
+ Div.new({ id: 'version', innerText: '0.0.1-unstable' }),
+ ]
+ }),
+ ]
+ })
+ )
+ }
+}
diff --git a/static/pages/shuffle/index.css b/static/pages/shuffle/index.css
new file mode 100644
index 0000000..534a647
--- /dev/null
+++ b/static/pages/shuffle/index.css
@@ -0,0 +1,6 @@
+
+#container {
+ width: 100%;
+ height: 100%;
+ background: var(--page-background);
+}
diff --git a/static/pages/shuffle/index.js b/static/pages/shuffle/index.js
new file mode 100644
index 0000000..b282556
--- /dev/null
+++ b/static/pages/shuffle/index.js
@@ -0,0 +1,17 @@
+import * as sfw from 'sfw';
+const { Div } = sfw.element.native;
+
+const css = await sfw.css(import.meta.url, './index.css');
+
+export default class ShuffleView extends sfw.element.Container {
+ constructor() {
+ super({ css });
+
+ this.body.append(
+ Div.new({
+ id: 'container',
+ innerText: 'shuffle',
+ })
+ )
+ }
+}