Merge branch 'develop'
This commit is contained in:
commit
e172bf6618
3
.gitignore
vendored
3
.gitignore
vendored
@ -5,3 +5,6 @@ dist
|
|||||||
yarn-error.log
|
yarn-error.log
|
||||||
|
|
||||||
src/package.json
|
src/package.json
|
||||||
|
|
||||||
|
intermediates/
|
||||||
|
dist/
|
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"bundles": {
|
|
||||||
"app": "ts/app.ts",
|
|
||||||
"layout": "sass/layout.scss",
|
|
||||||
"error": "sass/error.scss",
|
|
||||||
"logo": "img/logo.svg",
|
|
||||||
"logo_png": "img/logox128.png",
|
|
||||||
"logo_png_xxl": "img/logox1024.png"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
/* vietnamese */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Nunito Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: swap;
|
|
||||||
src: local('Nunito Sans Light'), local('NunitoSans-Light'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8WAc5iU1EQVg.woff2) format('woff2');
|
|
||||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
|
||||||
}
|
|
||||||
/* latin-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Nunito Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: swap;
|
|
||||||
src: local('Nunito Sans Light'), local('NunitoSans-Light'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8WAc5jU1EQVg.woff2) format('woff2');
|
|
||||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
||||||
}
|
|
||||||
/* latin */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Nunito Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: swap;
|
|
||||||
src: local('Nunito Sans Light'), local('NunitoSans-Light'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8WAc5tU1E.woff2) format('woff2');
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
/* vietnamese */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Nunito Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: local('Nunito Sans Regular'), local('NunitoSans-Regular'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe0qMImSLYBIv1o4X1M8cceyI9tScg.woff2) format('woff2');
|
|
||||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
|
||||||
}
|
|
||||||
/* latin-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Nunito Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: local('Nunito Sans Regular'), local('NunitoSans-Regular'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe0qMImSLYBIv1o4X1M8ccezI9tScg.woff2) format('woff2');
|
|
||||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
||||||
}
|
|
||||||
/* latin */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Nunito Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: local('Nunito Sans Regular'), local('NunitoSans-Regular'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe0qMImSLYBIv1o4X1M8cce9I9s.woff2) format('woff2');
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
/* vietnamese */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Nunito Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
font-display: swap;
|
|
||||||
src: local('Nunito Sans Bold'), local('NunitoSans-Bold'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8GBs5iU1EQVg.woff2) format('woff2');
|
|
||||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
|
||||||
}
|
|
||||||
/* latin-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Nunito Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
font-display: swap;
|
|
||||||
src: local('Nunito Sans Bold'), local('NunitoSans-Bold'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8GBs5jU1EQVg.woff2) format('woff2');
|
|
||||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
||||||
}
|
|
||||||
/* latin */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Nunito Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
font-display: swap;
|
|
||||||
src: local('Nunito Sans Bold'), local('NunitoSans-Bold'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8GBs5tU1E.woff2) format('woff2');
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
$primary: darken(#242b33, 2%);
|
|
||||||
$primaryForeground: #f0f0f0;
|
|
||||||
$secondary: lighten(#00766c, 10%);
|
|
||||||
$secondaryForeground: $primaryForeground;
|
|
||||||
|
|
||||||
$backgroundColor: darken($primary, 4%);
|
|
||||||
$defaultTextColor: #ffffff;
|
|
||||||
|
|
||||||
$headerBackground: transparent;
|
|
||||||
$headerContainer: true;
|
|
||||||
$footerBackground: transparent;
|
|
||||||
$panelBackground: darken($backgroundColor, 3.2%);
|
|
||||||
$inputBackground: darken($panelBackground, 4%);
|
|
||||||
|
|
||||||
$info: #4499ff;
|
|
||||||
$infoText: darken($info, 42%);
|
|
||||||
$infoColor: desaturate($infoText, 50%);
|
|
||||||
|
|
||||||
$success: #55ff55;
|
|
||||||
$successText: darken($success, 45%);
|
|
||||||
$successColor: desaturate($successText, 50%);
|
|
||||||
|
|
||||||
$warning: #ffcc00;
|
|
||||||
$warningText: darken($warning, 30%);
|
|
||||||
$warningColor: desaturate($warningText, 50%);
|
|
||||||
|
|
||||||
$error: #ff0000;
|
|
||||||
$errorText: darken($error, 30%);
|
|
||||||
$errorColor: desaturate($errorText, 50%);
|
|
||||||
|
|
||||||
// Responsivity
|
|
||||||
$mobileThreshold: 632px;
|
|
||||||
$desktopThreshold: 940px;
|
|
@ -1 +0,0 @@
|
|||||||
@import "layout";
|
|
@ -1,90 +0,0 @@
|
|||||||
@import "layout";
|
|
||||||
|
|
||||||
header, footer {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.messages {
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-code {
|
|
||||||
font-size: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
font-size: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-instructions {
|
|
||||||
margin-top: 32px;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
|
||||||
margin-top: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "Oops";
|
|
||||||
position: absolute;
|
|
||||||
z-index: -1;
|
|
||||||
|
|
||||||
font-size: #{'min(50vh, 40vw)'};
|
|
||||||
opacity: 0.025;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact {
|
|
||||||
text-align: center;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 24px;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
a {
|
|
||||||
position: relative;
|
|
||||||
padding: 16px;
|
|
||||||
|
|
||||||
color: $defaultTextColor;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
|
|
||||||
background-image: url(../img/logo.svg);
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: center;
|
|
||||||
background-size: 64px;
|
|
||||||
|
|
||||||
opacity: 0.075;
|
|
||||||
filter: contrast(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,952 +0,0 @@
|
|||||||
@import "vars";
|
|
||||||
@import 'fonts';
|
|
||||||
@import "responsivity_tools";
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
margin: 0;
|
|
||||||
font-family: "Nunito Sans", sans-serif;
|
|
||||||
font-size: 16px;
|
|
||||||
|
|
||||||
color: $defaultTextColor;
|
|
||||||
background-color: $backgroundColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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-reverse;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
$headerHeight: 64px;
|
|
||||||
height: $headerHeight;
|
|
||||||
line-height: $headerHeight;
|
|
||||||
|
|
||||||
background: $headerBackground;
|
|
||||||
|
|
||||||
@if $headerContainer {
|
|
||||||
@include container;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: $mobileThreshold) {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
padding: 0 16px 0 8px;
|
|
||||||
font-size: 24px;
|
|
||||||
color: $defaultTextColor;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: lighten($defaultTextColor, 10%);
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: initial;
|
|
||||||
height: calc(#{$headerHeight} - 16px);
|
|
||||||
margin-right: 8px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
|
||||||
> ul {
|
|
||||||
position: fixed;
|
|
||||||
z-index: -1;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
height: 100%;
|
|
||||||
transform: translateX(-100%);
|
|
||||||
transition: transform ease-out 150ms;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin: 0;
|
|
||||||
padding: $headerHeight 8px 8px;
|
|
||||||
|
|
||||||
font-size: 20px;
|
|
||||||
|
|
||||||
background: $panelBackground;
|
|
||||||
|
|
||||||
li {
|
|
||||||
position: relative;
|
|
||||||
list-style: none;
|
|
||||||
margin-top: 8px;
|
|
||||||
|
|
||||||
a, button {
|
|
||||||
position: relative;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
height: auto;
|
|
||||||
padding: 8px;
|
|
||||||
|
|
||||||
border-radius: 3px;
|
|
||||||
|
|
||||||
&:hover, &:active {
|
|
||||||
&:not(button) {
|
|
||||||
background-color: rgba(255, 255, 255, 0.07);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.feather {
|
|
||||||
--icon-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tip {
|
|
||||||
position: static;
|
|
||||||
visibility: visible;
|
|
||||||
opacity: 1;
|
|
||||||
display: block;
|
|
||||||
height: auto;
|
|
||||||
margin-left: 8px;
|
|
||||||
padding: 0 0 0 4px;
|
|
||||||
transform: none;
|
|
||||||
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 16px;
|
|
||||||
|
|
||||||
color: inherit;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: inherit;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.tip {
|
|
||||||
visibility: visible;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity ease-out 100ms;
|
|
||||||
transition-delay: 150ms;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
margin: 0;
|
|
||||||
height: 32px;
|
|
||||||
|
|
||||||
.feather {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.auth-user {
|
|
||||||
img {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 3px;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown {
|
|
||||||
position: initial;
|
|
||||||
display: block;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> li:not(:first-child) {
|
|
||||||
border-top: 1px solid transparentize($defaultTextColor, 0.8);
|
|
||||||
padding-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.open {
|
|
||||||
transform: translateX(0%);
|
|
||||||
box-shadow: 0 0 5px darken($panelBackground, 20%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#menu-button {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
display: block;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0 16px;
|
|
||||||
line-height: $headerHeight;
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
background: transparent;
|
|
||||||
border-radius: 0;
|
|
||||||
|
|
||||||
.feather {
|
|
||||||
--icon-size: 28px;
|
|
||||||
margin: 0 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
border: 0;
|
|
||||||
border-bottom: 1px solid $defaultTextColor;
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: $mobileThreshold) {
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
nav {
|
|
||||||
#menu-button {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
position: static;
|
|
||||||
flex-direction: row;
|
|
||||||
transform: none;
|
|
||||||
padding: 0;
|
|
||||||
background: transparent;
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-left: 8px;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
a, button, .button {
|
|
||||||
.tip {
|
|
||||||
left: unset;
|
|
||||||
right: 4px;
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown {
|
|
||||||
position: absolute;
|
|
||||||
z-index: -1;
|
|
||||||
top: 100%;
|
|
||||||
right: 0;
|
|
||||||
display: none;
|
|
||||||
padding: 8px;
|
|
||||||
|
|
||||||
white-space: nowrap;
|
|
||||||
background: $panelBackground;
|
|
||||||
border-radius: 0 0 3px 3px;
|
|
||||||
|
|
||||||
box-shadow: 0 2px 2px transparentize(darken($panelBackground, 20%), 0.75);
|
|
||||||
border-top: 4px solid lighten($panelBackground, 5%);
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin-left: 0;
|
|
||||||
|
|
||||||
&:not(:first-child) {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .dropdown {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> li:not(:first-child) {
|
|
||||||
border-top: 0;
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body > footer {
|
|
||||||
padding: 8px;
|
|
||||||
margin-top: 8px;
|
|
||||||
text-align: center;
|
|
||||||
background-color: $footerBackground;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
flex: 1;
|
|
||||||
padding: 8px 0;
|
|
||||||
|
|
||||||
button, .button {
|
|
||||||
@include tip;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 32px;
|
|
||||||
|
|
||||||
& + p {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2 {
|
|
||||||
font-weight: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3, h4 {
|
|
||||||
font-weight: 300;
|
|
||||||
}
|
|
||||||
|
|
||||||
section > h2, .panel > h2 {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
position: relative;
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 4px;
|
|
||||||
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 1;
|
|
||||||
|
|
||||||
.feather {
|
|
||||||
margin: 0 16px 0 0;
|
|
||||||
opacity: 0.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: "";
|
|
||||||
flex: 1;
|
|
||||||
margin: 0 16px;
|
|
||||||
height: 0;
|
|
||||||
border-bottom: 1px solid $defaultTextColor;
|
|
||||||
opacity: 0.1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
section > hr, .panel > hr {
|
|
||||||
border: 0;
|
|
||||||
border-bottom: 1px solid $defaultTextColor;
|
|
||||||
opacity: 0.2;
|
|
||||||
|
|
||||||
margin: 8px 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: $secondary;
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: lighten($secondary, 30%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.feather.feather-external-link {
|
|
||||||
--icon-size: 16px;
|
|
||||||
margin-left: 4px;
|
|
||||||
margin-top: -3px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
|
||||||
padding: 8px 16px;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
.form-field:not(.hidden) {
|
|
||||||
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;
|
|
||||||
top: 20px;
|
|
||||||
user-select: none;
|
|
||||||
font-size: 16px;
|
|
||||||
opacity: 0.75;
|
|
||||||
|
|
||||||
transition-property: top, font-size;
|
|
||||||
transition-duration: 150ms;
|
|
||||||
transition-timing-function: ease-out;
|
|
||||||
|
|
||||||
cursor: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
[disabled] {
|
|
||||||
opacity: 0.5;
|
|
||||||
|
|
||||||
& ~ label {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input, select, textarea, .input-group {
|
|
||||||
z-index: 1;
|
|
||||||
border: 0;
|
|
||||||
color: $defaultTextColor;
|
|
||||||
background: transparent;
|
|
||||||
font-size: 16px;
|
|
||||||
|
|
||||||
&:focus, &:not([value=""]), &[type="file"] {
|
|
||||||
~ label {
|
|
||||||
top: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input, select, textarea, .form-display {
|
|
||||||
display: block;
|
|
||||||
padding: 32px 8px 8px 8px;
|
|
||||||
width: 100%;
|
|
||||||
height: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
-moz-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
|
|
||||||
&::-ms-expand {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
& + .feather {
|
|
||||||
position: absolute;
|
|
||||||
pointer-events: none;
|
|
||||||
right: 8px;
|
|
||||||
top: 30px;
|
|
||||||
|
|
||||||
transition: transform 150ms ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Temporary
|
|
||||||
&:focus + .feather {
|
|
||||||
transform: rotateX(180deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
resize: vertical;
|
|
||||||
min-height: 100px;
|
|
||||||
font-family: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type=color] {
|
|
||||||
height: calc(32px + 8px + 32px);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.inline {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
.control {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
flex-grow: 1;
|
|
||||||
|
|
||||||
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-grow: 1;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
div {
|
|
||||||
position: relative;
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
input {
|
|
||||||
width: 100%;
|
|
||||||
border: 0;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-fields {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: start;
|
|
||||||
margin: 16px auto;
|
|
||||||
|
|
||||||
.form-field {
|
|
||||||
flex: 1;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
> :not(.form-field) {
|
|
||||||
padding: 32px 8px 8px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
+ {
|
|
||||||
.error, .hint {
|
|
||||||
margin-top: -16px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-field, .inline-fields + {
|
|
||||||
.error, .hint {
|
|
||||||
padding: 2px;
|
|
||||||
text-align: left;
|
|
||||||
font-size: 14px;
|
|
||||||
|
|
||||||
.feather {
|
|
||||||
--icon-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
color: $error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button, .button {
|
|
||||||
display: inline-flex;
|
|
||||||
margin: 8px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bolder;
|
|
||||||
|
|
||||||
line-height: 16px;
|
|
||||||
|
|
||||||
.feather {
|
|
||||||
--icon-size: 16px;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feather.last {
|
|
||||||
margin-right: 0;
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&, &.primary {
|
|
||||||
color: $primaryForeground;
|
|
||||||
background-color: darken($secondary, 10%);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $secondary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.info {
|
|
||||||
background-color: $infoColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.success {
|
|
||||||
background-color: $successColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.warning {
|
|
||||||
background-color: $warningColor;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: lighten($warningColor, 10%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.error, &.danger {
|
|
||||||
background-color: $errorColor;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: lighten($errorColor, 10%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.transparent {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $primaryForeground;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ---
|
|
||||||
// --- Tables
|
|
||||||
// ---
|
|
||||||
td.actions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
form {
|
|
||||||
padding: 0;
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
button, .button {
|
|
||||||
margin: 0;
|
|
||||||
padding: 8px;
|
|
||||||
|
|
||||||
.feather {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> *:not(:first-child) {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table {
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
border-collapse: collapse;
|
|
||||||
|
|
||||||
th, td {
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
border-bottom: 1px solid #39434a;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr:nth-child(even) {
|
|
||||||
background-color: rgba(255, 255, 255, 0.03);
|
|
||||||
}
|
|
||||||
|
|
||||||
tr:hover {
|
|
||||||
background-color: rgba(255, 255, 255, 0.09);
|
|
||||||
}
|
|
||||||
|
|
||||||
thead tr:hover {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ---
|
|
||||||
// --- Breadcrumb widget
|
|
||||||
// ---
|
|
||||||
.breadcrumb {
|
|
||||||
list-style: none;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
margin: 0;
|
|
||||||
padding: 8px;
|
|
||||||
|
|
||||||
> *:not(:first-child)::before {
|
|
||||||
content: '›';
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ---
|
|
||||||
// --- Layout helpers
|
|
||||||
// ---
|
|
||||||
.center {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
|
||||||
position: relative;
|
|
||||||
margin: 16px 0 48px;
|
|
||||||
padding: 8px;
|
|
||||||
background-color: $panelBackground;
|
|
||||||
border-radius: 5px;
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 16px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .feather:first-child {
|
|
||||||
position: absolute;
|
|
||||||
--icon-size: 24px;
|
|
||||||
opacity: 0.1;
|
|
||||||
top: 8px;
|
|
||||||
left: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sub-panel {
|
|
||||||
margin: 32px 0;
|
|
||||||
padding: 1px 16px;
|
|
||||||
border: 2px solid lighten($panelBackground, 4%);
|
|
||||||
border-radius: 5px;
|
|
||||||
|
|
||||||
form > & {
|
|
||||||
margin: 32px -18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ---
|
|
||||||
// --- Feather
|
|
||||||
// ---
|
|
||||||
.feather {
|
|
||||||
display: inline-flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: var(--icon-size);
|
|
||||||
height: var(--icon-size);
|
|
||||||
|
|
||||||
--icon-size: 16px;
|
|
||||||
font-size: var(--icon-size);
|
|
||||||
stroke: currentColor;
|
|
||||||
stroke-width: 2;
|
|
||||||
stroke-linecap: square;
|
|
||||||
stroke-linejoin: miter;
|
|
||||||
fill: none;
|
|
||||||
vertical-align: middle;
|
|
||||||
|
|
||||||
h1 > &, h2 > &, h3 > & {
|
|
||||||
--icon-size: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---
|
|
||||||
// --- Helper classes
|
|
||||||
// ---
|
|
||||||
.message {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
padding: 8px 16px;
|
|
||||||
|
|
||||||
border-radius: 5px;
|
|
||||||
|
|
||||||
.feather {
|
|
||||||
--icon-size: 24px;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(&-discreet) {
|
|
||||||
background-color: rgba(255, 255, 255, 0.33);
|
|
||||||
|
|
||||||
&[data-type=info], &[data-type=question] {
|
|
||||||
background-color: $infoColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-type=success] {
|
|
||||||
background-color: $successColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-type=warning] {
|
|
||||||
background-color: $warningColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-type=error] {
|
|
||||||
background-color: $errorColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-discreet {
|
|
||||||
color: mix($panelBackground, #fff, 35%);
|
|
||||||
|
|
||||||
.feather {
|
|
||||||
--icon-size: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.messages .message:not(:last-child) {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container > .messages:first-child {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copyable-text {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
margin: 8px;
|
|
||||||
|
|
||||||
background-color: darken($backgroundColor, 2%);
|
|
||||||
border-radius: 5px;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
width: 0;
|
|
||||||
flex-grow: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy-button {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
|
|
||||||
.feather {
|
|
||||||
--icon-size: 20px;
|
|
||||||
margin: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar {
|
|
||||||
position: relative;
|
|
||||||
display: block;
|
|
||||||
margin: 8px;
|
|
||||||
padding: 4px;
|
|
||||||
background: #fff1;
|
|
||||||
border-radius: 5px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
.content {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
width: var(--progress);
|
|
||||||
height: 100%;
|
|
||||||
transition: width ease-out 150ms;
|
|
||||||
|
|
||||||
background: $secondary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-col-grow {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination {
|
|
||||||
ul {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
list-style: none;
|
|
||||||
padding: 8px;
|
|
||||||
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
li {
|
|
||||||
a, &.active, &.ellipsis {
|
|
||||||
display: block;
|
|
||||||
min-width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
padding: 4px;
|
|
||||||
|
|
||||||
line-height: 32px;
|
|
||||||
text-align:center;
|
|
||||||
|
|
||||||
&:hover:not(.active):not(.ellipsis) {
|
|
||||||
background-color: #fff5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
@import "vars";
|
|
||||||
|
|
||||||
@mixin container {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0 8px;
|
|
||||||
|
|
||||||
@media (min-width: $mobileThreshold) {
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: $desktopThreshold) {
|
|
||||||
width: $desktopThreshold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
@include container;
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
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;
|
|
@ -1,10 +0,0 @@
|
|||||||
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';
|
|
@ -1,15 +0,0 @@
|
|||||||
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');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,11 +0,0 @@
|
|||||||
import feather from "feather-icons";
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
document.querySelectorAll('a[target="_blank"]').forEach(el => {
|
|
||||||
if (!el.classList.contains('no-icon')) {
|
|
||||||
el.innerHTML += `<i data-feather="external-link"></i>`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
feather.replace();
|
|
||||||
});
|
|
@ -1,4 +0,0 @@
|
|||||||
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';
|
|
@ -1,43 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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<HTMLInputElement | HTMLTextAreaElement>('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<unknown> },
|
|
||||||
): 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 = `<i data-feather="x-circle"></i> ${messages[fieldName].message}`;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
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');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,26 +0,0 @@
|
|||||||
import feather from "feather-icons";
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const messageTypeToIcon: { [p: string]: string } = {
|
|
||||||
info: 'info',
|
|
||||||
success: 'check',
|
|
||||||
warning: 'alert-triangle',
|
|
||||||
error: 'x-circle',
|
|
||||||
question: 'help-circle',
|
|
||||||
};
|
|
||||||
|
|
||||||
document.querySelectorAll<HTMLElement>('.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();
|
|
||||||
|
|
||||||
if (svgContainer.firstChild) el.insertBefore(svgContainer.firstChild, icon);
|
|
||||||
icon.remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
feather.replace();
|
|
||||||
});
|
|
@ -1,31 +0,0 @@
|
|||||||
export function updateTooltips(): void {
|
|
||||||
console.debug('Updating tooltips');
|
|
||||||
const elements = document.querySelectorAll<HTMLElement>('.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();
|
|
||||||
});
|
|
||||||
});
|
|
47
package.json
47
package.json
@ -10,58 +10,47 @@
|
|||||||
"test": "jest --verbose --runInBand",
|
"test": "jest --verbose --runInBand",
|
||||||
"clean": "node scripts/clean.js",
|
"clean": "node scripts/clean.js",
|
||||||
"prepare-sources": "node scripts/prepare-sources.js",
|
"prepare-sources": "node scripts/prepare-sources.js",
|
||||||
"compile": "yarn clean && tsc",
|
"compile": "yarn clean && yarn prepare-sources && tsc --build",
|
||||||
"build": "yarn prepare-sources && yarn compile && webpack --mode production",
|
"build": "yarn compile && node . pre-compile-views && node scripts/dist.js",
|
||||||
"dev": "yarn prepare-sources && concurrently -k -n \"Typescript,Node,Webpack,Maildev\" -p \"[{name}]\" -c \"blue,green,red,yellow\" \"tsc --watch\" \"nodemon\" \"webpack --watch --mode development\" \"maildev\"",
|
"build-production": "NODE_ENV=production yarn build",
|
||||||
"start": "yarn build && node",
|
"dev": "yarn compile && concurrently -k -n \"Maildev,Typescript,ViewPreCompile,Node\" -p \"[{name}]\" -c \"yellow,blue,red,green\" \"maildev\" \"tsc --build --watch --preserveWatchOutput\" \"nodemon -i public -i intermediates -- pre-compile-views --watch\" \"nodemon -i public -i intermediates\"",
|
||||||
"lint": "eslint ."
|
"lint": "eslint .",
|
||||||
|
"start": "yarn build-production && node ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.9.0",
|
"@tsconfig/svelte": "^2.0.1",
|
||||||
"@babel/preset-env": "^7.9.5",
|
"@types/config": "^0.0.40",
|
||||||
"@fortawesome/fontawesome-free": "^5.14.0",
|
|
||||||
"@types/config": "^0.0.38",
|
|
||||||
"@types/express": "^4.17.6",
|
"@types/express": "^4.17.6",
|
||||||
"@types/express-session": "^1.17.0",
|
"@types/express-session": "^1.17.0",
|
||||||
"@types/feather-icons": "^4.7.0",
|
"@types/feather-icons": "^4.7.0",
|
||||||
"@types/formidable": "^1.0.31",
|
"@types/formidable": "^2.0.0",
|
||||||
"@types/jest": "^26.0.4",
|
"@types/jest": "^27.0.3",
|
||||||
"@types/mysql": "^2.15.15",
|
"@types/mysql": "^2.15.15",
|
||||||
"@types/node": "^15.0.1",
|
"@types/node": "^16.11.9",
|
||||||
"@types/nodemailer": "^6.4.0",
|
"@types/nodemailer": "^6.4.0",
|
||||||
"@types/nunjucks": "^3.1.3",
|
"@types/nunjucks": "^3.1.3",
|
||||||
"@types/ws": "^7.2.6",
|
"@types/ws": "^8.2.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.3.0",
|
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||||
"@typescript-eslint/parser": "^4.3.0",
|
"@typescript-eslint/parser": "^5.4.0",
|
||||||
"babel-loader": "^8.1.0",
|
|
||||||
"concurrently": "^6.0.0",
|
"concurrently": "^6.0.0",
|
||||||
"css-loader": "^5.0.0",
|
"eslint": "^8.2.0",
|
||||||
"eslint": "^7.10.0",
|
|
||||||
"feather-icons": "^4.28.0",
|
"feather-icons": "^4.28.0",
|
||||||
"file-loader": "^6.0.0",
|
|
||||||
"image-minimizer-webpack-plugin": "^2.2.0",
|
|
||||||
"imagemin-gifsicle": "^7.0.0",
|
"imagemin-gifsicle": "^7.0.0",
|
||||||
"imagemin-mozjpeg": "^9.0.0",
|
"imagemin-mozjpeg": "^9.0.0",
|
||||||
"imagemin-pngquant": "^9.0.2",
|
"imagemin-pngquant": "^9.0.2",
|
||||||
"imagemin-svgo": "^9.0.0",
|
"imagemin-svgo": "^10.0.0",
|
||||||
"imagemin-webp": "^6.0.0",
|
"imagemin-webp": "^6.0.0",
|
||||||
"jest": "^27.0.4",
|
"jest": "^27.0.4",
|
||||||
"maildev": "^1.1.0",
|
"maildev": "^1.1.0",
|
||||||
"mini-css-extract-plugin": "^1.2.1",
|
|
||||||
"nodemon": "^2.0.3",
|
"nodemon": "^2.0.3",
|
||||||
"sass": "^1.32.12",
|
"sass": "^1.32.12",
|
||||||
"sass-loader": "^12.0.0",
|
|
||||||
"svgo": "^2.3.0",
|
"svgo": "^2.3.0",
|
||||||
"terser-webpack-plugin": "^5.0.3",
|
|
||||||
"ts-jest": "^27.0.3",
|
"ts-jest": "^27.0.3",
|
||||||
"ts-loader": "^9.1.0",
|
"typescript": "^4.0.2"
|
||||||
"typescript": "^4.0.2",
|
|
||||||
"webpack": "^5.3.2",
|
|
||||||
"webpack-cli": "^4.1.0"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"config": "^3.3.1",
|
"config": "^3.3.1",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"swaf": "^0.23.0"
|
"swaf": "^0.24.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
22
scripts/_functions.js
Normal file
22
scripts/_functions.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
function copyRecursively(file, destination) {
|
||||||
|
const target = path.join(destination, path.basename(file));
|
||||||
|
if (fs.statSync(file).isDirectory()) {
|
||||||
|
console.log('mkdir', target);
|
||||||
|
|
||||||
|
fs.mkdirSync(target, {recursive: true});
|
||||||
|
fs.readdirSync(file).forEach(f => {
|
||||||
|
copyRecursively(path.join(file, f), target);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('> cp ', target);
|
||||||
|
|
||||||
|
fs.copyFileSync(file, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
copyRecursively,
|
||||||
|
};
|
@ -1,7 +1,9 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
[
|
[
|
||||||
|
'intermediates',
|
||||||
'dist',
|
'dist',
|
||||||
|
'public',
|
||||||
].forEach(file => {
|
].forEach(file => {
|
||||||
if (fs.existsSync(file)) {
|
if (fs.existsSync(file)) {
|
||||||
console.log('Cleaning', file, '...');
|
console.log('Cleaning', file, '...');
|
||||||
|
22
scripts/dist.js
Normal file
22
scripts/dist.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const {copyRecursively} = require('./_functions.js');
|
||||||
|
|
||||||
|
|
||||||
|
[
|
||||||
|
'yarn.lock',
|
||||||
|
'README.md',
|
||||||
|
'config/',
|
||||||
|
].forEach(file => {
|
||||||
|
copyRecursively(file, 'dist');
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.mkdirSync('dist/types', {recursive: true});
|
||||||
|
|
||||||
|
fs.readdirSync('src/types').forEach(file => {
|
||||||
|
copyRecursively(path.join('src/types', file), 'dist/types');
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.readdirSync('src/assets').forEach(file => {
|
||||||
|
copyRecursively(path.join('src/assets', file), 'dist/assets');
|
||||||
|
});
|
@ -1,4 +1,28 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
fs.copyFileSync('package.json', path.join('src', 'package.json'));
|
// These folders must exist for nodemon not to loop indefinitely.
|
||||||
|
[
|
||||||
|
'public',
|
||||||
|
'dist',
|
||||||
|
'intermediates',
|
||||||
|
'intermediates/assets',
|
||||||
|
].forEach(dir => {
|
||||||
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Symlink to build/common
|
||||||
|
const commonLocalSymlink = path.resolve('intermediates/common-local');
|
||||||
|
if (!fs.existsSync(commonLocalSymlink)) {
|
||||||
|
const target = path.resolve('dist/common-local');
|
||||||
|
fs.symlinkSync(target, commonLocalSymlink);
|
||||||
|
}
|
||||||
|
|
||||||
|
const commonSymlink = path.resolve('intermediates/common');
|
||||||
|
if (!fs.existsSync(commonSymlink)) {
|
||||||
|
const target = path.resolve('node_modules/swaf/common');
|
||||||
|
fs.symlinkSync(target, commonSymlink);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy package.json
|
||||||
|
fs.copyFileSync('package.json', 'dist/package.json');
|
||||||
|
34
src/App.ts
34
src/App.ts
@ -2,7 +2,6 @@ import Application from "swaf/Application";
|
|||||||
import Migration, {MigrationType} from "swaf/db/Migration";
|
import Migration, {MigrationType} from "swaf/db/Migration";
|
||||||
import CreateMigrationsTable from "swaf/migrations/CreateMigrationsTable";
|
import CreateMigrationsTable from "swaf/migrations/CreateMigrationsTable";
|
||||||
import ExpressAppComponent from "swaf/components/ExpressAppComponent";
|
import ExpressAppComponent from "swaf/components/ExpressAppComponent";
|
||||||
import NunjucksComponent from "swaf/components/NunjucksComponent";
|
|
||||||
import MysqlComponent from "swaf/components/MysqlComponent";
|
import MysqlComponent from "swaf/components/MysqlComponent";
|
||||||
import LogRequestsComponent from "swaf/components/LogRequestsComponent";
|
import LogRequestsComponent from "swaf/components/LogRequestsComponent";
|
||||||
import RedisComponent from "swaf/components/RedisComponent";
|
import RedisComponent from "swaf/components/RedisComponent";
|
||||||
@ -18,14 +17,22 @@ import AutoUpdateComponent from "swaf/components/AutoUpdateComponent";
|
|||||||
import DummyMigration from "swaf/migrations/DummyMigration";
|
import DummyMigration from "swaf/migrations/DummyMigration";
|
||||||
import DropLegacyLogsTable from "swaf/migrations/DropLegacyLogsTable";
|
import DropLegacyLogsTable from "swaf/migrations/DropLegacyLogsTable";
|
||||||
import PreviousUrlComponent from "swaf/components/PreviousUrlComponent";
|
import PreviousUrlComponent from "swaf/components/PreviousUrlComponent";
|
||||||
import packageJson = require('./package.json');
|
import MailViewEngine from "swaf/frontend/MailViewEngine";
|
||||||
|
import AssetCompiler from "swaf/frontend/AssetCompiler";
|
||||||
|
import FrontendToolsComponent from "swaf/components/FrontendToolsComponent";
|
||||||
|
import CopyAssetPreCompiler from "swaf/frontend/CopyAssetPreCompiler";
|
||||||
|
import ScssAssetPreCompiler from "swaf/frontend/ScssAssetPreCompiler";
|
||||||
|
import TypeScriptPreCompiler from "swaf/frontend/TypeScriptPreCompiler";
|
||||||
|
import SvelteViewEngine from "swaf/frontend/SvelteViewEngine";
|
||||||
|
import NunjucksViewEngine from "swaf/frontend/NunjucksViewEngine";
|
||||||
|
|
||||||
export default class App extends Application {
|
export default class App extends Application {
|
||||||
public constructor(
|
public constructor(
|
||||||
|
version: string,
|
||||||
private readonly addr: string,
|
private readonly addr: string,
|
||||||
private readonly port: number,
|
private readonly port: number,
|
||||||
) {
|
) {
|
||||||
super(packageJson.version);
|
super(version);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getMigrations(): MigrationType<Migration>[] {
|
protected getMigrations(): MigrationType<Migration>[] {
|
||||||
@ -52,18 +59,27 @@ export default class App extends Application {
|
|||||||
this.use(new ServeStaticDirectoryComponent('node_modules/feather-icons/dist', '/icons'));
|
this.use(new ServeStaticDirectoryComponent('node_modules/feather-icons/dist', '/icons'));
|
||||||
|
|
||||||
// Dynamic views and routes
|
// Dynamic views and routes
|
||||||
this.use(new NunjucksComponent());
|
const intermediateDirectory = 'intermediates/assets';
|
||||||
|
const assetCompiler = new AssetCompiler(intermediateDirectory, 'public');
|
||||||
|
const additionalViewPaths = ['test/assets'];
|
||||||
|
this.use(new FrontendToolsComponent(
|
||||||
|
assetCompiler,
|
||||||
|
new CopyAssetPreCompiler(intermediateDirectory, '', 'json', additionalViewPaths, false),
|
||||||
|
new ScssAssetPreCompiler(intermediateDirectory, assetCompiler.targetDir, 'scss', additionalViewPaths),
|
||||||
|
new CopyAssetPreCompiler(intermediateDirectory, 'img', 'svg', additionalViewPaths, true),
|
||||||
|
new TypeScriptPreCompiler(intermediateDirectory, additionalViewPaths),
|
||||||
|
new SvelteViewEngine(intermediateDirectory, ...additionalViewPaths),
|
||||||
|
new NunjucksViewEngine(intermediateDirectory, ...additionalViewPaths),
|
||||||
|
));
|
||||||
this.use(new PreviousUrlComponent());
|
this.use(new PreviousUrlComponent());
|
||||||
|
|
||||||
// Maintenance
|
// Maintenance
|
||||||
this.use(new MaintenanceComponent(this, () => {
|
this.use(new MaintenanceComponent());
|
||||||
return this.as(RedisComponent).canServe() && this.as(MysqlComponent).canServe();
|
|
||||||
}));
|
|
||||||
this.use(new AutoUpdateComponent());
|
this.use(new AutoUpdateComponent());
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
this.use(new MysqlComponent());
|
this.use(new MysqlComponent());
|
||||||
this.use(new MailComponent());
|
this.use(new MailComponent(new MailViewEngine(intermediateDirectory, ...additionalViewPaths)));
|
||||||
|
|
||||||
// Session
|
// Session
|
||||||
this.use(new RedisComponent());
|
this.use(new RedisComponent());
|
||||||
@ -76,7 +92,7 @@ export default class App extends Application {
|
|||||||
this.use(new CsrfProtectionComponent());
|
this.use(new CsrfProtectionComponent());
|
||||||
|
|
||||||
// WebSocket server
|
// WebSocket server
|
||||||
this.use(new WebSocketServerComponent(this, this.as(ExpressAppComponent), this.as(RedisComponent)));
|
this.use(new WebSocketServerComponent());
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerWebSocketListeners() {
|
private registerWebSocketListeners() {
|
||||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
3
src/assets/package.json
Normal file
3
src/assets/package.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type": "commonjs"
|
||||||
|
}
|
6
src/assets/ts/tsconfig.eslint.json
Normal file
6
src/assets/ts/tsconfig.eslint.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"include": [
|
||||||
|
"./**/*"
|
||||||
|
]
|
||||||
|
}
|
27
src/assets/ts/tsconfig.json
Normal file
27
src/assets/ts/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"baseUrl": "../../../intermediates/assets",
|
||||||
|
"rootDir": "../../../intermediates/assets/ts-source",
|
||||||
|
"sourceRoot": "../../../intermediates/assets/ts-source",
|
||||||
|
"outDir": "../../../intermediates/assets/ts",
|
||||||
|
"declaration": false,
|
||||||
|
|
||||||
|
"typeRoots": [],
|
||||||
|
"resolveJsonModule": false,
|
||||||
|
"lib": [
|
||||||
|
"es2020",
|
||||||
|
"DOM"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"../../../intermediates/assets/ts-source/**/*"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../../common"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
15
src/assets/views/tsconfig.json
Normal file
15
src/assets/views/tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "public/js",
|
||||||
|
"rootDir": "../../../intermediates/assets",
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/assets/ts/**/*"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../../common"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
1
src/common/dummy.ts
Normal file
1
src/common/dummy.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
console.log('common code between back and front');
|
3
src/common/package.json
Normal file
3
src/common/package.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type": "commonjs"
|
||||||
|
}
|
20
src/common/tsconfig.json
Normal file
20
src/common/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
|
||||||
|
"module": "CommonJS",
|
||||||
|
|
||||||
|
"baseUrl": "../../dist/common-local",
|
||||||
|
"rootDir": "./",
|
||||||
|
"sourceRoot": "./",
|
||||||
|
"outDir": "../../dist/common-local",
|
||||||
|
|
||||||
|
"typeRoots": [
|
||||||
|
"src/types"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./**/*"
|
||||||
|
]
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import Controller from "swaf/Controller";
|
import Controller from "swaf/Controller";
|
||||||
import {Request, Response} from "express";
|
import {Request, Response} from "express";
|
||||||
|
import {route} from "swaf/common/Routing";
|
||||||
|
|
||||||
export default class HomeController extends Controller {
|
export default class HomeController extends Controller {
|
||||||
public routes(): void {
|
public routes(): void {
|
||||||
@ -20,6 +21,6 @@ export default class HomeController extends Controller {
|
|||||||
* This is to test and assert that swaf extended types are available
|
* This is to test and assert that swaf extended types are available
|
||||||
*/
|
*/
|
||||||
protected async goBack(req: Request, res: Response): Promise<void> {
|
protected async goBack(req: Request, res: Response): Promise<void> {
|
||||||
res.redirect(req.getPreviousUrl() || Controller.route('home'));
|
res.redirect(req.getPreviousUrl() || route('home'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,11 +9,18 @@ process.env['NODE_CONFIG_DIR'] =
|
|||||||
import {logger} from "swaf/Logger";
|
import {logger} from "swaf/Logger";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import config from "config";
|
import config from "config";
|
||||||
|
import {promises as fs} from "fs";
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
logger.debug('Config path:', process.env['NODE_CONFIG_DIR']);
|
logger.debug('Config path:', process.env['NODE_CONFIG_DIR']);
|
||||||
|
|
||||||
const app = new App(config.get<string>('listen_addr'), config.get<number>('port'));
|
const packageJson = JSON.parse((await fs.readFile('package.json')).toString());
|
||||||
|
|
||||||
|
const app = new App(
|
||||||
|
packageJson.version,
|
||||||
|
config.get<string>('app.listen_addr'),
|
||||||
|
config.get<number>('app.port'),
|
||||||
|
);
|
||||||
await app.start();
|
await app.start();
|
||||||
})().catch(err => {
|
})().catch(err => {
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
|
30
src/tsconfig.json
Normal file
30
src/tsconfig.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
|
||||||
|
"module": "CommonJS",
|
||||||
|
|
||||||
|
"baseUrl": "../dist",
|
||||||
|
"rootDir": "./",
|
||||||
|
"sourceRoot": "./",
|
||||||
|
"outDir": "../dist",
|
||||||
|
|
||||||
|
"typeRoots": [
|
||||||
|
"src/types"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./**/*",
|
||||||
|
"../node_modules/swaf/types"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"./assets/**/*",
|
||||||
|
"./common/**/*"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./common"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
0
src/types/.gitkeep
Normal file
0
src/types/.gitkeep
Normal file
@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "public/js",
|
|
||||||
"rootDir": "./assets",
|
|
||||||
"target": "ES6",
|
|
||||||
"strict": true,
|
|
||||||
"lib": [
|
|
||||||
"es2020",
|
|
||||||
"DOM"
|
|
||||||
],
|
|
||||||
"typeRoots": [
|
|
||||||
"./node_modules/@types"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"assets/ts/**/*"
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,22 +1,43 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "CommonJS",
|
"target": "ESNext",
|
||||||
"esModuleInterop": true,
|
"module": "ESNext",
|
||||||
"outDir": "dist",
|
"declaration": true,
|
||||||
"rootDir": "./src",
|
"stripInternal": true,
|
||||||
"target": "ES6",
|
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"baseUrl": "dist",
|
||||||
|
"inlineSourceMap": true,
|
||||||
|
"inlineSources": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
|
||||||
|
"typeRoots": [
|
||||||
|
"node_modules/@types",
|
||||||
|
"src/types"
|
||||||
|
],
|
||||||
"lib": [
|
"lib": [
|
||||||
"es2020",
|
"es2020",
|
||||||
"DOM"
|
"dom"
|
||||||
],
|
],
|
||||||
"typeRoots": [
|
"resolveJsonModule": true,
|
||||||
"./node_modules/@types"
|
"skipLibCheck": true,
|
||||||
],
|
"allowJs": true
|
||||||
"resolveJsonModule": true
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [],
|
||||||
"src/**/*",
|
"references": [
|
||||||
"node_modules/swaf/types"
|
{
|
||||||
|
"path": "src",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/assets/ts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "src/assets/views",
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -11,4 +11,4 @@
|
|||||||
"src/types/**/*",
|
"src/types/**/*",
|
||||||
"test/**/*"
|
"test/**/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
{% extends 'layouts/base.njk' %}
|
|
||||||
|
|
||||||
{% set title = app.name + ' - About us' %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
<h1>Very interesting</h1>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<section class="panel">
|
|
||||||
<h2>This is us</h2>
|
|
||||||
<p class="center">And we like swaf!</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -1,7 +0,0 @@
|
|||||||
{% extends 'layouts/base.njk' %}
|
|
||||||
|
|
||||||
{% set title = app.name + ' - Hello world!' %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
<h1>Hello world!</h1>
|
|
||||||
{% endblock %}
|
|
@ -1,41 +0,0 @@
|
|||||||
{% extends 'layouts/barebone.njk' %}
|
|
||||||
{% import 'macros.njk' as macros %}
|
|
||||||
|
|
||||||
{% block _stylesheets %}
|
|
||||||
{{ super() }}
|
|
||||||
<link rel="stylesheet" href="/css/app.css">
|
|
||||||
{% block stylesheets %}{% endblock %}
|
|
||||||
{% endblock %}
|
|
||||||
{% block _scripts %}
|
|
||||||
{{ super() }}
|
|
||||||
{% block scripts %}{% endblock %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block header %}
|
|
||||||
<a href="/" class="logo"><img src="/img/logo.svg" alt="Logo"> {{ app.name }}</a>
|
|
||||||
<nav>
|
|
||||||
<button id="menu-button"><i data-feather="menu"></i></button>
|
|
||||||
<ul id="main-menu">
|
|
||||||
<li><a href="{{ route('about') }}"><i data-feather="info"></i> <span class="tip">About</span></a></li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block _body %}
|
|
||||||
<div class="container">
|
|
||||||
{{ macros.messages(flash) }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
{% if h1 %}
|
|
||||||
<h1>{{ h1 }}</h1>
|
|
||||||
{% endif %}
|
|
||||||
{% if subtitle %}
|
|
||||||
<p>{{ subtitle }}</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% block body %}{% endblock %}
|
|
||||||
</main>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block footer %}{{ app.name }} v{{ app_version }} - all rights reserved.{% endblock %}
|
|
@ -1,121 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
|
||||||
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
|
|
||||||
const {extendDefaultPlugins} = require("svgo");
|
|
||||||
const TerserPlugin = require('terser-webpack-plugin');
|
|
||||||
|
|
||||||
const dev = process.env.NODE_ENV === 'development';
|
|
||||||
|
|
||||||
const userConfig = require('./assets/config.json');
|
|
||||||
for (const b in userConfig.bundles) {
|
|
||||||
if (userConfig.bundles.hasOwnProperty(b)) {
|
|
||||||
userConfig.bundles[b] = `./assets/${userConfig.bundles[b]}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
entry: userConfig.bundles,
|
|
||||||
output: {
|
|
||||||
path: path.resolve(__dirname, 'public/js'),
|
|
||||||
filename: '[name].js'
|
|
||||||
},
|
|
||||||
devtool: dev ? 'eval-source-map' : undefined,
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.js$/i,
|
|
||||||
use: [
|
|
||||||
{
|
|
||||||
loader: 'babel-loader',
|
|
||||||
options: {
|
|
||||||
presets: ['@babel/preset-env'],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.s[ac]ss$/i,
|
|
||||||
use: [
|
|
||||||
{
|
|
||||||
loader: MiniCssExtractPlugin.loader,
|
|
||||||
options: {
|
|
||||||
publicPath: '/',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'css-loader',
|
|
||||||
'sass-loader',
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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: [
|
|
||||||
'file-loader?name=../img/[name].[ext]',
|
|
||||||
],
|
|
||||||
type: 'asset',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.tsx', '.ts', '.js'],
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new MiniCssExtractPlugin({
|
|
||||||
filename: '../css/[name].css',
|
|
||||||
}),
|
|
||||||
new ImageMinimizerPlugin({
|
|
||||||
minimizerOptions: {
|
|
||||||
// Lossless optimization with custom option
|
|
||||||
// Feel free to experiment with options for better result for you
|
|
||||||
plugins: [
|
|
||||||
["gifsicle", {}],
|
|
||||||
["mozjpeg", {}],
|
|
||||||
["pngquant", {}],
|
|
||||||
["webp", {quality: 90}],
|
|
||||||
// Svgo configuration here https://github.com/svg/svgo#configuration
|
|
||||||
[
|
|
||||||
"svgo",
|
|
||||||
{
|
|
||||||
plugins: extendDefaultPlugins([
|
|
||||||
{
|
|
||||||
name: "removeViewBox",
|
|
||||||
active: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "addAttributesToSVGElement",
|
|
||||||
params: {
|
|
||||||
attributes: [{xmlns: "http://www.w3.org/2000/svg"}],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!dev) {
|
|
||||||
config.optimization = {
|
|
||||||
minimize: true,
|
|
||||||
minimizer: [
|
|
||||||
new TerserPlugin(),
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = config;
|
|
Loading…
Reference in New Issue
Block a user