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(), ] }; }