start home page and settings

This commit is contained in:
marlon-erler
2024-07-19 23:45:07 +02:00
parent 5da13eace5
commit eaaf4b813c
13 changed files with 349 additions and 301 deletions
+15 -119
View File
@@ -1,130 +1,26 @@
/* src/styles.css */
[unread] {
border-left: 10px solid var(--text-warning);
}
:root {
--split-thread-height: 8rem;
--split-thread-width: 20rem;
}
.object-entry-wide {
width: 320px;
min-width: 320px;
}
#message-tab {
flex-direction: column;
}
.chat-object-view {
display: none;
}
[showingobjects] header {
border-bottom: 1px solid var(--border);
}
[showingobjects] footer {
display: none;
}
[showingobjects] .chat-object-view {
display: flex;
top: var(--header-height);
}
[showingobjects] .thread-view {
display: none;
}
[showingobjects][chatvisible] .chat-object-view {
right: var(--split-thread-width);
border-right: 1px solid var(--border);
}
[showingobjects][chatvisible] .thread-view {
top: var(--header-height);
padding-top: var(--content-padding);
}
[showingobjects][chatvisible] :is(.thread-view, footer) {
display: flex;
right: 0px;
left: unset;
width: var(--split-thread-width);
}
main:has([showingobjects][chatopen]) {
display: flex;
}
main:has([showingobjects][chatopen]) #settings-tab {
display: none;
}
@media (max-width: 640px) {
[showingobjects][chatvisible][chatopen] .chat-object-view {
width: 100%;
bottom: calc(var(--split-thread-height) + var(--control-height));
border-right: none;
border-bottom: 1px solid var(--border);
}
[showingobjects][chatvisible][chatopen] .thread-view {
top: unset;
bottom: calc(var(--control-height) + var(--safe-area-bottom));
width: 100%;
height: var(--split-thread-height);
padding: 0px;
}
[showingobjects][chatvisible][chatopen] footer {
width: 100%;
}
}
:root {
--day-view-width: 400px;
}
.calendar-wrapper {
/* src/View/homePage.css */
#home-page > div {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
gap: var(--large-gap);
}
.calendar-wrapper .month-grid {
border-bottom: 1px solid var(--border);
}
.calendar-wrapper > * {
#home-page > div > div {
display: flex;
flex-direction: column;
justify-content: start;
gap: var(--gap-between-tiles);
width: 100%;
height: 100%;
flex: 1;
}
.day-miniature[today] h3 {
color: var(--primary);
#home-page #overview-section {
max-width: var(--max-input-width);
}
.day-miniature:has(> div:not(:empty)):not([selected]) {
border: var(--gap-between-tiles) solid var(--text-warning);
}
@media (min-width: 750px) {
.calendar-wrapper:not([chatvisible] *) {
@media (min-width: 1000px) {
#home-page > div {
flex-direction: row;
gap: 8rem;
}
.calendar-wrapper:not([chatvisible] *) .month-grid {
border-bottom: none;
border-right: 1px solid var(--border);
}
.calendar-wrapper:not([chatvisible] *) .day-view {
max-width: var(--day-view-width);
}
}
@media (min-width: 1200px) {
.calendar-wrapper {
flex-direction: row;
}
.calendar-wrapper .month-grid {
border-bottom: none;
border-right: 1px solid var(--border);
}
.calendar-wrapper .day-view {
max-width: var(--day-view-width);
}
}
@media (max-width: 600px) or ((min-width: 750px) and (max-width: 1100px)) {
.day-miniature {
padding: 0px !important;
align-items: center !important;
justify-content: center;
}
.day-miniature h3 {
font-size: 1rem;
text-align: center;
}
.day-miniature > div {
display: none;
#home-page #overview-section {
width: var(--max-input-width);
}
}
+164 -170
View File
@@ -1,150 +1,4 @@
(() => {
// node_modules/bloatless-react/index.ts
var State = class {
_value;
_bindings = /* @__PURE__ */ new Set();
// init
constructor(initialValue) {
this._value = initialValue;
}
// value
get value() {
return this._value;
}
set value(newValue) {
if (this._value == newValue) return;
this._value = newValue;
this.callSubscriptions();
}
// subscriptions
callSubscriptions() {
this._bindings.forEach((fn) => fn(this._value));
}
subscribe(fn) {
this._bindings.add(fn);
fn(this._value);
}
subscribeSilent(fn) {
this._bindings.add(fn);
}
// stringify
toString() {
return JSON.stringify(this._value);
}
};
function persistState(localStorageKey, state) {
state.subscribe(() => {
const stringifiedValue = state.toString();
localStorage.setItem(localStorageKey, stringifiedValue);
});
}
function restoreState(localStorageKey, initialStateValue) {
const storedString = localStorage.getItem(localStorageKey) ?? JSON.stringify(initialStateValue);
const convertedValue = JSON.parse(storedString);
const state = new State(convertedValue);
persistState(localStorageKey, state);
return state;
}
function createElement(tagName, attributes = {}, ...children) {
const element = document.createElement(tagName);
if (attributes != null)
Object.entries(attributes).forEach((entry) => {
const [attributename, value] = entry;
const [directiveKey, directiveValue] = attributename.split(":");
switch (directiveKey) {
case "on": {
switch (directiveValue) {
case "enter": {
element.addEventListener("keydown", (e) => {
if (e.key != "Enter") return;
value();
});
break;
}
default: {
element.addEventListener(directiveValue, value);
}
}
break;
}
case "subscribe": {
const state = value;
state.subscribe(
(newValue) => element[directiveValue] = newValue
);
break;
}
case "bind": {
const state = value;
state.subscribe(
(newValue) => element[directiveValue] = newValue
);
element.addEventListener(
"input",
() => state.value = element[directiveValue]
);
break;
}
case "toggle": {
if (value.subscribe) {
const state = value;
state.subscribe(
(newValue) => element.toggleAttribute(directiveValue, newValue)
);
} else {
element.toggleAttribute(directiveValue, value);
}
break;
}
case "set": {
const state = value;
state.subscribe(
(newValue) => element.setAttribute(directiveValue, newValue)
);
break;
}
case "children": {
switch (directiveValue) {
case "set": {
const state = value;
state.subscribe((newValue) => {
element.innerHTML = "";
element.append(...[newValue].flat());
});
break;
}
case "append":
case "appendandscroll":
case "prepend": {
element.style.scrollBehavior = "smooth";
try {
const [listState, toElement] = value;
listState.handleAddition((newItem) => {
const child = toElement(newItem);
listState.handleRemoval(
newItem,
() => child.remove()
);
if (directiveValue == "append") {
element.append(child);
} else if (directiveValue == "prepend") {
element.prepend(child);
}
});
} catch {
throw `error: cannot process subscribe:children directive because StateItemConverter is not defined. Usage: "subscribe:children={[list, converter]}"; you can find a more detailed example in the documentation`;
}
}
}
}
default:
element.setAttribute(attributename, value);
}
});
children.filter((x) => x).forEach((child) => element.append(child));
return element;
}
// node_modules/udn-frontend/index.ts
var UDNFrontend = class {
ws;
@@ -257,7 +111,7 @@
}
};
// src/utility.ts
// src/Model/Utility/utility.ts
function stringify(data) {
return JSON.stringify(data);
}
@@ -308,30 +162,164 @@
}
};
// src/View/maintenance.tsx
var addressInput = restoreState("maintenance-address", "");
var connectedAddress = new State("");
var isConnected = new State(false);
var connectionModel = new ConnectionModel({
connectionChangeHandler() {
isConnected.value = connectionModel.isConnected;
connectedAddress.value = connectionModel.address ?? "---";
},
messageHandler(data) {
console.log(data);
}
});
function connect() {
connectionModel.connect(addressInput.value);
// node_modules/bloatless-react/index.ts
function createElement(tagName, attributes = {}, ...children) {
const element = document.createElement(tagName);
if (attributes != null)
Object.entries(attributes).forEach((entry) => {
const [attributename, value] = entry;
const [directiveKey, directiveValue] = attributename.split(":");
switch (directiveKey) {
case "on": {
switch (directiveValue) {
case "enter": {
element.addEventListener("keydown", (e) => {
if (e.key != "Enter") return;
value();
});
break;
}
default: {
element.addEventListener(directiveValue, value);
}
}
break;
}
case "subscribe": {
const state = value;
state.subscribe(
(newValue) => element[directiveValue] = newValue
);
break;
}
case "bind": {
const state = value;
state.subscribe(
(newValue) => element[directiveValue] = newValue
);
element.addEventListener(
"input",
() => state.value = element[directiveValue]
);
break;
}
case "toggle": {
if (value.subscribe) {
const state = value;
state.subscribe(
(newValue) => element.toggleAttribute(directiveValue, newValue)
);
} else {
element.toggleAttribute(directiveValue, value);
}
break;
}
case "set": {
const state = value;
state.subscribe(
(newValue) => element.setAttribute(directiveValue, newValue)
);
break;
}
case "children": {
switch (directiveValue) {
case "set": {
const state = value;
state.subscribe((newValue) => {
element.innerHTML = "";
element.append(...[newValue].flat());
});
break;
}
case "append":
case "appendandscroll":
case "prepend": {
element.style.scrollBehavior = "smooth";
try {
const [listState, toElement] = value;
listState.handleAddition((newItem) => {
const child = toElement(newItem);
listState.handleRemoval(
newItem,
() => child.remove()
);
if (directiveValue == "append") {
element.append(child);
} else if (directiveValue == "prepend") {
element.prepend(child);
}
});
} catch {
throw `error: cannot process subscribe:children directive because StateItemConverter is not defined. Usage: "subscribe:children={[list, converter]}"; you can find a more detailed example in the documentation`;
}
}
}
}
default:
element.setAttribute(attributename, value);
}
});
children.filter((x) => x).forEach((child) => element.append(child));
return element;
}
function disconnect() {
connectionModel.disconnect();
}
document.querySelector("main").append(
/* @__PURE__ */ createElement("div", { class: "flex-column" }, /* @__PURE__ */ createElement("input", { "bind:value": addressInput }), /* @__PURE__ */ createElement("button", { "on:click": connect, "toggle:disabled": isConnected }, "Connect"), /* @__PURE__ */ createElement("button", { "on:click": disconnect }, "Disconnect"), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("span", { "subscribe:innerText": connectedAddress }))
);
// src/typeSafety.ts
// src/View/translations.ts
var englishTranslations = {
general: {
closeButton: "close"
},
homePage: {
appName: "Comms",
///
overviewHeadline: "Overview",
statusHeadline: "Status",
settingsButton: "Settings",
///
chatsHeadline: "Chats",
addChatAudioLabel: "name of new chat",
addChatPlaceholder: "Add chat",
addChatButton: "Add chat"
},
settings: {
settingsHeadline: "Settings"
///
}
};
var allTranslations = {
en: englishTranslations
};
var language = navigator.language.substring(0, 2);
var translations = allTranslations[language] || allTranslations.en;
// src/View/homePage.tsx
function HomePage() {
return /* @__PURE__ */ createElement("article", { id: "home-page" }, /* @__PURE__ */ createElement("header", null, /* @__PURE__ */ createElement("span", null, translations.homePage.appName)), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("div", { id: "overview-section" }, /* @__PURE__ */ createElement("h2", null, translations.homePage.overviewHeadline), /* @__PURE__ */ createElement("div", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "cell_tower"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("h3", null, translations.homePage.statusHeadline))), /* @__PURE__ */ createElement("button", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "settings"), /* @__PURE__ */ createElement("div", null, translations.homePage.settingsButton))), /* @__PURE__ */ createElement("div", { id: "chat-section" }, /* @__PURE__ */ createElement("h2", null, translations.homePage.chatsHeadline), /* @__PURE__ */ createElement("div", { class: "flex-row width-input" }, /* @__PURE__ */ createElement(
"input",
{
placeholder: translations.homePage.addChatPlaceholder,
"aria-label": translations.homePage.addChatAudioLabel
}
), /* @__PURE__ */ createElement(
"button",
{
class: "primary",
"aria-label": translations.homePage.addChatButton
},
/* @__PURE__ */ createElement("span", { class: "icon" }, "add")
)))));
}
// src/View/mainPage.tsx
function MainPage() {
return /* @__PURE__ */ createElement("article", { id: "home-page" }, /* @__PURE__ */ createElement("header", null, /* @__PURE__ */ createElement("span", null, translations.homePage.appName)), /* @__PURE__ */ createElement("div", null));
}
// src/View/setingsModal.tsx
function SetttingsModal() {
return /* @__PURE__ */ createElement("div", { class: "modal" }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, translations.settings.settingsHeadline)), /* @__PURE__ */ createElement("button", null, translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close"))));
}
// src/Model/Utility/typeSafety.ts
var DATA_VERSION = "v2";
// src/Model/storageModel.ts
@@ -421,5 +409,11 @@
// src/index.tsx
var storageModel = new StorageModel();
console.log(storageModel.list("z"));
var connectionModel = new ConnectionModel({
connectionChangeHandler() {
},
messageHandler(data) {
}
});
document.querySelector("main").append(HomePage(), MainPage(), SetttingsModal());
})();
+2 -2
View File
@@ -2,8 +2,8 @@
import StorageModel, { storageKeys } from "./storageModel";
import { checkIsValidObject } from "../typeSafety";
import { createTimestamp } from "../utility";
import { checkIsValidObject } from "./Utility/typeSafety";
import { createTimestamp } from "./Utility/utility";
import { v4 } from "uuid";
export class ChatModel {
+1 -1
View File
@@ -3,7 +3,7 @@
import UDNFrontend, { Message } from "udn-frontend";
import { ChatMessage } from "./chatModel";
import { stringify } from "../utility";
import { stringify } from "./Utility/utility";
export default class ConnectionModel {
// basic
+2 -2
View File
@@ -1,8 +1,8 @@
// this file is responsible for reading and writing persistent data; all storage shall be handled by this file.
import { parse, stringify } from "../utility";
import { parse, stringify } from "./Utility/utility";
import { DATA_VERSION } from "../typeSafety";
import { DATA_VERSION } from "./Utility/typeSafety";
export default class StorageModel {
storageEntryTree: StorageEntry = {};
+33
View File
@@ -0,0 +1,33 @@
#home-page > div {
display: flex;
flex-direction: column;
gap: var(--large-gap);
}
#home-page > div > div {
display: flex;
flex-direction: column;
justify-content: start;
gap: var(--gap-between-tiles);
width: 100%;
flex: 1;
}
#home-page #overview-section {
max-width: var(--max-input-width);
}
@media (min-width: 1000px) {
#home-page > div {
flex-direction: row;
gap: 8rem;
}
#home-page #overview-section {
width: var(--max-input-width);
}
}
+48
View File
@@ -0,0 +1,48 @@
import "./homePage.css";
import * as React from "bloatless-react";
import { translations } from "./translations";
export function HomePage() {
return (
<article id="home-page">
<header>
<span>{translations.homePage.appName}</span>
</header>
<div>
<div id="overview-section">
<h2>{translations.homePage.overviewHeadline}</h2>
<div class="tile flex-no">
<span class="icon">cell_tower</span>
<div>
<h3>{translations.homePage.statusHeadline}</h3>
</div>
</div>
<button class="tile flex-no">
<span class="icon">settings</span>
<div>{translations.homePage.settingsButton}</div>
</button>
</div>
<div id="chat-section">
<h2>{translations.homePage.chatsHeadline}</h2>
<div class="flex-row width-input">
<input
placeholder={translations.homePage.addChatPlaceholder}
aria-label={translations.homePage.addChatAudioLabel}
></input>
<button
class="primary"
aria-label={translations.homePage.addChatButton}
>
<span class="icon">add</span>
</button>
</div>
</div>
</div>
</article>
);
}
+14
View File
@@ -0,0 +1,14 @@
import * as React from "bloatless-react";
import { translations } from "./translations";
export function MainPage() {
return (
<article id="home-page">
<header>
<span>{translations.homePage.appName}</span>
</header>
<div></div>
</article>
);
}
+19
View File
@@ -0,0 +1,19 @@
import * as React from "bloatless-react";
import { translations } from "./translations";
export function SetttingsModal() {
return (
<div class="modal">
<div>
<main>
<h2>{translations.settings.settingsHeadline}</h2>
</main>
<button>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
);
}
+39
View File
@@ -0,0 +1,39 @@
const englishTranslations = {
general: {
closeButton: "close",
},
homePage: {
appName: "Comms",
///
overviewHeadline: "Overview",
statusHeadline: "Status",
settingsButton: "Settings",
///
chatsHeadline: "Chats",
addChatAudioLabel: "name of new chat",
addChatPlaceholder: "Add chat",
addChatButton: "Add chat",
},
settings: {
settingsHeadline: "Settings",
///
}
};
const allTranslations: { [language: string]: typeof englishTranslations } = {
en: englishTranslations,
};
const language = navigator.language.substring(0, 2);
export const translations = allTranslations[language] || allTranslations.en;
+12 -7
View File
@@ -1,12 +1,17 @@
import ConnectionModel from "./Model/connectionModel";
import { HomePage } from "./View/homePage";
import { MainPage } from "./View/mainPage";
import { SetttingsModal } from "./View/setingsModal";
import StorageModel from "./Model/storageModel";
// models
const storageModel = new StorageModel();
const connectionModel = new ConnectionModel({
connectionChangeHandler() {
},
messageHandler(data) {
},
})
connectionChangeHandler() {},
messageHandler(data) {},
});
// view
document
.querySelector("main")!
.append(HomePage(), MainPage(), SetttingsModal());