diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..cdf57c6
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,114 @@
+{
+ "root": true,
+ "parser": "@typescript-eslint/parser",
+ "plugins": [
+ "@typescript-eslint"
+ ],
+ "parserOptions": {
+ "project": [
+ "./tsconfig.json",
+ "./tsconfig.test.json",
+ "./tsconfig.frontend.json"
+ ]
+ },
+ "extends": [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/recommended"
+ ],
+ "rules": {
+ "indent": [
+ "error",
+ 4,
+ {
+ "SwitchCase": 1
+ }
+ ],
+ "no-trailing-spaces": "error",
+ "max-len": [
+ "error",
+ {
+ "code": 120,
+ "ignoreStrings": true,
+ "ignoreTemplateLiterals": true,
+ "ignoreRegExpLiterals": true
+ }
+ ],
+ "semi": "off",
+ "@typescript-eslint/semi": [
+ "error"
+ ],
+ "no-extra-semi": "error",
+ "eol-last": "error",
+ "comma-dangle": "off",
+ "@typescript-eslint/comma-dangle": [
+ "error",
+ {
+ "arrays": "always-multiline",
+ "objects": "always-multiline",
+ "imports": "always-multiline",
+ "exports": "always-multiline",
+ "functions": "always-multiline",
+ "enums": "always-multiline",
+ "generics": "always-multiline",
+ "tuples": "always-multiline"
+ }
+ ],
+ "no-extra-parens": "off",
+ "@typescript-eslint/no-extra-parens": [
+ "error"
+ ],
+ "no-nested-ternary": "error",
+ "@typescript-eslint/no-inferrable-types": "off",
+ "@typescript-eslint/explicit-module-boundary-types": "error",
+ "@typescript-eslint/no-unnecessary-condition": "error",
+ "@typescript-eslint/no-unused-vars": [
+ "error",
+ {
+ "argsIgnorePattern": "^_"
+ }
+ ],
+ "@typescript-eslint/no-non-null-assertion": "error",
+ "no-useless-return": "error",
+ "no-useless-constructor": "off",
+ "@typescript-eslint/no-useless-constructor": [
+ "error"
+ ],
+ "no-return-await": "off",
+ "@typescript-eslint/return-await": [
+ "error",
+ "always"
+ ],
+ "@typescript-eslint/explicit-member-accessibility": [
+ "error",
+ {
+ "accessibility": "explicit"
+ }
+ ],
+ "@typescript-eslint/no-floating-promises": "error"
+ },
+ "ignorePatterns": [
+ "jest.config.js",
+ "webpack.config.js",
+ "dist/**/*",
+ "public/**/*",
+ "config/**/*"
+ ],
+ "overrides": [
+ {
+ "files": [
+ "test/**/*"
+ ],
+ "rules": {
+ "max-len": [
+ "error",
+ {
+ "code": 120,
+ "ignoreTemplateLiterals": true,
+ "ignoreRegExpLiterals": true,
+ "ignoreStrings": true
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/README.md b/README.md
index 0d02b04..4ff4bc9 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,3 @@
# ily.li is a simple file self-hosting solution
+
+Boilerplate for a quickstart with [swaf](https://eternae.ink/arisu/swaf)
diff --git a/app.service b/app.service
index d303c75..c90548c 100644
--- a/app.service
+++ b/app.service
@@ -1,3 +1,6 @@
+# Please customize values i.e. paths, user, group, WorkingDirectory based on your environment. Do not use the same
+# user and group for different applications.
+
[Unit]
Description=ily.li website
After=network-online.target
diff --git a/assets/config.json b/assets/config.json
index b60dc10..c5ed8d1 100644
--- a/assets/config.json
+++ b/assets/config.json
@@ -1,6 +1,6 @@
{
"bundles": {
- "app": "js/app.js",
+ "app": "ts/app.ts",
"fm": "js/fm.js",
"url-shrinker": "js/url-shrinker.js",
"layout": "sass/layout.scss",
diff --git a/assets/js/copyable_text.js b/assets/js/copyable_text.js
deleted file mode 100644
index 6501a1a..0000000
--- a/assets/js/copyable_text.js
+++ /dev/null
@@ -1,12 +0,0 @@
-document.addEventListener('DOMContentLoaded', () => {
- document.querySelectorAll('.copyable-text').forEach(el => {
- const contentEl = el.querySelector('.content');
- contentEl.addEventListener('click', () => {
- window.getSelection().selectAllChildren(contentEl);
- });
- el.querySelector('.copy-button').addEventListener('click', () => {
- window.getSelection().selectAllChildren(contentEl);
- document.execCommand('copy');
- });
- });
-});
\ No newline at end of file
diff --git a/assets/js/forms.js b/assets/js/forms.js
deleted file mode 100644
index dd3f184..0000000
--- a/assets/js/forms.js
+++ /dev/null
@@ -1,29 +0,0 @@
-// For labels to update their state (css selectors based on the value attribute)
-document.addEventListener('DOMContentLoaded', () => {
- document.querySelectorAll('input').forEach(el => {
- if (el.type !== 'checkbox') {
- el.setAttribute('value', el.value);
- el.addEventListener('change', () => {
- el.setAttribute('value', el.value);
- });
- }
- });
-});
-
-window.applyFormMessages = function (formElement, messages) {
- for (const fieldName of Object.keys(messages)) {
- const field = formElement.querySelector('#field-' + fieldName);
- let parent = field.parentElement;
- while (parent && !parent.classList.contains('form-field')) parent = parent.parentElement;
-
- if (field) {
- let err = field.querySelector('.error');
- if (!err) {
- err = document.createElement('div');
- err.classList.add('error');
- parent.insertBefore(err, parent.querySelector('.hint') || parent);
- }
- err.innerHTML = ` ${messages[fieldName].message}`;
- }
- }
-}
\ No newline at end of file
diff --git a/assets/js/main_menu.js b/assets/js/main_menu.js
deleted file mode 100644
index f2a89da..0000000
--- a/assets/js/main_menu.js
+++ /dev/null
@@ -1,17 +0,0 @@
-document.addEventListener('DOMContentLoaded', () => {
- const menuButton = document.getElementById('menu-button');
- const mainMenu = document.getElementById('main-menu');
-
- menuButton.addEventListener('click', (e) => {
- e.stopPropagation();
- mainMenu.classList.toggle('open');
- });
-
- mainMenu.addEventListener('click', (e) => {
- e.stopPropagation();
- });
-
- document.addEventListener('click', () => {
- mainMenu.classList.remove('open');
- });
-});
\ No newline at end of file
diff --git a/assets/sass/_vars.scss b/assets/sass/_vars.scss
index b3df9c8..39529f9 100644
--- a/assets/sass/_vars.scss
+++ b/assets/sass/_vars.scss
@@ -9,6 +9,7 @@ $defaultTextColor: #ffffff;
$headerBackground: darken($primary, 7.5%);
$footerBackground: lighten($headerBackground, 1%);
$panelBackground: lighten($headerBackground, 1%);
+$inputBackground: darken($panelBackground, 4%);
$info: #4499ff;
$infoText: darken($info, 42%);
@@ -27,4 +28,4 @@ $errorText: darken($error, 30%);
$errorColor: desaturate($errorText, 50%);
// Responsivity
-$menuLayoutSwitchTreshold: 700px;
+$mobileThreshold: 632px;
diff --git a/assets/sass/layout.scss b/assets/sass/layout.scss
index ce1e32f..6fa0630 100644
--- a/assets/sass/layout.scss
+++ b/assets/sass/layout.scss
@@ -21,7 +21,53 @@ body {
background-color: $backgroundColor;
}
-header {
+@mixin tip {
+ position: relative;
+
+ .tip {
+ visibility: hidden;
+ position: absolute;
+ z-index: 10000;
+ pointer-events: none;
+ display: block;
+ width: max-content;
+ height: 30px;
+ padding: 4px 8px;
+ line-height: 22px;
+ top: calc(100% + 8px);
+ left: 50%;
+ transform: translateX(-50%);
+
+ text-align: center;
+ font-size: 18px;
+ color: $defaultTextColor;
+ opacity: 0;
+ transition: opacity ease-out 100ms, visibility step-end 150ms;
+ transition-delay: 0ms;
+ background-color: #000;
+ border-radius: 5px;
+
+ text-transform: initial;
+ font-weight: initial;
+
+ &.top {
+ top: auto;
+ bottom: calc(100% + 8px);
+ }
+ }
+
+ &:hover, &:active {
+ .tip {
+ visibility: visible;
+ opacity: 1;
+ transition: opacity ease-out 100ms;
+ transition-delay: 150ms;
+ }
+ }
+}
+
+body > header {
+ z-index: 50;
display: flex;
flex-direction: row;
justify-content: space-between;
@@ -61,6 +107,7 @@ header {
font-size: 20px;
li {
+ position: relative;
list-style: none;
a, button {
@@ -77,13 +124,6 @@ header {
&:not(button) {
background-color: rgba(255, 255, 255, 0.07);
}
-
- .tip {
- visibility: visible;
- opacity: 1;
- transition: opacity ease-out 100ms;
- transition-delay: 150ms;
- }
}
.feather {
@@ -99,11 +139,6 @@ header {
.feather {
margin-right: 0;
}
-
- .tip {
- text-transform: initial;
- font-weight: initial;
- }
}
form {
@@ -112,6 +147,34 @@ header {
align-items: center;
padding: 0;
}
+
+ &.auth-user {
+ img {
+ width: 48px;
+ height: 48px;
+ border-radius: 3px;
+ margin-right: 8px;
+ }
+ }
+
+ .dropdown {
+ position: absolute;
+ z-index: -1;
+ top: 100%;
+ right: 0;
+
+ white-space: nowrap;
+ background: $headerBackground;
+ border-radius: 0 0 3px 3px;
+
+ a {
+ padding: 0 8px;
+ }
+ }
+
+ &:hover .dropdown {
+ display: block;
+ }
}
}
@@ -120,7 +183,7 @@ header {
}
}
- @media (max-width: $menuLayoutSwitchTreshold) {
+ @media (max-width: $mobileThreshold) {
flex-direction: row-reverse;
.logo {
@@ -149,7 +212,7 @@ header {
}
}
- ul {
+ > ul {
flex-direction: column;
position: absolute;
z-index: 10;
@@ -172,39 +235,25 @@ header {
font-weight: inherit;
}
}
+
+ .dropdown {
+ position: initial;
+ display: block;
+ padding-left: 32px;
+ }
}
}
}
}
- @media (min-width: $menuLayoutSwitchTreshold) {
+ @media (min-width: $mobileThreshold) {
nav ul li {
- a, button {
- .tip {
- visibility: hidden;
- position: absolute;
- display: block;
- width: max-content;
- height: 30px;
- padding: 4px 8px;
- line-height: 22px;
- top: calc(100% + 8px);
- left: 50%;
- transform: translateX(-50%);
-
- text-align: center;
- font-size: 18px;
- color: $defaultTextColor;
- opacity: 0;
- transition: opacity ease-out 100ms, visibility step-end 150ms;
- transition-delay: 0ms;
- background-color: #000;
- border-radius: 5px;
- }
+ a, button, .button {
+ @include tip;
}
&:last-child {
- a, button {
+ a, button, .button {
.tip {
left: unset;
right: 4px;
@@ -225,7 +274,11 @@ footer {
main {
flex: 1;
- padding: 8px;
+ padding: 8px 0;
+
+ button, .button {
+ @include tip;
+ }
}
h1 {
@@ -252,15 +305,23 @@ section > h2, .panel > h2 {
align-items: center;
position: relative;
text-align: center;
- margin-top: 16px;
+ margin-top: 4px;
- &::before, &::after {
+ font-size: 24px;
+ line-height: 1;
+
+ .feather {
+ margin: 0 16px 0 0;
+ opacity: 0.1;
+ }
+
+ &::after {
content: "";
flex: 1;
- margin: 0 32px;
+ margin: 0 16px;
height: 0;
border-bottom: 1px solid $defaultTextColor;
- opacity: 0.2;
+ opacity: 0.1;
}
}
@@ -277,7 +338,7 @@ a {
text-decoration: none;
&:hover {
- color: lighten($secondary, 5%);
+ color: lighten($secondary, 10%);
}
.feather.feather-external-link {
@@ -292,11 +353,27 @@ form {
text-align: center;
.form-field {
- position: relative;
display: flex;
flex-direction: column;
margin: 16px auto;
+ .control {
+ position: relative;
+ background: $inputBackground;
+ border-radius: 5px;
+ }
+
+ .feather.icon {
+ position: absolute;
+ top: 50%;
+ right: 8px;
+ transform: translateY(-50%);
+ z-index: 0;
+
+ --icon-size: 24px;
+ opacity: 0.75;
+ }
+
label {
position: absolute;
left: 8px;
@@ -321,11 +398,11 @@ form {
}
}
- input, select, .input-group {
+ input, select, textarea, .input-group {
+ z-index: 1;
border: 0;
color: $defaultTextColor;
- background: lighten($panelBackground, 4%);
- border-radius: 5px;
+ background: transparent;
font-size: 16px;
&:focus, &:not([value=""]), &[type="file"] {
@@ -336,10 +413,11 @@ form {
}
}
- input, select, .form-display {
+ input, select, textarea, .form-display {
display: block;
padding: 32px 8px 8px 8px;
width: 100%;
+ height: 60px;
}
select {
@@ -353,9 +431,9 @@ form {
& + .feather {
position: absolute;
- z-index: -1;
+ pointer-events: none;
right: 8px;
- bottom: 8px;
+ top: 30px;
transition: transform 150ms ease-out;
}
@@ -366,30 +444,48 @@ form {
}
}
+ textarea {
+ resize: vertical;
+ min-height: 100px;
+ font-family: inherit;
+ }
+
input[type=color] {
height: calc(32px + 8px + 32px);
}
&.inline {
+ display: flex;
flex-direction: row;
- input[type=checkbox] {
- text-align: left;
- width: min-content;
- height: min-content;
+ .control {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ flex-grow: 1;
- & ~ label {
- position: static;
- display: inline;
- padding-left: 8px;
- font-size: 16px;
+ input[type=checkbox] {
+ width: min-content;
+ height: min-content;
+ margin: 8px;
+ text-align: left;
+
+ & ~ label {
+ position: static;
+ flex-grow: 1;
+ display: inline;
+ padding: 8px;
+
+ font-size: 16px;
+ text-align: left;
+ }
}
}
}
.input-group {
display: flex;
- flex-shrink: 1;
+ flex-grow: 1;
flex-direction: row;
div {
@@ -398,19 +494,9 @@ form {
input {
width: 100%;
- margin-top: 24px;
- padding-top: 8px;
border: 0;
background: transparent;
}
-
- > input + * {
- position: absolute;
- top: 32px;
- right: 28px;
- user-select: none;
- text-align: right;
- }
}
}
}
@@ -418,7 +504,7 @@ form {
.inline-fields {
display: flex;
flex-direction: row;
- align-items: center;
+ align-items: start;
margin: 16px auto;
.form-field {
@@ -547,8 +633,17 @@ button, .button {
}
}
-.max-content {
- width: max-content;
+.breadcrumb {
+ list-style: none;
+ display: flex;
+ flex-direction: row;
+ margin: 0;
+ padding: 8px;
+
+ > *:not(:first-child)::before {
+ content: '›';
+ padding: 0 8px;
+ }
}
// ---
@@ -558,14 +653,27 @@ button, .button {
text-align: center;
}
-.container {
+@mixin container {
+ width: $mobileThreshold;
padding: 0 16px;
- max-width: 632px;
- margin: 0 auto;
+
+ @media (min-width: $mobileThreshold) {
+ margin: 0 auto;
+ }
+
+ @media (max-width: $mobileThreshold) {
+ width: 100%;
+ padding: 0 8px;
+ }
+}
+
+.container {
+ @include container;
}
.panel {
- margin: 16px 0;
+ position: relative;
+ margin: 16px 0 48px;
padding: 8px;
background-color: $panelBackground;
border-radius: 5px;
@@ -573,6 +681,14 @@ button, .button {
p {
margin: 16px 8px;
}
+
+ > .feather:first-child {
+ position: absolute;
+ --icon-size: 24px;
+ opacity: 0.1;
+ top: 8px;
+ left: 8px;
+ }
}
.sub-panel {
@@ -587,10 +703,16 @@ button, .button {
// --- Feather
// ---
.feather {
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+
flex-shrink: 0;
- --icon-size: 24px;
width: var(--icon-size);
height: var(--icon-size);
+
+ --icon-size: 16px;
+ font-size: var(--icon-size);
stroke: currentColor;
stroke-width: 2;
stroke-linecap: square;
diff --git a/assets/ts/PersistentWebSocket.ts b/assets/ts/PersistentWebSocket.ts
new file mode 100644
index 0000000..98e474f
--- /dev/null
+++ b/assets/ts/PersistentWebSocket.ts
@@ -0,0 +1,39 @@
+export default class PersistentWebsocket {
+ private webSocket?: WebSocket;
+
+ public constructor(
+ protected readonly url: string,
+ private readonly handler: MessageHandler,
+ protected readonly reconnectOnClose: boolean = true,
+ ) {
+ }
+
+ public run(): void {
+ const _webSocket = this.webSocket = new WebSocket(this.url);
+ this.webSocket.addEventListener('open', () => {
+ console.debug('Websocket connected');
+ });
+ this.webSocket.addEventListener('message', (e) => {
+ this.handler(_webSocket, e);
+ });
+ this.webSocket.addEventListener('error', (e) => {
+ console.error('Websocket error', e);
+ });
+ this.webSocket.addEventListener('close', (e) => {
+ this.webSocket = undefined;
+ console.debug('Websocket closed', e.code, e.reason);
+
+ if (this.reconnectOnClose) {
+ setTimeout(() => this.run(), 1000);
+ }
+ });
+ }
+
+ public send(data: string): void {
+ if (!this.webSocket) throw new Error('WebSocket not connected');
+
+ this.webSocket.send(data);
+ }
+}
+
+export type MessageHandler = (webSocket: WebSocket, e: MessageEvent) => void;
diff --git a/assets/js/app.js b/assets/ts/app.ts
similarity index 68%
rename from assets/js/app.js
rename to assets/ts/app.ts
index 10fa287..8594552 100644
--- a/assets/js/app.js
+++ b/assets/ts/app.ts
@@ -2,8 +2,9 @@ import './external_links';
import './message_icons';
import './forms';
import './copyable_text';
+import './tooltips-and-dropdowns';
import './main_menu';
+import './font-awesome';
+// css
import '../sass/app.scss';
-
-console.log('Hello world!');
\ No newline at end of file
diff --git a/assets/ts/copyable_text.ts b/assets/ts/copyable_text.ts
new file mode 100644
index 0000000..60d58ab
--- /dev/null
+++ b/assets/ts/copyable_text.ts
@@ -0,0 +1,15 @@
+document.addEventListener('DOMContentLoaded', () => {
+ document.querySelectorAll('.copyable-text').forEach(el => {
+ const contentEl = el.querySelector('.content');
+ const selection = window.getSelection();
+ if (contentEl && selection) {
+ contentEl.addEventListener('click', () => {
+ selection.selectAllChildren(contentEl);
+ });
+ el.querySelector('.copy-button')?.addEventListener('click', () => {
+ selection.selectAllChildren(contentEl);
+ document.execCommand('copy');
+ });
+ }
+ });
+});
diff --git a/assets/js/external_links.js b/assets/ts/external_links.ts
similarity index 59%
rename from assets/js/external_links.js
rename to assets/ts/external_links.ts
index 7108c4e..bc9ba26 100644
--- a/assets/js/external_links.js
+++ b/assets/ts/external_links.ts
@@ -2,8 +2,10 @@ import feather from "feather-icons";
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('a[target="_blank"]').forEach(el => {
- el.innerHTML += ``;
+ if (!el.classList.contains('no-icon')) {
+ el.innerHTML += ``;
+ }
});
feather.replace();
-});
\ No newline at end of file
+});
diff --git a/assets/ts/font-awesome.ts b/assets/ts/font-awesome.ts
new file mode 100644
index 0000000..5ce65ff
--- /dev/null
+++ b/assets/ts/font-awesome.ts
@@ -0,0 +1,4 @@
+import '../../node_modules/@fortawesome/fontawesome-free/scss/fontawesome.scss';
+import '../../node_modules/@fortawesome/fontawesome-free/scss/regular.scss';
+import '../../node_modules/@fortawesome/fontawesome-free/scss/solid.scss';
+import '../../node_modules/@fortawesome/fontawesome-free/scss/brands.scss';
diff --git a/assets/ts/forms.ts b/assets/ts/forms.ts
new file mode 100644
index 0000000..ece465d
--- /dev/null
+++ b/assets/ts/forms.ts
@@ -0,0 +1,43 @@
+/*
+ * For labels to update their state (css selectors based on the value attribute)
+ */
+import {ValidationError} from "swaf/db/Validator";
+
+export function updateInputs(): void {
+ document.querySelectorAll('input, textarea').forEach(el => {
+ if (!el.dataset.inputSetup) {
+ el.dataset.inputSetup = 'true';
+ if (el.type !== 'checkbox') {
+ el.setAttribute('value', el.value);
+ el.addEventListener('change', () => {
+ el.setAttribute('value', el.value);
+ });
+ }
+ }
+ });
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+ updateInputs();
+});
+
+export function applyFormMessages(
+ formElement: HTMLFormElement,
+ messages: { [p: string]: ValidationError },
+): void {
+ for (const fieldName of Object.keys(messages)) {
+ const field = formElement.querySelector('#field-' + fieldName);
+ if (!field) continue;
+
+ let parent = field.parentElement;
+ while (parent && !parent.classList.contains('form-field')) parent = parent.parentElement;
+
+ let err = field.querySelector('.error');
+ if (!err) {
+ err = document.createElement('div');
+ err.classList.add('error');
+ parent?.insertBefore(err, parent.querySelector('.hint') || parent);
+ }
+ err.innerHTML = ` ${messages[fieldName].message}`;
+ }
+}
diff --git a/assets/ts/main_menu.ts b/assets/ts/main_menu.ts
new file mode 100644
index 0000000..4e19e40
--- /dev/null
+++ b/assets/ts/main_menu.ts
@@ -0,0 +1,21 @@
+document.addEventListener('DOMContentLoaded', () => {
+ const menuButton = document.getElementById('menu-button');
+ const mainMenu = document.getElementById('main-menu');
+
+ if (menuButton) {
+ menuButton.addEventListener('click', (e) => {
+ e.stopPropagation();
+ mainMenu?.classList.toggle('open');
+ });
+ }
+
+ if (mainMenu) {
+ mainMenu.addEventListener('click', (e) => {
+ e.stopPropagation();
+ });
+
+ document.addEventListener('click', () => {
+ mainMenu.classList.remove('open');
+ });
+ }
+});
diff --git a/assets/js/message_icons.js b/assets/ts/message_icons.ts
similarity index 60%
rename from assets/js/message_icons.js
rename to assets/ts/message_icons.ts
index b625f55..b737de3 100644
--- a/assets/js/message_icons.js
+++ b/assets/ts/message_icons.ts
@@ -1,21 +1,26 @@
import feather from "feather-icons";
document.addEventListener('DOMContentLoaded', () => {
- const messageTypeToIcon = {
+ const messageTypeToIcon: { [p: string]: string } = {
info: 'info',
success: 'check',
warning: 'alert-triangle',
error: 'x-circle',
question: 'help-circle',
};
- document.querySelectorAll('.message').forEach(el => {
- const type = el.dataset['type'];
+
+ document.querySelectorAll('.message').forEach(el => {
const icon = el.querySelector('.icon');
+ const type = el.dataset['type'];
+ if (!icon || !type) return;
+ if (!messageTypeToIcon[type]) throw new Error(`No icon for type ${type}`);
+
const svgContainer = document.createElement('div');
svgContainer.innerHTML = feather.icons[messageTypeToIcon[type]].toSvg();
- el.insertBefore(svgContainer.firstChild, icon);
+
+ if (svgContainer.firstChild) el.insertBefore(svgContainer.firstChild, icon);
icon.remove();
});
feather.replace();
-});
\ No newline at end of file
+});
diff --git a/assets/ts/tooltips-and-dropdowns.ts b/assets/ts/tooltips-and-dropdowns.ts
new file mode 100644
index 0000000..069b405
--- /dev/null
+++ b/assets/ts/tooltips-and-dropdowns.ts
@@ -0,0 +1,31 @@
+export function updateTooltips(): void {
+ console.debug('Updating tooltips');
+ const elements = document.querySelectorAll('.tip, .dropdown');
+
+ // Calculate max potential displacement
+ let max = 0;
+ elements.forEach(el => {
+ const box = el.getBoundingClientRect();
+ if (max < box.height) max = box.height;
+ });
+
+ // Prevent displacement
+ elements.forEach(el => {
+ if (!el.dataset.tooltipSetup) {
+ el.dataset.tooltipSetup = 'true';
+ const box = el.getBoundingClientRect();
+ if (box.bottom >= document.body.clientHeight - (max + 32)) {
+ el.classList.add('top');
+ }
+ }
+ });
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+ window.addEventListener('popstate', () => {
+ updateTooltips();
+ });
+ window.requestAnimationFrame(() => {
+ updateTooltips();
+ });
+});
diff --git a/config/default.json5 b/config/default.json5
new file mode 100644
index 0000000..e0c6b9b
--- /dev/null
+++ b/config/default.json5
@@ -0,0 +1,43 @@
+{
+ app: {
+ name: 'Example App',
+ contact_email: 'contact@example.net'
+ },
+ log_level: "DEV",
+ db_log_level: "ERROR",
+ public_url: "http://localhost:4899",
+ public_websocket_url: "ws://localhost:4899",
+ port: 4899,
+ mysql: {
+ connectionLimit: 10,
+ host: "localhost",
+ user: "root",
+ password: "",
+ database: "example_app",
+ create_database_automatically: false
+ },
+ redis: {
+ host: "127.0.0.1",
+ port: 6379,
+ prefix: 'example_app'
+ },
+ session: {
+ cookie: {
+ secure: false
+ }
+ },
+ mail: {
+ host: "127.0.0.1",
+ port: "1025",
+ secure: false,
+ username: "",
+ password: "",
+ allow_invalid_tls: true,
+ from: 'contact@example.net',
+ from_name: 'Example App',
+ },
+ view: {
+ cache: false
+ },
+ approval_mode: false,
+}
diff --git a/config/production.json5 b/config/production.json5
new file mode 100644
index 0000000..6993e0e
--- /dev/null
+++ b/config/production.json5
@@ -0,0 +1,15 @@
+{
+ log_level: "DEBUG",
+ db_log_level: "ERROR",
+ public_url: "https://watch-my.stream",
+ public_websocket_url: "wss://watch-my.stream",
+ session: {
+ cookie: {
+ secure: true
+ }
+ },
+ mail: {
+ secure: true,
+ allow_invalid_tls: false
+ }
+}
diff --git a/config/test.json5 b/config/test.json5
new file mode 100644
index 0000000..cda9d47
--- /dev/null
+++ b/config/test.json5
@@ -0,0 +1,9 @@
+{
+ mysql: {
+ host: "localhost",
+ user: "root",
+ password: "",
+ database: "swaf_test",
+ create_database_automatically: true
+ }
+}
diff --git a/package.json b/package.json
index 2a24231..c271d25 100644
--- a/package.json
+++ b/package.json
@@ -5,30 +5,38 @@
"repository": "https://gitlab.com/ArisuOngaku/ily.li",
"author": "Alice Gaudon ",
"license": "GPL-3.0-only",
- "main": "dist/main.js",
+ "main": "dist/src/main.js",
"scripts": {
- "test": "jest --verbose --runInBand",
"dist-webpack": "webpack --mode production",
- "dist": "tsc && npm run dist-webpack",
+ "clean": "(test ! -d dist || rm -r dist)",
+ "compile": "yarn clean && tsc",
+ "build": "yarn compile && yarn dist-webpack",
+ "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"dev": "concurrently -k -n \"Typescript,Node,Webpack,Maildev\" -p \"[{name}]\" -c \"blue,green,red,yellow\" \"tsc --watch\" \"nodemon\" \"webpack --watch --mode development\" \"maildev\"",
- "start": "yarn dist && node dist/main.js"
+ "start": "yarn build && node dist/src/main.js",
+ "test": "jest --verbose --runInBand"
},
"devDependencies": {
"@babel/core": "^7.9.0",
"@babel/preset-env": "^7.9.5",
+ "@fortawesome/fontawesome-free": "^5.14.0",
"@types/config": "^0.0.36",
"@types/express": "^4.17.6",
"@types/express-session": "^1.17.0",
+ "@types/feather-icons": "^4.7.0",
"@types/formidable": "^1.0.31",
"@types/jest": "^26.0.4",
"@types/mysql": "^2.15.15",
- "@types/node": "^14.0.27",
+ "@types/node": "^14.6.3",
"@types/nodemailer": "^6.4.0",
"@types/nunjucks": "^3.1.3",
"@types/ws": "^7.2.6",
+ "@typescript-eslint/eslint-plugin": "^4.3.0",
+ "@typescript-eslint/parser": "^4.3.0",
"babel-loader": "^8.1.0",
"concurrently": "^5.1.0",
- "css-loader": "^5.0.1",
+ "css-loader": "^5.0.0",
+ "eslint": "^7.10.0",
"feather-icons": "^4.28.0",
"file-loader": "^6.0.0",
"imagemin": "^7.0.1",
@@ -38,15 +46,16 @@
"imagemin-svgo": "^8.0.0",
"img-loader": "^3.0.1",
"jest": "^26.1.0",
- "mini-css-extract-plugin": "^1.3.1",
+ "mini-css-extract-plugin": "^1.2.1",
"node-sass": "^5.0.0",
"nodemon": "^2.0.3",
"sass-loader": "^10.0.1",
+ "terser-webpack-plugin": "^5.0.3",
"ts-jest": "^26.1.1",
+ "ts-loader": "^8.0.4",
"typescript": "^4.0.2",
- "uglifyjs-webpack-plugin": "^2.2.0",
- "webpack": "^5.6.0",
- "webpack-cli": "^4.2.0"
+ "webpack": "^5.3.2",
+ "webpack-cli": "^4.1.0"
},
"dependencies": {
"config": "^3.3.1",
diff --git a/src/App.ts b/src/App.ts
index 4b05f30..3a117cf 100644
--- a/src/App.ts
+++ b/src/App.ts
@@ -40,17 +40,17 @@ import URLRedirectController from "./controllers/URLRedirectController";
import LinkController from "./controllers/LinkController";
import BackendController from "wms-core/helpers/BackendController";
import RedirectBackComponent from "wms-core/components/RedirectBackComponent";
+import packageJson = require('../package.json');
export default class App extends Application {
- private readonly port: number;
- private magicLinkWebSocketListener?: MagicLinkWebSocketListener;
-
- constructor(port: number) {
- super(require('../package.json').version);
- this.port = port;
+ public constructor(
+ private readonly addr: string,
+ private readonly port: number,
+ ) {
+ super(packageJson.version);
}
- protected getMigrations(): Type[] {
+ protected getMigrations(): MigrationType[] {
return [
CreateMigrationsTable,
CreateLogsTable,
@@ -74,7 +74,7 @@ export default class App extends Application {
const redisComponent = new RedisComponent();
const mysqlComponent = new MysqlComponent();
- const expressAppComponent = new ExpressAppComponent(this.port);
+ const expressAppComponent = new ExpressAppComponent(this.addr, this.port);
this.use(expressAppComponent);
// Base
@@ -158,4 +158,4 @@ export default class App extends Application {
this.use(new FileController());
this.use(new URLRedirectController());
}
-}
\ No newline at end of file
+}
diff --git a/src/controllers/HomeController.ts b/src/controllers/HomeController.ts
new file mode 100644
index 0000000..d8a0379
--- /dev/null
+++ b/src/controllers/HomeController.ts
@@ -0,0 +1,25 @@
+import Controller from "swaf/Controller";
+import {Request, Response} from "express";
+
+export default class HomeController extends Controller {
+ public routes(): void {
+ this.get('/', this.getHome, 'home');
+ this.get('/about', this.getAbout, 'about');
+ this.get('/back', this.goBack, 'about');
+ }
+
+ protected async getHome(req: Request, res: Response): Promise {
+ res.render('home');
+ }
+
+ protected async getAbout(req: Request, res: Response): Promise {
+ res.render('about');
+ }
+
+ /**
+ * This is to test and assert that swaf extended types are available
+ */
+ protected async goBack(req: Request, res: Response): Promise {
+ res.redirectBack();
+ }
+}
diff --git a/src/main.ts b/src/main.ts
index 1706351..0033726 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,10 +1,20 @@
-import Logger from "wms-core/Logger";
+import {delimiter} from "path";
+
+// Load config from specified path or default + swaf/config (default defaults)
+process.env['NODE_CONFIG_DIR'] =
+ __dirname + '/../../node_modules/swaf/config/'
+ + delimiter
+ + (process.env['NODE_CONFIG_DIR'] || __dirname + '/../../config/');
+
+import {log} from "swaf/Logger";
import App from "./App";
import config from "config";
(async () => {
- const app = new App(config.get('port'));
+ log.debug('Config path:', process.env['NODE_CONFIG_DIR']);
+
+ const app = new App(config.get('listen_addr'), config.get('port'));
await app.start();
})().catch(err => {
- Logger.error(err);
-});
\ No newline at end of file
+ log.error(err);
+});
diff --git a/test/App.test.ts b/test/App.test.ts
index 451f0fb..d116a22 100644
--- a/test/App.test.ts
+++ b/test/App.test.ts
@@ -2,4 +2,4 @@ describe('Write your tests', () => {
test('Remove this when you have some tests', () => {
expect(false).toBe(true);
});
-});
\ No newline at end of file
+});
diff --git a/tsconfig.frontend.json b/tsconfig.frontend.json
new file mode 100644
index 0000000..4b5a3f5
--- /dev/null
+++ b/tsconfig.frontend.json
@@ -0,0 +1,18 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "public/js",
+ "target": "ES6",
+ "strict": true,
+ "lib": [
+ "es2020",
+ "DOM"
+ ],
+ "typeRoots": [
+ "./node_modules/@types"
+ ]
+ },
+ "include": [
+ "assets/ts/**/*"
+ ]
+}
diff --git a/tsconfig.json b/tsconfig.json
index a6bf63a..9034d02 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -6,14 +6,16 @@
"target": "ES6",
"strict": true,
"lib": [
- "es2020"
+ "es2020",
+ "DOM"
],
"typeRoots": [
"./node_modules/@types"
- ]
+ ],
+ "resolveJsonModule": true
},
"include": [
"src/**/*",
- "node_modules/wms-core"
+ "node_modules/swaf/types"
]
-}
\ No newline at end of file
+}
diff --git a/tsconfig.test.json b/tsconfig.test.json
new file mode 100644
index 0000000..897a6b4
--- /dev/null
+++ b/tsconfig.test.json
@@ -0,0 +1,14 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "typeRoots": [
+ "node_modules/@types",
+ "src/types",
+ "test/types"
+ ]
+ },
+ "include": [
+ "src/types/**/*",
+ "test/**/*"
+ ]
+}
\ No newline at end of file
diff --git a/views/about.njk b/views/about.njk
index a836b83..34bc644 100644
--- a/views/about.njk
+++ b/views/about.njk
@@ -11,4 +11,4 @@
Powered by wms-core
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/webpack.config.js b/webpack.config.js
index 478541b..598acfd 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,6 +1,6 @@
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
-const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
+const TerserPlugin = require('terser-webpack-plugin');
const dev = process.env.NODE_ENV === 'development';
@@ -48,6 +48,16 @@ const config = {
test: /\.(woff2?|eot|ttf|otf)$/i,
use: 'file-loader?name=../fonts/[name].[ext]',
},
+ {
+ test: /\.tsx?$/i,
+ use: {
+ loader: 'ts-loader',
+ options: {
+ configFile: 'tsconfig.frontend.json',
+ }
+ },
+ exclude: '/node_modules/'
+ },
{
test: /\.(png|jpe?g|gif|svg)$/i,
use: [
@@ -68,6 +78,9 @@ const config = {
}
],
},
+ resolve: {
+ extensions: ['.tsx', '.ts', '.js'],
+ },
plugins: [
new MiniCssExtractPlugin({
filename: '../css/[name].css',
@@ -77,8 +90,9 @@ const config = {
if (!dev) {
config.optimization = {
+ minimize: true,
minimizer: [
- new UglifyJSPlugin(),
+ new TerserPlugin(),
]
};
}