Initial commit. Files are uploaded to the filesystem.
This commit is contained in:
commit
745b00dcdc
|
@ -0,0 +1,14 @@
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# Unix-style newlines with a newline ending every file
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
# Matches multiple files with brace expansion notation
|
||||||
|
# Set default charset
|
||||||
|
[*.js]
|
||||||
|
charset = utf-8
|
|
@ -0,0 +1,12 @@
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: { browser: false, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
],
|
||||||
|
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: [],
|
||||||
|
rules: {},
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
config/*
|
||||||
|
uploads/full/*
|
||||||
|
uploads/thumbs/*
|
||||||
|
uploads/temp/*
|
||||||
|
node_modules
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Pubload
|
||||||
|
Straight-forward self-hosted file sharing
|
|
@ -0,0 +1,10 @@
|
||||||
|
@custom-media --small-60 (max-width: 350px);
|
||||||
|
@custom-media --small-50 (max-width: 410px);
|
||||||
|
@custom-media --small-40 (max-width: 480px);
|
||||||
|
@custom-media --small-30 (max-width: 540px);
|
||||||
|
@custom-media --small-20 (max-width: 650px);
|
||||||
|
@custom-media --small-15 (max-width: 720px);
|
||||||
|
@custom-media --small-10 (max-width: 768px);
|
||||||
|
@custom-media --small (max-width: 900px);
|
||||||
|
@custom-media --compact (max-width: 1200px);
|
||||||
|
@custom-media --big (max-width: 1500px);
|
|
@ -0,0 +1,6 @@
|
||||||
|
code {
|
||||||
|
font-family: monospace;
|
||||||
|
background-color: #eaeaea;
|
||||||
|
padding: 3px 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
.form {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 30rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
|
||||||
|
.input {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-column {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-heading {
|
||||||
|
color: var(--primary-light-10);
|
||||||
|
margin: 0 0 .75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-error {
|
||||||
|
background: var(--error);
|
||||||
|
color: var(--text-light);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: .25rem;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
|
@ -0,0 +1,297 @@
|
||||||
|
.input {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: .5rem .75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
flex-basis: 0;
|
||||||
|
color: inherit;
|
||||||
|
border: solid 1px var(--glass-weak-30);
|
||||||
|
border-radius: .25rem;
|
||||||
|
background: var(--background-base-10);
|
||||||
|
font: inherit;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-light-10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-inline {
|
||||||
|
border-radius: 0;
|
||||||
|
border: none;
|
||||||
|
border-bottom: solid 1px var(--grey-light-30);
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: stretch;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: .5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: .25rem;
|
||||||
|
background: var(--background);
|
||||||
|
box-shadow: 0 0 3px var(--shadow-weak-30);
|
||||||
|
color: var(--glass);
|
||||||
|
font-size: .9rem;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
height: auto;
|
||||||
|
padding: 0 .75rem 0 .25rem;
|
||||||
|
fill: var(--glass);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--primary);
|
||||||
|
color: var(--text-light);
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
fill: var(--text-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-label {
|
||||||
|
margin-right: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-submit {
|
||||||
|
background: var(--primary);
|
||||||
|
color: var(--text-light);
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: var(--primary-dark-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background: var(--glass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-primary {
|
||||||
|
background: var(--primary);
|
||||||
|
color: var(--text-light);
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: var(--primary-dark-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background: var(--glass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-inline {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: var(--grey-dark-20);
|
||||||
|
padding: 0 0 .5rem 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
fill: var(--grey-dark-20);
|
||||||
|
padding-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background: none;
|
||||||
|
color: var(--primary);
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
fill: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-cancel {
|
||||||
|
background: none;
|
||||||
|
color: var(--glass);
|
||||||
|
font-weight: normal;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
color: var(--error);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
color: var(--glass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio {
|
||||||
|
margin: 0 .5rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-values {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-split {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-container,
|
||||||
|
.range-container {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
align-items: center;
|
||||||
|
padding: .5rem 0;
|
||||||
|
|
||||||
|
&.on {
|
||||||
|
.toggle-label.on {
|
||||||
|
color: var(--enabled);
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
fill: var(--enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
background-color: var(--enabled-background);
|
||||||
|
|
||||||
|
&::-webkit-slider-thumb {
|
||||||
|
background: var(--enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-range-thumb {
|
||||||
|
background: var(--enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.off {
|
||||||
|
.toggle-label.off {
|
||||||
|
color: var(--disabled);
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
fill: var(--disabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
background-color: var(--disabled-background);
|
||||||
|
|
||||||
|
&::-webkit-slider-thumb {
|
||||||
|
background: var(--disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-range-thumb {
|
||||||
|
background: var(--disabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 1.5rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0 .5rem;
|
||||||
|
color: var(--glass);
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: .9rem;
|
||||||
|
|
||||||
|
&.on {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
fill: var(--glass);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.on {
|
||||||
|
color: var(--enabled);
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
fill: var(--enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.off {
|
||||||
|
color: var(--disabled);
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
fill: var(--disabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: .5rem 0 .25rem .25rem;
|
||||||
|
color: var(--glass);
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: .9rem;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
margin: 0 .75rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin: 0 .5rem 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
width: 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 1.25rem;
|
||||||
|
appearance: none;
|
||||||
|
border-radius: 1rem;
|
||||||
|
background-color: var(--glass-weak-40);
|
||||||
|
background-image: radial-gradient(circle, var(--glass-weak-10) .3rem, transparent calc(.3rem + 1px));
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&::-webkit-slider-thumb {
|
||||||
|
appearance: none;
|
||||||
|
background: var(--disabled-handle);
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
border-radius: .625rem;
|
||||||
|
box-shadow: 0 0 3px var(--shadow-weak-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-range-thumb {
|
||||||
|
appearance: none;
|
||||||
|
background: var(--disabled-handle);
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: .625rem;
|
||||||
|
box-shadow: 0 0 3px var(--shadow-weak-10);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
.markdown-body {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 50rem;
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
text-align: justify;
|
||||||
|
|
||||||
|
& h1 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& h2 {
|
||||||
|
color: var(--primary);
|
||||||
|
margin: 1rem 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
.noselect {
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nolist {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nolink {
|
||||||
|
display: inline-block;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nolink-active {
|
||||||
|
display: inline-block;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nobutton {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nobar {
|
||||||
|
scrollbar-width: none;
|
||||||
|
-mis-overflow-style: none;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
background: transparent;
|
||||||
|
width: 0px;
|
||||||
|
height: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ellipsis {
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noshrink {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capitalize {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
@import 'reset';
|
||||||
|
@import 'links';
|
||||||
|
@import 'code';
|
||||||
|
@import 'theme';
|
||||||
|
@import 'states';
|
||||||
|
@import 'inputs';
|
||||||
|
@import 'forms';
|
||||||
|
@import 'markdown';
|
||||||
|
@import 'tooltip';
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#app {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
color: var(--link);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
:root {
|
||||||
|
--primary-dark-10: #e54485;
|
||||||
|
--primary: #f65596;
|
||||||
|
--primary-light-10: #f075a6;
|
||||||
|
--primary-light-20: #f2a6c4;
|
||||||
|
--primary-light-30: #f7c9dc;
|
||||||
|
|
||||||
|
--primary: #D81159;
|
||||||
|
--primary-dark-10: #8F2D56;
|
||||||
|
--secondary: #73D2DE;
|
||||||
|
--secondary-dark-10: 218380;
|
||||||
|
|
||||||
|
--grey-dark-50: #111;
|
||||||
|
--grey-dark-40: #222;
|
||||||
|
--grey-dark-30: #444;
|
||||||
|
--grey-dark-20: #666;
|
||||||
|
--grey-dark-10: #888;
|
||||||
|
--grey: #aaa;
|
||||||
|
--grey-light-10: #bbb;
|
||||||
|
--grey-light-20: #ccc;
|
||||||
|
--grey-light-30: #ddd;
|
||||||
|
--grey-light-40: #eee;
|
||||||
|
--grey-light-50: #fafafa;
|
||||||
|
--grey-light-60: #fcfcfc;
|
||||||
|
|
||||||
|
--background-dark-20: #eee;
|
||||||
|
--background-dark-10: #f8f8f8;
|
||||||
|
--background: #fff;
|
||||||
|
|
||||||
|
--background-base: #fff;
|
||||||
|
--background-base-10: #fafafa;
|
||||||
|
--background-base-20: #f0f0f0;
|
||||||
|
--background-level-10: #fff;
|
||||||
|
--background-level-20: #eee;
|
||||||
|
--background-level-30: #eee;
|
||||||
|
--background-dim: var(--shadow-weak-10);
|
||||||
|
--background-error: rgba(255, 0, 0, .1);
|
||||||
|
|
||||||
|
--shadow-weak-50: rgba(0, 0, 0, .02);
|
||||||
|
--shadow-weak-40: rgba(0, 0, 0, .05);
|
||||||
|
--shadow-weak-30: rgba(0, 0, 0, .1);
|
||||||
|
--shadow-weak-20: rgba(0, 0, 0, .2);
|
||||||
|
--shadow-weak-10: rgba(0, 0, 0, .35);
|
||||||
|
--shadow: rgba(0, 0, 0, .5);
|
||||||
|
--shadow-strong-10: rgba(0, 0, 0, .6);
|
||||||
|
--shadow-strong-20: rgba(0, 0, 0, .75);
|
||||||
|
--shadow-strong-30: rgba(0, 0, 0, .9);
|
||||||
|
|
||||||
|
--highlight-weak-40: rgba(255, 255, 255, .05);
|
||||||
|
--highlight-weak-30: rgba(255, 255, 255, .1);
|
||||||
|
--highlight-weak-20: rgba(255, 255, 255, .2);
|
||||||
|
--highlight-weak-10: rgba(255, 255, 255, .35);
|
||||||
|
--highlight: rgba(255, 255, 255, .5);
|
||||||
|
--highlight-strong-10: rgba(255, 255, 255, .6);
|
||||||
|
--highlight-strong-20: rgba(255, 255, 255, .75);
|
||||||
|
--highlight-strong-30: rgba(255, 255, 255, .9);
|
||||||
|
|
||||||
|
--glass-weak-50: rgba(0, 0, 0, .02);
|
||||||
|
--glass-weak-40: rgba(0, 0, 0, .05);
|
||||||
|
--glass-weak-30: rgba(0, 0, 0, .1);
|
||||||
|
--glass-weak-20: rgba(0, 0, 0, .2);
|
||||||
|
--glass-weak-10: rgba(0, 0, 0, .35);
|
||||||
|
--glass: rgba(0, 0, 0, .5);
|
||||||
|
--glass-strong-10: rgba(0, 0, 0, .6);
|
||||||
|
--glass-strong-20: rgba(0, 0, 0, .75);
|
||||||
|
--glass-strong-30: rgba(0, 0, 0, .9);
|
||||||
|
|
||||||
|
--text: #222;
|
||||||
|
--text-light: #fff;
|
||||||
|
|
||||||
|
/* --link: #48f; */
|
||||||
|
--link: var(--primary);
|
||||||
|
|
||||||
|
--male: #0af;
|
||||||
|
--female: #f0a;
|
||||||
|
|
||||||
|
--enabled: #5c2;
|
||||||
|
--enabled-background: rgba(0, 255, 0, .1);
|
||||||
|
--disabled: #c20;
|
||||||
|
--disabled-background: rgba(255, 0, 0, .1);
|
||||||
|
--disabled-handle: var(--grey-light-10);
|
||||||
|
|
||||||
|
--error: #f66;
|
||||||
|
--alert: #f00;
|
||||||
|
--warn: #e80;
|
||||||
|
--success: #5c2;
|
||||||
|
--notice: #25c;
|
||||||
|
|
||||||
|
--approve: #3a1;
|
||||||
|
--reject: #a22;
|
||||||
|
|
||||||
|
--gold: #d5b522;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background-dark-20: #000;
|
||||||
|
--background-dark-10: #111;
|
||||||
|
--background: #222;
|
||||||
|
|
||||||
|
--background-base: #252525;
|
||||||
|
--background-base-10: #1a1a1a;
|
||||||
|
--background-base-20: #050505;
|
||||||
|
--background-level-10: #fff;
|
||||||
|
--background-level-20: #eee;
|
||||||
|
--background-level-30: #eee;
|
||||||
|
--background-dim: var(--shadow-weak-10);
|
||||||
|
|
||||||
|
--text: #fcfcfc;
|
||||||
|
|
||||||
|
--glass-weak-50: rgba(255, 255, 255, .02);
|
||||||
|
--glass-weak-40: rgba(255, 255, 255, .05);
|
||||||
|
--glass-weak-30: rgba(255, 255, 255, .1);
|
||||||
|
--glass-weak-20: rgba(255, 255, 255, .2);
|
||||||
|
--glass-weak-10: rgba(255, 255, 255, .35);
|
||||||
|
--glass: rgba(255, 255, 255, .5);
|
||||||
|
--glass-strong-10: rgba(255, 255, 255, .6);
|
||||||
|
--glass-strong-20: rgba(255, 255, 255, .75);
|
||||||
|
--glass-strong-30: rgba(255, 255, 255, .9);
|
||||||
|
|
||||||
|
--grey-dark-50: #101010;
|
||||||
|
--grey-dark-40: #1f1f1f;
|
||||||
|
--grey-dark-30: #3c3c3c;
|
||||||
|
--grey-dark-20: #606060;
|
||||||
|
--grey-dark-10: #808080;
|
||||||
|
--grey: #aaa;
|
||||||
|
--grey-light-10: #bbb;
|
||||||
|
--grey-light-20: #ccc;
|
||||||
|
--grey-light-30: #ddd;
|
||||||
|
--grey-light-40: #eee;
|
||||||
|
--grey-light-50: #fafafa;
|
||||||
|
--grey-light-60: #fcfcfc;
|
||||||
|
}
|
|
@ -0,0 +1,205 @@
|
||||||
|
/* Content */
|
||||||
|
|
||||||
|
.v-popper__popper {
|
||||||
|
z-index: 10000;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
outline: none;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__wrapper {
|
||||||
|
margin: 0 .25rem .25rem .25rem; /* arrow provides top clearance */
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper.v-popper__popper--hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity .15s, visibility .15s;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper.v-popper__popper--shown {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity .15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper.v-popper__popper--skip-transition,
|
||||||
|
.v-popper__popper.v-popper__popper--skip-transition > .v-popper__wrapper {
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__backdrop {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__inner {
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__inner > div {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
max-width: inherit;
|
||||||
|
max-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__arrow-container {
|
||||||
|
position: absolute;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper--arrow-overflow .v-popper__arrow-container,
|
||||||
|
.v-popper__popper--no-positioning .v-popper__arrow-container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__arrow-inner,
|
||||||
|
.v-popper__arrow-outer {
|
||||||
|
border-style: solid;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__arrow-inner {
|
||||||
|
visibility: hidden;
|
||||||
|
border-width: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__arrow-outer {
|
||||||
|
border-width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper[data-popper-placement^="top"] .v-popper__arrow-inner,
|
||||||
|
.v-popper__popper[data-popper-placement^="bottom"] .v-popper__arrow-inner {
|
||||||
|
left: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper[data-popper-placement^="top"] .v-popper__arrow-outer,
|
||||||
|
.v-popper__popper[data-popper-placement^="bottom"] .v-popper__arrow-outer {
|
||||||
|
left: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper[data-popper-placement^="top"] .v-popper__arrow-inner,
|
||||||
|
.v-popper__popper[data-popper-placement^="top"] .v-popper__arrow-outer {
|
||||||
|
border-bottom-width: 0;
|
||||||
|
border-left-color: transparent !important;
|
||||||
|
border-right-color: transparent !important;
|
||||||
|
border-bottom-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper[data-popper-placement^="top"] .v-popper__arrow-inner {
|
||||||
|
top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper[data-popper-placement^="bottom"] .v-popper__arrow-container {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper[data-popper-placement^="bottom"] .v-popper__arrow-inner,
|
||||||
|
.v-popper__popper[data-popper-placement^="bottom"] .v-popper__arrow-outer {
|
||||||
|
border-top-width: 0;
|
||||||
|
border-left-color: transparent !important;
|
||||||
|
border-right-color: transparent !important;
|
||||||
|
border-top-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper[data-popper-placement^="bottom"] .v-popper__arrow-inner {
|
||||||
|
top: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper[data-popper-placement^="bottom"] .v-popper__arrow-outer {
|
||||||
|
top: -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper[data-popper-placement^="left"] .v-popper__arrow-inner,
|
||||||
|
.v-popper__popper[data-popper-placement^="right"] .v-popper__arrow-inner {
|
||||||
|
top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper[data-popper-placement^="left"] .v-popper__arrow-outer,
|
||||||
|
.v-popper__popper[data-popper-placement^="right"] .v-popper__arrow-outer {
|
||||||
|
top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper[data-popper-placement^="right"] .v-popper__arrow-inner,
|
||||||
|
.v-popper__popper[data-popper-placement^="right"] .v-popper__arrow-outer {
|
||||||
|
border-left-width: 0;
|
||||||
|
border-left-color: transparent !important;
|
||||||
|
border-top-color: transparent !important;
|
||||||
|
border-bottom-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper[data-popper-placement^="right"] .v-popper__arrow-inner {
|
||||||
|
left: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper[data-popper-placement^="right"] .v-popper__arrow-outer {
|
||||||
|
left: -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper[data-popper-placement^="left"] .v-popper__arrow-container {
|
||||||
|
right: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper[data-popper-placement^="left"] .v-popper__arrow-inner,
|
||||||
|
.v-popper__popper[data-popper-placement^="left"] .v-popper__arrow-outer {
|
||||||
|
border-right-width: 0;
|
||||||
|
border-top-color: transparent !important;
|
||||||
|
border-right-color: transparent !important;
|
||||||
|
border-bottom-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper[data-popper-placement^="left"] .v-popper__arrow-inner {
|
||||||
|
left: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip */
|
||||||
|
|
||||||
|
.v-popper--theme-tooltip .v-popper__inner {
|
||||||
|
background: rgba(0, 0, 0, .8);
|
||||||
|
color: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 7px 12px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper--theme-tooltip .v-popper__arrow-outer {
|
||||||
|
border-color: rgba(0, 0, 0, .8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown */
|
||||||
|
|
||||||
|
.v-popper--theme-dropdown .v-popper__inner {
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--text);
|
||||||
|
border-radius: 6px;
|
||||||
|
border: solid 1px var(--glass-weak-40);
|
||||||
|
box-shadow: 0 6px 30px rgba(0, 0, 0, .1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper--theme-dropdown .v-popper__arrow-inner {
|
||||||
|
visibility: visible;
|
||||||
|
border-color: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper--theme-dropdown .v-popper__arrow-outer {
|
||||||
|
border-color: var(--glass-weak-40);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-observer {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="175" height="175" fill="none" version="1.1" viewBox="0 0 175 175" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||||
|
<metadata>
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||||
|
<dc:title/>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="linearGradient880" x1="108.64" x2="115.51" y1="88.726" y2="136.2" gradientTransform="matrix(1.0498 0 0 1.0498 -2.9171 -2.9658)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#ffea83" offset="0"/>
|
||||||
|
<stop stop-color="#FFDD35" offset=".083333"/>
|
||||||
|
<stop stop-color="#FFA800" offset="1"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint2_linear" x1="48.975" x2="61.299" y1="3.9232" y2="158.04" gradientTransform="translate(-2.832e-5)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#FFEA83" offset="0"/>
|
||||||
|
<stop stop-color="#FFDD35" offset=".083333"/>
|
||||||
|
<stop stop-color="#FFA800" offset="1"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint0_linear-6" x1="-1.4492" x2="116.62" y1="-5.8123" y2="137.08" gradientTransform="translate(-2.832e-5)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#41D1FF" offset="0"/>
|
||||||
|
<stop stop-color="#BD34FE" offset="1"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<circle cx="87.5" cy="87.5" r="87.5" fill="#c4c4c4"/>
|
||||||
|
<circle cx="87.5" cy="87.5" r="87.5" fill="url(#paint0_linear-6)"/>
|
||||||
|
<g transform="translate(632.92 54.355)" fill="#d38787" strokeWidth="1.0614">
|
||||||
|
<path d="m-549.75 68.457c-5.7533-3.1217-6.1166-5.2295-6.1166-35.489 0-30.458 0.35464-32.448 6.3339-35.54 3.9943-2.0655 24.279-2.2805 26.735-0.28333 0.89718 0.72974 6.7203 6.6637 12.94 13.187l11.309 11.86v19.575c0 18.473-0.12956 19.74-2.3011 22.5-4.0223 5.1136-7.558 5.8565-27.65 5.8099-14.15-0.03287-19.008-0.40294-21.25-1.6191zm42.473-6.3594c2.27-1.59 2.359-2.2909 2.359-18.575v-16.923h-6.9521c-12.443 0-16.4-4.0845-16.4-16.93v-7.4828h-8.9464c-6.7178 0-9.3619 0.41549-10.614 1.668-2.5031 2.5031-2.5031 55.724 0 58.228 2.4502 2.4502 37.058 2.4636 40.553 0.01609zm-1.8867-42.165c0-0.16422-2.8659-3.1346-6.3686-6.6008l-6.3686-6.3022v4.9328c0 6.3185 1.8955 8.2687 8.0366 8.2687 2.5854 0 4.7007-0.13434 4.7007-0.29859zm-57.57 44.279c-5.6185-3.0486-6.1166-5.593-6.1166-31.243 0-18.891 0.31331-24.063 1.6101-26.571 1.809-3.4981 6.5048-6.3339 10.489-6.3339 2.4847 0 2.5814 0.19984 1.541 3.1843-0.61054 1.7514-1.7457 3.1843-2.5226 3.1843-0.77686 0-2.1631 0.75059-3.0805 1.668-2.4923 2.4923-2.4923 47.244 0 49.736 0.91739 0.9174 2.3036 1.668 3.0805 1.668 0.77688 0 1.912 1.4329 2.5226 3.1843 1.0562 3.0298 0.97108 3.1822-1.7537 3.1418-1.575-0.02331-4.1713-0.75194-5.7694-1.6191zm-16.983-4.2458c-5.4392-2.9512-6.1166-5.9415-6.1166-26.997 0-15.096 0.345-19.878 1.6101-22.325 1.7476-3.3796 6.4758-6.3339 10.137-6.3339 1.8666 0 2.1789 0.44955 1.6594 2.3882-0.35184 1.3135-0.64655 2.7465-0.65453 3.1843-8e-3 0.43784-0.69682 0.79608-1.5308 0.79608-0.83399 0-2.2669 0.75059-3.1843 1.668-2.4767 2.4767-2.4767 38.768 0 41.244 0.91741 0.91739 2.2946 1.668 3.0605 1.668 1.196 0 2.6402 2.995 2.6871 5.5726 0.0241 1.3294-4.5804 0.80962-7.6676-0.8655z" style="mix-blend-mode:lighten"/>
|
||||||
|
<path d="m-552.2 68.911c-5.7533-3.1217-6.1166-5.2295-6.1166-35.489 0-30.458 0.35463-32.448 6.3339-35.54 3.9943-2.0655 24.279-2.2805 26.735-0.28333 0.89718 0.72974 6.7203 6.6637 12.94 13.187l11.309 11.86v19.575c0 18.473-0.12957 19.74-2.3011 22.5-4.0223 5.1136-7.558 5.8565-27.65 5.8099-14.15-0.03287-19.008-0.40294-21.25-1.6191zm42.473-6.3594c2.27-1.59 2.359-2.2909 2.359-18.575v-16.923h-6.952c-12.443 0-16.4-4.0845-16.4-16.93v-7.4828h-8.9464c-6.7179 0-9.3619 0.41549-10.614 1.668-2.5031 2.5031-2.5031 55.724 0 58.228 2.4502 2.4502 37.058 2.4636 40.553 0.01609zm-1.8867-42.165c0-0.16422-2.8659-3.1346-6.3686-6.6008l-6.3686-6.3022v4.9328c0 6.3185 1.8955 8.2688 8.0366 8.2688 2.5854 0 4.7007-0.13434 4.7007-0.29859zm-57.57 44.279c-5.6185-3.0486-6.1166-5.593-6.1166-31.243 0-18.891 0.31331-24.063 1.6101-26.571 1.809-3.4981 6.5048-6.3339 10.489-6.3339 2.4847 0 2.5814 0.19984 1.541 3.1843-0.61054 1.7514-1.7457 3.1843-2.5226 3.1843-0.77687 0-2.1631 0.75059-3.0805 1.668-2.4923 2.4923-2.4923 47.244 0 49.736 0.91741 0.91739 2.3036 1.668 3.0805 1.668 0.77686 0 1.912 1.4329 2.5226 3.1843 1.0562 3.0298 0.97107 3.1822-1.7537 3.1418-1.575-0.02331-4.1713-0.75194-5.7694-1.6191zm-16.983-4.2458c-5.4392-2.9512-6.1166-5.9415-6.1166-26.997 0-15.096 0.34502-19.878 1.6101-22.325 1.7476-3.3796 6.4758-6.3339 10.137-6.3339 1.8666 0 2.1789 0.44955 1.6594 2.3882-0.35182 1.3135-0.64653 2.7465-0.65452 3.1843-8e-3 0.43784-0.69683 0.79608-1.5308 0.79608-0.83397 0-2.2669 0.75059-3.1843 1.668-2.4767 2.4767-2.4767 38.768 0 41.245 0.9174 0.91739 2.2946 1.668 3.0605 1.668 1.196 0 2.6402 2.995 2.6871 5.5726 0.0241 1.3294-4.5804 0.80962-7.6676-0.8655z" fillOpacity=".47466" style="mix-blend-mode:lighten"/>
|
||||||
|
</g>
|
||||||
|
<path d="m128.48 88.913-24.027 4.6784c-0.39475 0.07685-0.68766 0.40944-0.71076 0.80849l-1.4782 24.805c-0.0347 0.58371 0.50497 1.0372 1.0792 0.90602l6.6886-1.5338c0.62676-0.14383 1.1916 0.40419 1.0635 1.0299l-1.9874 9.6702c-0.13438 0.65091 0.48084 1.2073 1.1202 1.0142l4.1322-1.2472c0.64041-0.19317 1.2556 0.36535 1.1202 1.0162l-3.158 15.191c-0.19842 0.95011 1.074 1.4677 1.6042 0.653l0.35485-0.54382 19.578-38.827c0.32755-0.64985-0.23727-1.391-0.95641-1.2535l-6.8849 1.3207c-0.6467 0.12389-1.1979-0.47453-1.0152-1.1034l4.4944-15.482c0.18266-0.63012-0.36955-1.2295-1.0173-1.1034z" fill="url(#linearGradient880)" strokeWidth="1.0498"/>
|
||||||
|
<rect x="3" y="3" width="169" height="169" rx="84.5" stroke="url(#paint2_linear)" strokeWidth="6" style="mix-blend-mode:soft-light"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 5.6 KiB |
|
@ -0,0 +1,27 @@
|
||||||
|
<template>
|
||||||
|
<a :class="{ active: isActive }">
|
||||||
|
<slot />
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useAttrs, computed } from 'vue'
|
||||||
|
import { usePageContext } from '../renderer/usePageContext'
|
||||||
|
|
||||||
|
const pageContext = usePageContext()
|
||||||
|
const { href } = useAttrs() as { href: string }
|
||||||
|
const isActive = computed(() => {
|
||||||
|
const { urlPathname } = pageContext.value
|
||||||
|
return href === '/' ? urlPathname === href : urlPathname.startsWith(href)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
a {
|
||||||
|
padding: 2px 10px;
|
||||||
|
margin-left: -10px;
|
||||||
|
}
|
||||||
|
a.active {
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,178 @@
|
||||||
|
<template>
|
||||||
|
<form
|
||||||
|
class="selector"
|
||||||
|
:class="{ dragging }"
|
||||||
|
@submit.prevent="upload"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref="dropzone"
|
||||||
|
class="dropzone"
|
||||||
|
@drop.prevent="dropFile"
|
||||||
|
@dragover="dragging = true"
|
||||||
|
@dragleave="dragging = false"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
@change="selectFile"
|
||||||
|
>
|
||||||
|
Drop or paste your files here
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="uploads nolist">
|
||||||
|
<li
|
||||||
|
v-for="upload in thumbs"
|
||||||
|
class="upload"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="upload.file.type.indexOf('image/') === 0"
|
||||||
|
:src="upload.thumb"
|
||||||
|
class="upload-thumb"
|
||||||
|
>
|
||||||
|
|
||||||
|
<video
|
||||||
|
v-if="upload.file.type.indexOf('video/') === 0"
|
||||||
|
class="upload-thumb"
|
||||||
|
autoplay
|
||||||
|
muted
|
||||||
|
loop
|
||||||
|
>
|
||||||
|
<source :src="upload.thumb">
|
||||||
|
</video>
|
||||||
|
|
||||||
|
<span class="upload-name ellipsis">{{ upload.file.name }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="button button-primary submit"
|
||||||
|
>Upload</button>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
v-model="addToAlbum"
|
||||||
|
type="checkbox"
|
||||||
|
>Add to album
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
|
||||||
|
import { post } from '#src/api.ts';
|
||||||
|
|
||||||
|
const dropzone = ref(null);
|
||||||
|
const dragging = ref(false);
|
||||||
|
|
||||||
|
const uploads = ref([]);
|
||||||
|
const thumbs = ref([]);
|
||||||
|
|
||||||
|
const addToAlbum = ref(true);
|
||||||
|
|
||||||
|
async function upload() {
|
||||||
|
console.log('UPLOAD!', uploads.value);
|
||||||
|
const form = new FormData();
|
||||||
|
|
||||||
|
uploads.value.forEach((file) => {
|
||||||
|
form.append('files', file);
|
||||||
|
});
|
||||||
|
|
||||||
|
form.append('options', JSON.stringify({
|
||||||
|
addToAlbum: addToAlbum.value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const res = await post('/files', form);
|
||||||
|
|
||||||
|
console.log(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addFiles(newFiles) {
|
||||||
|
console.log('NEW FILES', newFiles);
|
||||||
|
|
||||||
|
uploads.value = uploads.value.concat(newFiles);
|
||||||
|
|
||||||
|
thumbs.value = thumbs.value.concat(newFiles.map((file) => ({
|
||||||
|
file,
|
||||||
|
thumb: URL.createObjectURL(file),
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
function dropFile(event) {
|
||||||
|
dragging.value = false;
|
||||||
|
|
||||||
|
const newFiles = Array.from(event.dataTransfer.items)
|
||||||
|
.filter((item) => item.kind === 'file')
|
||||||
|
.map((item) => item.getAsFile());
|
||||||
|
|
||||||
|
addFiles(newFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectFile(event) {
|
||||||
|
const newFiles = Array.from(event.target.files);
|
||||||
|
|
||||||
|
addFiles(newFiles);
|
||||||
|
|
||||||
|
event.target.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('dragover', (event) => event.preventDefault());
|
||||||
|
window.addEventListener('drop', (event) => event.preventDefault());
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.selector {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone {
|
||||||
|
width: 100%;
|
||||||
|
height: 15rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border: solid 1px var(--glass-weak-20);
|
||||||
|
|
||||||
|
&.dragging {
|
||||||
|
background: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploads {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
|
||||||
|
gap: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-thumb {
|
||||||
|
width: 8rem;
|
||||||
|
height: 6rem;
|
||||||
|
object-fit: cover;
|
||||||
|
margin-bottom: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-name {
|
||||||
|
font-size: .8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
padding: .75rem 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,174 @@
|
||||||
|
import { parse } from '@brillout/json-serializer/parse'; /* eslint-disable-line import/extensions */
|
||||||
|
import events from '#src/events';
|
||||||
|
const postHeaders = {
|
||||||
|
mode: 'cors',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
function getQuery(data) {
|
||||||
|
if (!data) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const curatedQuery = Object.fromEntries(Object.entries(data).map(([key, value]) => (value === undefined ? null : [key, value])).filter(Boolean));
|
||||||
|
return `?${new URLSearchParams(curatedQuery).toString()}`; // recode so commas aren't encoded
|
||||||
|
}
|
||||||
|
function showFeedback(isSuccess, options = {}, errorMessage) {
|
||||||
|
if (!isSuccess && (typeof options.errorFeedback === 'string' || options.appendErrorMessage)) {
|
||||||
|
events.emit('feedback', {
|
||||||
|
type: 'error',
|
||||||
|
message: options.appendErrorMessage && errorMessage
|
||||||
|
? `${options.errorFeedback ? `${options.errorFeedback}: ` : ''}${errorMessage}`
|
||||||
|
: options.errorFeedback,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isSuccess) {
|
||||||
|
events.emit('feedback', {
|
||||||
|
type: 'error',
|
||||||
|
message: 'Error, please try again',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isSuccess && options.successFeedback) {
|
||||||
|
events.emit('feedback', {
|
||||||
|
type: 'success',
|
||||||
|
message: options.successFeedback,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isSuccess && options.undoFeedback) {
|
||||||
|
events.emit('feedback', {
|
||||||
|
type: 'undo',
|
||||||
|
message: options.undoFeedback,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function get(path, query = {}, options = {}) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api${path}${getQuery(query)}`);
|
||||||
|
const body = parse(await res.text());
|
||||||
|
if (res.ok) {
|
||||||
|
showFeedback(true, options);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
showFeedback(false, options, body.statusMessage);
|
||||||
|
throw new Error(body.statusMessage);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
showFeedback(false, options, error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function post(path, data, options = {}) {
|
||||||
|
try {
|
||||||
|
const isForm = data instanceof FormData;
|
||||||
|
console.log('POST', isForm, data);
|
||||||
|
const curatedData = !data || isForm
|
||||||
|
? data
|
||||||
|
: JSON.stringify(data);
|
||||||
|
const res = await fetch(`/api${path}${getQuery(options.query)}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: curatedData,
|
||||||
|
/*
|
||||||
|
...postHeaders,
|
||||||
|
headers: {
|
||||||
|
...postHeaders.headers,
|
||||||
|
...(isForm ? { 'Content-Type': 'multipart/form-data' } : {}),
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
});
|
||||||
|
if (res.status === 204) {
|
||||||
|
showFeedback(true, options);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const body = parse(await res.text());
|
||||||
|
if (res.ok) {
|
||||||
|
showFeedback(true, options);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
showFeedback(false, options, body.statusMessage);
|
||||||
|
throw new Error(body.statusMessage);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
showFeedback(false, options, error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function postForm(path, form, options = {}) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api${path}${getQuery(options.query)}`, {
|
||||||
|
method: 'POST',
|
||||||
|
...postHeaders,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
body: form,
|
||||||
|
});
|
||||||
|
if (res.status === 204) {
|
||||||
|
showFeedback(true, options);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const body = parse(await res.text());
|
||||||
|
if (res.ok) {
|
||||||
|
showFeedback(true, options);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
showFeedback(false, options, body.statusMessage);
|
||||||
|
throw new Error(body.statusMessage);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
showFeedback(false, options, error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function patch(path, data, options = {}) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api${path}${getQuery(options.query)}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: data && JSON.stringify(data),
|
||||||
|
...postHeaders,
|
||||||
|
});
|
||||||
|
if (res.status === 204) {
|
||||||
|
showFeedback(true, options);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const body = parse(await res.text());
|
||||||
|
if (res.ok) {
|
||||||
|
showFeedback(true, options);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
showFeedback(false, options, body.statusMessage);
|
||||||
|
throw new Error(body.statusMessage);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
showFeedback(false, options, error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function del(path, options = {}) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api${path}${getQuery(options.query)}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
body: options.data && JSON.stringify(options.data),
|
||||||
|
...postHeaders,
|
||||||
|
});
|
||||||
|
if (res.status === 204) {
|
||||||
|
showFeedback(true, options);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const body = parse(await res.text());
|
||||||
|
if (res.ok) {
|
||||||
|
showFeedback(true, options);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
showFeedback(false, options, body.statusMessage);
|
||||||
|
throw new Error(body.statusMessage);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
showFeedback(false, options, error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=api.js.map
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,13 @@
|
||||||
|
import config from 'config';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import { initServer } from '#src/web/server.js';
|
||||||
|
async function init() {
|
||||||
|
if (config.uploads.flushTempOnStart) {
|
||||||
|
await fs.rmdir(path.join(config.uploads.path, 'temp'), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(config.uploads.path, 'temp'));
|
||||||
|
}
|
||||||
|
await initServer();
|
||||||
|
}
|
||||||
|
init();
|
||||||
|
//# sourceMappingURL=app.js.map
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,MAAM,aAAa,CAAC;AAE7B,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhD,KAAK,UAAU,IAAI;IAClB,IAAI,MAAM,CAAC,OAAO,CAAC,gBAAgB,EAAE,CAAC;QACrC,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC5E,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IACxD,CAAC;IAED,MAAM,UAAU,EAAE,CAAC;AACpB,CAAC;AAED,IAAI,EAAE,CAAC"}
|
|
@ -0,0 +1,14 @@
|
||||||
|
export class HttpError extends Error {
|
||||||
|
constructor(message, httpCode, friendlyMessage, data) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'HttpError';
|
||||||
|
this.httpCode = httpCode;
|
||||||
|
if (friendlyMessage) {
|
||||||
|
this.friendlyMessage = friendlyMessage;
|
||||||
|
}
|
||||||
|
if (data) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=errors.js.map
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.js"],"names":[],"mappings":"AAAA,MAAM,OAAO,SAAU,SAAQ,KAAK;IACnC,YAAY,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,IAAI;QACnD,KAAK,CAAC,OAAO,CAAC,CAAC;QAEf,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;QACxB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QAEzB,IAAI,eAAe,EAAE,CAAC;YACrB,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;QACxC,CAAC;QAED,IAAI,IAAI,EAAE,CAAC;YACV,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QAClB,CAAC;IACF,CAAC;CACD"}
|
|
@ -0,0 +1,3 @@
|
||||||
|
import mitt from 'mitt';
|
||||||
|
export default mitt();
|
||||||
|
//# sourceMappingURL=events.js.map
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,eAAe,IAAI,EAAE,CAAC"}
|
|
@ -0,0 +1,46 @@
|
||||||
|
import config from 'config';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import mime from 'mime';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import { fileTypeFromBuffer } from 'file-type';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
import { HttpError } from '#src/errors.js';
|
||||||
|
function hashFile(fileBuffer) {
|
||||||
|
return crypto.createHash('sha256')
|
||||||
|
.update(fileBuffer)
|
||||||
|
.digest('hex');
|
||||||
|
}
|
||||||
|
async function validateMimetype(file, fileBuffer) {
|
||||||
|
const { mime } = await fileTypeFromBuffer(fileBuffer);
|
||||||
|
if (mime !== file.mimetype) {
|
||||||
|
throw new HttpError(`MIME type mismatch: ${file.mimetype} expected, ${mime} found`, 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function generateThumb(type, fileBuffer, hash, targetPath) {
|
||||||
|
if (type === 'image') {
|
||||||
|
await sharp(fileBuffer)
|
||||||
|
.resize({ height: config.uploads.thumbHeight })
|
||||||
|
.jpeg({ quality: config.uploads.thumbQuality })
|
||||||
|
.toFile(path.join(targetPath, `${hash}_t.jpg`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function uploadFiles(files, options) {
|
||||||
|
console.log('FILES', files, options);
|
||||||
|
await files.reduce(async (chain, file) => {
|
||||||
|
await chain;
|
||||||
|
const tempFilePath = path.join(config.uploads.path, 'temp', file.tempName);
|
||||||
|
const fileBuffer = await fs.readFile(tempFilePath);
|
||||||
|
const hash = hashFile(fileBuffer);
|
||||||
|
await validateMimetype(file, fileBuffer);
|
||||||
|
const targetPath = path.join(config.uploads.path, 'full', hash.slice(0, 2), hash.slice(2, 4));
|
||||||
|
const extension = mime.getExtension(file.mimetype);
|
||||||
|
await fs.mkdir(targetPath, { recursive: true });
|
||||||
|
await Promise.all([
|
||||||
|
generateThumb(file.mimetype.split('/')[0], fileBuffer, hash, targetPath),
|
||||||
|
fs.rename(path.join(config.uploads.path, 'temp', file.tempName), path.join(targetPath, `${hash}.${extension}`)),
|
||||||
|
]);
|
||||||
|
}, Promise.resolve());
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=files.js.map
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"files.js","sourceRoot":"","sources":["../src/files.ts"],"names":[],"mappings":"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"}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { uploadFiles } from '#src/files.js';
|
||||||
|
export async function uploadFilesApi(req, res) {
|
||||||
|
const files = req.files;
|
||||||
|
const uploads = await uploadFiles(files.map((file) => ({
|
||||||
|
fileName: file.originalname,
|
||||||
|
encoding: file.encoding,
|
||||||
|
mimetype: file.mimetype,
|
||||||
|
tempName: file.filename,
|
||||||
|
size: file.size,
|
||||||
|
})), JSON.parse(req.body.options));
|
||||||
|
res.send(uploads);
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=files.js.map
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"files.js","sourceRoot":"","sources":["../../src/web/files.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,GAAY,EAAE,GAAa;IAC/D,MAAM,KAAK,GAAG,GAAG,CAAC,KAA8B,CAAC;IAEjD,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACrD,QAAQ,EAAE,IAAI,CAAC,YAAY;QAC3B,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,IAAI,EAAE,IAAI,CAAC,IAAI;KAChB,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;IAEnC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACnB,CAAC"}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
export const root = `${__dirname}/../..`;
|
||||||
|
//# sourceMappingURL=root.js.map
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"root.js","sourceRoot":"","sources":["../../src/web/root.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAC/B,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AACpC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE1D,MAAM,CAAC,MAAM,IAAI,GAAG,GAAG,SAAS,QAAQ,CAAC"}
|
|
@ -0,0 +1,55 @@
|
||||||
|
import config from 'config';
|
||||||
|
import express from 'express';
|
||||||
|
import bodyParser from 'express';
|
||||||
|
import compression from 'compression';
|
||||||
|
import { renderPage, createDevMiddleware } from 'vike/server';
|
||||||
|
import multer from 'multer';
|
||||||
|
import { root } from '#web/root.js';
|
||||||
|
import { uploadFilesApi } from '#web/files.js';
|
||||||
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
|
export async function initServer() {
|
||||||
|
const app = express();
|
||||||
|
// const upload = multer({ dest: './uploads' });
|
||||||
|
const upload = multer({ dest: './uploads/temp' });
|
||||||
|
app.use(compression());
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
app.use(bodyParser.urlencoded());
|
||||||
|
// Vite integration
|
||||||
|
if (isProduction) {
|
||||||
|
// In production, we need to serve our static assets ourselves.
|
||||||
|
// (In dev, Vite's middleware serves our static assets.)
|
||||||
|
const sirv = (await import('sirv')).default;
|
||||||
|
app.use(sirv(`${root}/dist/client`));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const { devMiddleware } = await createDevMiddleware({ root });
|
||||||
|
app.use(devMiddleware);
|
||||||
|
}
|
||||||
|
// app.post('/api/files', uploadFilesApi);
|
||||||
|
app.post('/api/files', upload.array('files'), uploadFilesApi);
|
||||||
|
// Vike middleware. It should always be our last middleware (because it's a
|
||||||
|
// catch-all middleware superseding any middleware placed after it).
|
||||||
|
app.get('*', async (req, res) => {
|
||||||
|
const pageContextInit = {
|
||||||
|
urlOriginal: req.originalUrl,
|
||||||
|
headersOriginal: req.headers
|
||||||
|
};
|
||||||
|
const pageContext = await renderPage(pageContextInit);
|
||||||
|
if (pageContext.errorWhileRendering) {
|
||||||
|
// Install error tracking here, see https://vike.dev/error-tracking
|
||||||
|
}
|
||||||
|
const { httpResponse } = pageContext;
|
||||||
|
if (res.writeEarlyHints)
|
||||||
|
res.writeEarlyHints({ link: httpResponse.earlyHints.map((e) => e.earlyHintLink) });
|
||||||
|
httpResponse.headers.forEach(([name, value]) => res.setHeader(name, value));
|
||||||
|
res.status(httpResponse.statusCode);
|
||||||
|
// For HTTP streams use pageContext.httpResponse.pipe() instead, see https://vike.dev/streaming
|
||||||
|
res.send(httpResponse.body);
|
||||||
|
});
|
||||||
|
const host = process.env.HOST || config.web.host || 'localhost';
|
||||||
|
const port = process.env.PORT || config.web.port || 3000;
|
||||||
|
app.listen(port, host, () => {
|
||||||
|
console.log(`Server running at http://${host}:${port}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=server.js.map
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"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"}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,49 @@
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"dev": "npm run server:dev",
|
||||||
|
"prod": "npm run build && npm run server:prod",
|
||||||
|
"build": "vike build",
|
||||||
|
"server": "node --loader ts-node/esm ./src/app.ts",
|
||||||
|
"server:dev": "npm run server",
|
||||||
|
"server:prod": "cross-env NODE_ENV=production npm run server"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/compression": "^1.7.5",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/multer": "^2.0.0",
|
||||||
|
"@types/node": "^22.10.2",
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"@vue/compiler-sfc": "^3.5.13",
|
||||||
|
"@vue/server-renderer": "^3.5.13",
|
||||||
|
"body-parser": "^2.2.0",
|
||||||
|
"compression": "^1.7.5",
|
||||||
|
"config": "^4.1.1",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"express": "^4.21.2",
|
||||||
|
"file-type": "^21.0.0",
|
||||||
|
"knex": "^3.1.0",
|
||||||
|
"mime": "^4.1.0",
|
||||||
|
"mitt": "^3.0.1",
|
||||||
|
"multer": "^2.0.2",
|
||||||
|
"mysql": "^2.18.1",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
|
"pg": "^8.16.3",
|
||||||
|
"sharp": "^0.34.4",
|
||||||
|
"sirv": "^3.0.0",
|
||||||
|
"sqlite3": "^5.1.7",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.9.2",
|
||||||
|
"typescript-eslint": "^8.44.0",
|
||||||
|
"vike": "^0.4.223",
|
||||||
|
"vite": "^6.0.5",
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vue-tsc": "^2.1.10"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"imports": {
|
||||||
|
"#src/*": "./src/*",
|
||||||
|
"#web/*": "./src/web/*",
|
||||||
|
"#layouts/*": "./layouts/*",
|
||||||
|
"#pages/*": "./pages/*"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<header class="header">
|
||||||
|
<h1>Pubload</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="content"><slot /></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import Link from '../components/Link.vue'
|
||||||
|
import '../assets/css/style.css'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1000px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,28 @@
|
||||||
|
import type { Config } from 'vike/types'
|
||||||
|
|
||||||
|
// https://vike.dev/config
|
||||||
|
export default {
|
||||||
|
// https://vike.dev/clientRouting
|
||||||
|
clientRouting: true,
|
||||||
|
// https://vike.dev/meta
|
||||||
|
meta: {
|
||||||
|
// Define new setting 'title'
|
||||||
|
title: {
|
||||||
|
env: {
|
||||||
|
server: true,
|
||||||
|
client: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Define new setting 'description'
|
||||||
|
description: {
|
||||||
|
env: { server: true }
|
||||||
|
},
|
||||||
|
Layout: {
|
||||||
|
env: {
|
||||||
|
server: true,
|
||||||
|
client: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hydrationCanBeAborted: true
|
||||||
|
} satisfies Config
|
|
@ -0,0 +1,25 @@
|
||||||
|
<template>
|
||||||
|
<div class="center">
|
||||||
|
<p>{{ abortReason }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { usePageContext } from '../../renderer/usePageContext'
|
||||||
|
|
||||||
|
const pageContext = usePageContext()
|
||||||
|
let { is404, abortReason } = pageContext.value
|
||||||
|
if (!abortReason) {
|
||||||
|
abortReason = is404 ? 'Page not found.' : 'Something went wrong.'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.center {
|
||||||
|
height: calc(100vh - 100px);
|
||||||
|
display: flex;
|
||||||
|
font-size: 1.3em;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<Dropzone />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import Dropzone from '#components/upload/drop.vue';
|
||||||
|
</script>
|
|
@ -0,0 +1,19 @@
|
||||||
|
import type { Config } from 'vike/types'
|
||||||
|
|
||||||
|
// https://vike.dev/config
|
||||||
|
export default {
|
||||||
|
// https://vike.dev/clientRouting
|
||||||
|
clientRouting: true,
|
||||||
|
// https://vike.dev/meta
|
||||||
|
meta: {
|
||||||
|
// Define new setting 'title'
|
||||||
|
title: {
|
||||||
|
env: { server: true, client: true }
|
||||||
|
},
|
||||||
|
// Define new setting 'description'
|
||||||
|
description: {
|
||||||
|
env: { server: true }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hydrationCanBeAborted: true
|
||||||
|
} satisfies Config
|
|
@ -0,0 +1,9 @@
|
||||||
|
// https://vike.dev/onPageTransitionEnd
|
||||||
|
export { onPageTransitionEnd }
|
||||||
|
|
||||||
|
import type { OnPageTransitionEndAsync } from 'vike/types'
|
||||||
|
|
||||||
|
const onPageTransitionEnd: OnPageTransitionEndAsync = async (): ReturnType<OnPageTransitionEndAsync> => {
|
||||||
|
console.log('Page transition end')
|
||||||
|
document.querySelector('body')!.classList.remove('page-is-transitioning')
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
// https://vike.dev/onPageTransitionStart
|
||||||
|
export { onPageTransitionStart }
|
||||||
|
|
||||||
|
import type { OnPageTransitionStartAsync } from 'vike/types'
|
||||||
|
|
||||||
|
const onPageTransitionStart: OnPageTransitionStartAsync = async (): ReturnType<OnPageTransitionStartAsync> => {
|
||||||
|
console.log('Page transition start')
|
||||||
|
document.querySelector('body')!.classList.add('page-is-transitioning')
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
// https://vike.dev/onRenderClient
|
||||||
|
export { onRenderClient }
|
||||||
|
|
||||||
|
import { createVueApp } from './createVueApp'
|
||||||
|
import { getPageTitle } from './getPageTitle'
|
||||||
|
import type { OnRenderClientAsync } from 'vike/types'
|
||||||
|
|
||||||
|
let app: ReturnType<typeof createVueApp>
|
||||||
|
const onRenderClient: OnRenderClientAsync = async (pageContext): ReturnType<OnRenderClientAsync> => {
|
||||||
|
// This onRenderClient() hook only supports SSR, see https://vike.dev/render-modes for how to modify onRenderClient()
|
||||||
|
// to support SPA
|
||||||
|
if (!pageContext.Page) throw new Error('My onRenderClient() hook expects pageContext.Page to be defined')
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
app = createVueApp(pageContext)
|
||||||
|
app.mount('#app')
|
||||||
|
} else {
|
||||||
|
app.changePage(pageContext)
|
||||||
|
}
|
||||||
|
document.title = getPageTitle(pageContext)
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
// https://vike.dev/onRenderHtml
|
||||||
|
export { onRenderHtml }
|
||||||
|
|
||||||
|
import { renderToString as renderToString_ } from '@vue/server-renderer'
|
||||||
|
import type { App } from 'vue'
|
||||||
|
import { escapeInject, dangerouslySkipEscape } from 'vike/server'
|
||||||
|
import { createVueApp } from './createVueApp'
|
||||||
|
import logoUrl from '../assets/img/logo.svg'
|
||||||
|
import type { OnRenderHtmlAsync } from 'vike/types'
|
||||||
|
import { getPageTitle } from './getPageTitle'
|
||||||
|
|
||||||
|
const onRenderHtml: OnRenderHtmlAsync = async (pageContext): ReturnType<OnRenderHtmlAsync> => {
|
||||||
|
// This onRenderHtml() hook only supports SSR, see https://vike.dev/render-modes for how to modify
|
||||||
|
// onRenderHtml() to support SPA
|
||||||
|
if (!pageContext.Page) throw new Error('My render() hook expects pageContext.Page to be defined')
|
||||||
|
|
||||||
|
const app = createVueApp(pageContext)
|
||||||
|
|
||||||
|
const appHtml = await renderToString(app)
|
||||||
|
|
||||||
|
const title = getPageTitle(pageContext)
|
||||||
|
const desc = pageContext.data?.description || pageContext.config.description || 'Demo of using Vike'
|
||||||
|
|
||||||
|
const documentHtml = escapeInject`<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="${logoUrl}" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="description" content="${desc}" />
|
||||||
|
<title>${title}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">${dangerouslySkipEscape(appHtml)}</div>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
|
||||||
|
return {
|
||||||
|
documentHtml,
|
||||||
|
pageContext: {
|
||||||
|
// We can add custom pageContext properties here, see https://vike.dev/pageContext#custom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderToString(app: App) {
|
||||||
|
let err: unknown
|
||||||
|
// Workaround: renderToString_() swallows errors in production, see https://github.com/vuejs/core/issues/7876
|
||||||
|
app.config.errorHandler = (err_) => {
|
||||||
|
err = err_
|
||||||
|
}
|
||||||
|
const appHtml = await renderToString_(app)
|
||||||
|
if (err) throw err
|
||||||
|
return appHtml
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { createSSRApp, h, shallowRef } from 'vue'
|
||||||
|
import { setPageContext } from './usePageContext'
|
||||||
|
import { setData } from './useData'
|
||||||
|
import type { PageContext } from 'vike/types'
|
||||||
|
import { objectAssign } from './utils'
|
||||||
|
|
||||||
|
// import Layout from './Layout.vue'
|
||||||
|
|
||||||
|
export function createVueApp(pageContext: PageContext) {
|
||||||
|
const pageContextRef = shallowRef(pageContext)
|
||||||
|
const dataRef = shallowRef(pageContext.data)
|
||||||
|
const pageRef = shallowRef(pageContext.Page)
|
||||||
|
|
||||||
|
// const RootComponent = () => h(Layout, null, () => h(pageRef.value))
|
||||||
|
const RootComponent = () => h(pageContext.config.Layout, null, () => h(pageRef.value))
|
||||||
|
const app = createSSRApp(RootComponent)
|
||||||
|
setPageContext(app, pageContextRef)
|
||||||
|
setData(app, dataRef)
|
||||||
|
|
||||||
|
// app.changePage() is called upon navigation, see +onRenderClient.ts
|
||||||
|
objectAssign(app, {
|
||||||
|
changePage: (pageContext: PageContext) => {
|
||||||
|
pageContextRef.value = pageContext
|
||||||
|
dataRef.value = pageContext.data
|
||||||
|
pageRef.value = pageContext.Page
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return app
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
export { getPageTitle }
|
||||||
|
|
||||||
|
import type { PageContext } from 'vike/types'
|
||||||
|
|
||||||
|
function getPageTitle(pageContext: PageContext): string {
|
||||||
|
const title =
|
||||||
|
// Title defined dynamically by data()
|
||||||
|
pageContext.data?.title ||
|
||||||
|
// Title defined statically by /pages/some-page/+title.js (or by `export default { title }` in /pages/some-page/+config.js)
|
||||||
|
// The setting 'pageContext.config.title' is a custom setting we defined at ./+config.ts
|
||||||
|
pageContext.config.title ||
|
||||||
|
'Vike Demo'
|
||||||
|
return title
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
export type { Component }
|
||||||
|
|
||||||
|
import type { ComponentPublicInstance } from 'vue'
|
||||||
|
|
||||||
|
type Component = ComponentPublicInstance // https://stackoverflow.com/questions/63985658/how-to-type-vue-instance-out-of-definecomponent-in-vue-3/63986086#63986086
|
||||||
|
type Page = Component
|
||||||
|
|
||||||
|
// https://vike.dev/pageContext#typescript
|
||||||
|
declare global {
|
||||||
|
namespace Vike {
|
||||||
|
interface PageContext {
|
||||||
|
Page: Page
|
||||||
|
data?: {
|
||||||
|
/** Value for <title> defined dynamically by by /pages/some-page/+data.js */
|
||||||
|
title?: string
|
||||||
|
/** Value for <meta name="description"> defined dynamically */
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
config: {
|
||||||
|
/** Value for <title> defined statically by /pages/some-page/+title.js (or by `export default { title }` in /pages/some-page/+config.js) */
|
||||||
|
title?: string
|
||||||
|
/** Value for <meta name="description"> defined statically */
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
/** https://vike.dev/render */
|
||||||
|
abortReason?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
// https://vike.dev/useData
|
||||||
|
export { useData }
|
||||||
|
export { setData }
|
||||||
|
|
||||||
|
import { inject } from 'vue'
|
||||||
|
import type { App, InjectionKey, Ref } from 'vue'
|
||||||
|
|
||||||
|
const key: InjectionKey<Ref<unknown>> = Symbol()
|
||||||
|
|
||||||
|
/** https://vike.dev/useData */
|
||||||
|
function useData<Data>(): Ref<Data> {
|
||||||
|
const data = inject(key)
|
||||||
|
if (!data) throw new Error('setData() not called')
|
||||||
|
return data as Ref<Data>
|
||||||
|
}
|
||||||
|
|
||||||
|
function setData(app: App, data: Ref<unknown>): void {
|
||||||
|
app.provide(key, data)
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
// https://vike.dev/usePageContext
|
||||||
|
export { usePageContext }
|
||||||
|
export { setPageContext }
|
||||||
|
|
||||||
|
import { inject } from 'vue'
|
||||||
|
import type { App, InjectionKey, Ref } from 'vue'
|
||||||
|
import type { PageContext } from 'vike/types'
|
||||||
|
|
||||||
|
const key: InjectionKey<Ref<PageContext>> = Symbol()
|
||||||
|
|
||||||
|
/** https://vike.dev/usePageContext */
|
||||||
|
function usePageContext(): Ref<PageContext> {
|
||||||
|
const pageContext = inject(key)
|
||||||
|
if (!pageContext) throw new Error('setPageContext() not called in parent')
|
||||||
|
return pageContext
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPageContext(app: App, pageContext: Ref<PageContext>): void {
|
||||||
|
app.provide(key, pageContext)
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
// Same as Object.assign() but with type inference
|
||||||
|
export function objectAssign<Obj extends object, ObjAddendum>(
|
||||||
|
obj: Obj,
|
||||||
|
objAddendum: ObjAddendum
|
||||||
|
): asserts obj is Obj & ObjAddendum {
|
||||||
|
Object.assign(obj, objAddendum)
|
||||||
|
}
|
|
@ -0,0 +1,219 @@
|
||||||
|
import { parse } from '@brillout/json-serializer/parse'; /* eslint-disable-line import/extensions */
|
||||||
|
|
||||||
|
import events from '#src/events';
|
||||||
|
import { ResBody } from '#src/web/server';
|
||||||
|
|
||||||
|
const postHeaders: Partial<RequestInit> = {
|
||||||
|
mode: 'cors',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type FeedbackOptions = {
|
||||||
|
errorFeedback?: string,
|
||||||
|
successFeedback?: string,
|
||||||
|
undoFeedback?: string,
|
||||||
|
appendErrorMessage?: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
type RequestOptions = FeedbackOptions & {
|
||||||
|
query?: {},
|
||||||
|
data?: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
function getQuery(data: Object) {
|
||||||
|
if (!data) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const curatedQuery = Object.fromEntries(Object.entries(data).map(([key, value]) => (value === undefined ? null : [key, value])).filter(Boolean));
|
||||||
|
|
||||||
|
return `?${new URLSearchParams(curatedQuery).toString()}`; // recode so commas aren't encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
function showFeedback(isSuccess: Boolean, options:FeedbackOptions = {}, errorMessage?: string) {
|
||||||
|
if (!isSuccess && (typeof options.errorFeedback === 'string' || options.appendErrorMessage)) {
|
||||||
|
events.emit('feedback', {
|
||||||
|
type: 'error',
|
||||||
|
message: options.appendErrorMessage && errorMessage
|
||||||
|
? `${options.errorFeedback ? `${options.errorFeedback}: ` : ''}${errorMessage}`
|
||||||
|
: options.errorFeedback,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSuccess) {
|
||||||
|
events.emit('feedback', {
|
||||||
|
type: 'error',
|
||||||
|
message: 'Error, please try again',
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSuccess && options.successFeedback) {
|
||||||
|
events.emit('feedback', {
|
||||||
|
type: 'success',
|
||||||
|
message: options.successFeedback,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSuccess && options.undoFeedback) {
|
||||||
|
events.emit('feedback', {
|
||||||
|
type: 'undo',
|
||||||
|
message: options.undoFeedback,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get(path, query = {}, options: RequestOptions = {}) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api${path}${getQuery(query)}`);
|
||||||
|
const body = parse(await res.text()) as ResBody;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
showFeedback(true, options);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
showFeedback(false, options, body.statusMessage);
|
||||||
|
throw new Error(body.statusMessage);
|
||||||
|
} catch (error) {
|
||||||
|
showFeedback(false, options, error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function post(path, data, options: RequestOptions = {}) {
|
||||||
|
try {
|
||||||
|
const isForm = data instanceof FormData;
|
||||||
|
|
||||||
|
console.log('POST', isForm, data);
|
||||||
|
|
||||||
|
const curatedData = !data || isForm
|
||||||
|
? data
|
||||||
|
: JSON.stringify(data);
|
||||||
|
|
||||||
|
const res = await fetch(`/api${path}${getQuery(options.query)}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: curatedData,
|
||||||
|
/*
|
||||||
|
...postHeaders,
|
||||||
|
headers: {
|
||||||
|
...postHeaders.headers,
|
||||||
|
...(isForm ? { 'Content-Type': 'multipart/form-data' } : {}),
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 204) {
|
||||||
|
showFeedback(true, options);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body: ResBody = parse(await res.text()) as ResBody;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
showFeedback(true, options);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
showFeedback(false, options, body.statusMessage);
|
||||||
|
throw new Error(body.statusMessage);
|
||||||
|
} catch (error) {
|
||||||
|
showFeedback(false, options, error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postForm(path, form, options: RequestOptions = {}) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api${path}${getQuery(options.query)}`, {
|
||||||
|
method: 'POST',
|
||||||
|
...postHeaders,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
body: form,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 204) {
|
||||||
|
showFeedback(true, options);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body: ResBody = parse(await res.text()) as ResBody;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
showFeedback(true, options);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
showFeedback(false, options, body.statusMessage);
|
||||||
|
throw new Error(body.statusMessage);
|
||||||
|
} catch (error) {
|
||||||
|
showFeedback(false, options, error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function patch(path, data, options: RequestOptions = {}) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api${path}${getQuery(options.query)}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: data && JSON.stringify(data),
|
||||||
|
...postHeaders,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 204) {
|
||||||
|
showFeedback(true, options);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = parse(await res.text()) as ResBody;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
showFeedback(true, options);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
showFeedback(false, options, body.statusMessage);
|
||||||
|
throw new Error(body.statusMessage);
|
||||||
|
} catch (error) {
|
||||||
|
showFeedback(false, options, error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function del(path, options: RequestOptions = {}) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api${path}${getQuery(options.query)}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
body: options.data && JSON.stringify(options.data),
|
||||||
|
...postHeaders,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 204) {
|
||||||
|
showFeedback(true, options);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = parse(await res.text()) as ResBody;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
showFeedback(true, options);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
showFeedback(false, options, body.statusMessage);
|
||||||
|
throw new Error(body.statusMessage);
|
||||||
|
} catch (error) {
|
||||||
|
showFeedback(false, options, error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import config from 'config';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
|
||||||
|
import { initServer } from '#src/web/server.js';
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
if (config.uploads.flushTempOnStart) {
|
||||||
|
await fs.rmdir(path.join(config.uploads.path, 'temp'), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(config.uploads.path, 'temp'));
|
||||||
|
}
|
||||||
|
|
||||||
|
await initServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
|
@ -0,0 +1,16 @@
|
||||||
|
export class HttpError extends Error {
|
||||||
|
constructor(message, httpCode, friendlyMessage, data) {
|
||||||
|
super(message);
|
||||||
|
|
||||||
|
this.name = 'HttpError';
|
||||||
|
this.httpCode = httpCode;
|
||||||
|
|
||||||
|
if (friendlyMessage) {
|
||||||
|
this.friendlyMessage = friendlyMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
import mitt from 'mitt';
|
||||||
|
|
||||||
|
export default mitt();
|
|
@ -0,0 +1,71 @@
|
||||||
|
import config from 'config';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import mime from 'mime';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import { fileTypeFromBuffer } from 'file-type';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
|
import { HttpError } from '#src/errors.js';
|
||||||
|
|
||||||
|
type Upload = {
|
||||||
|
fileName: string,
|
||||||
|
encoding: string,
|
||||||
|
mimetype: string,
|
||||||
|
tempName: string,
|
||||||
|
size: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
type UploadOptions = {
|
||||||
|
addToAlbum: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
function hashFile(fileBuffer) {
|
||||||
|
return crypto.createHash('sha256')
|
||||||
|
.update(fileBuffer)
|
||||||
|
.digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateMimetype(file, fileBuffer) {
|
||||||
|
const { mime } = await fileTypeFromBuffer(fileBuffer);
|
||||||
|
|
||||||
|
if (mime !== file.mimetype) {
|
||||||
|
throw new HttpError(`MIME type mismatch: ${file.mimetype} expected, ${mime} found`, 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateThumb(type, fileBuffer, hash, targetPath) {
|
||||||
|
if (type === 'image') {
|
||||||
|
await sharp(fileBuffer)
|
||||||
|
.resize({ height: config.uploads.thumbHeight })
|
||||||
|
.jpeg({ quality: config.uploads.thumbQuality })
|
||||||
|
.toFile(path.join(targetPath, `${hash}_t.jpg`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadFiles(files: Upload[], options: UploadOptions) {
|
||||||
|
console.log('FILES', files, options);
|
||||||
|
|
||||||
|
await files.reduce(async (chain, file) => {
|
||||||
|
await chain;
|
||||||
|
|
||||||
|
const tempFilePath = path.join(config.uploads.path, 'temp', file.tempName);
|
||||||
|
|
||||||
|
const fileBuffer = await fs.readFile(tempFilePath);
|
||||||
|
const hash = hashFile(fileBuffer);
|
||||||
|
|
||||||
|
await validateMimetype(file, fileBuffer);
|
||||||
|
|
||||||
|
const targetPath = path.join(config.uploads.path, 'full', hash.slice(0, 2), hash.slice(2, 4));
|
||||||
|
const extension = mime.getExtension(file.mimetype);
|
||||||
|
|
||||||
|
await fs.mkdir(targetPath, { recursive: true });
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
generateThumb(file.mimetype.split('/')[0], fileBuffer, hash, targetPath),
|
||||||
|
fs.rename(path.join(config.uploads.path, 'temp', file.tempName), path.join(targetPath, `${hash}.${extension}`)),
|
||||||
|
]);
|
||||||
|
}, Promise.resolve());
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
|
import { uploadFiles } from '#src/files.js';
|
||||||
|
|
||||||
|
export async function uploadFilesApi(req: Request, res: Response) {
|
||||||
|
const files = req.files as Express.Multer.File[];
|
||||||
|
|
||||||
|
const uploads = await uploadFiles(files.map((file) => ({
|
||||||
|
fileName: file.originalname,
|
||||||
|
encoding: file.encoding,
|
||||||
|
mimetype: file.mimetype,
|
||||||
|
tempName: file.filename,
|
||||||
|
size: file.size,
|
||||||
|
})), JSON.parse(req.body.options));
|
||||||
|
|
||||||
|
res.send(uploads);
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
export const root = `${__dirname}/../..`;
|
|
@ -0,0 +1,67 @@
|
||||||
|
import config from 'config';
|
||||||
|
import express from 'express';
|
||||||
|
import bodyParser from 'express';
|
||||||
|
import compression from 'compression';
|
||||||
|
import { renderPage, createDevMiddleware } from 'vike/server';
|
||||||
|
import multer from 'multer';
|
||||||
|
|
||||||
|
import { root } from '#web/root.js';
|
||||||
|
import { uploadFilesApi } from '#web/files.js';
|
||||||
|
|
||||||
|
const isProduction = process.env.NODE_ENV === 'production'
|
||||||
|
|
||||||
|
export type ResBody = {
|
||||||
|
body?: {},
|
||||||
|
statusMessage: string,
|
||||||
|
statusCode: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function initServer() {
|
||||||
|
const app = express()
|
||||||
|
// const upload = multer({ dest: './uploads' });
|
||||||
|
const upload = multer({ dest: './uploads/temp' });
|
||||||
|
|
||||||
|
app.use(compression())
|
||||||
|
app.use(bodyParser.json())
|
||||||
|
app.use(bodyParser.urlencoded())
|
||||||
|
|
||||||
|
// Vite integration
|
||||||
|
if (isProduction) {
|
||||||
|
// In production, we need to serve our static assets ourselves.
|
||||||
|
// (In dev, Vite's middleware serves our static assets.)
|
||||||
|
const sirv = (await import('sirv')).default
|
||||||
|
app.use(sirv(`${root}/dist/client`))
|
||||||
|
} else {
|
||||||
|
const { devMiddleware } = await createDevMiddleware({ root })
|
||||||
|
app.use(devMiddleware)
|
||||||
|
}
|
||||||
|
|
||||||
|
// app.post('/api/files', uploadFilesApi);
|
||||||
|
app.post('/api/files', upload.array('files'), uploadFilesApi);
|
||||||
|
|
||||||
|
// Vike middleware. It should always be our last middleware (because it's a
|
||||||
|
// catch-all middleware superseding any middleware placed after it).
|
||||||
|
app.get('*', async (req, res) => {
|
||||||
|
const pageContextInit = {
|
||||||
|
urlOriginal: req.originalUrl,
|
||||||
|
headersOriginal: req.headers
|
||||||
|
}
|
||||||
|
const pageContext = await renderPage(pageContextInit)
|
||||||
|
if (pageContext.errorWhileRendering) {
|
||||||
|
// Install error tracking here, see https://vike.dev/error-tracking
|
||||||
|
}
|
||||||
|
const { httpResponse } = pageContext
|
||||||
|
if (res.writeEarlyHints) res.writeEarlyHints({ link: httpResponse.earlyHints.map((e) => e.earlyHintLink) })
|
||||||
|
httpResponse.headers.forEach(([name, value]) => res.setHeader(name, value))
|
||||||
|
res.status(httpResponse.statusCode)
|
||||||
|
// For HTTP streams use pageContext.httpResponse.pipe() instead, see https://vike.dev/streaming
|
||||||
|
res.send(httpResponse.body)
|
||||||
|
})
|
||||||
|
|
||||||
|
const host = process.env.HOST || config.web.host || 'localhost';
|
||||||
|
const port = process.env.PORT || config.web.port || 3000;
|
||||||
|
|
||||||
|
app.listen(port, host, () => {
|
||||||
|
console.log(`Server running at http://${host}:${port}`)
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
// Make IDEs complain about missing file extension .js in import paths.
|
||||||
|
// Alternatively, we could always set "module" to "Node16" and add the file extension .js to import paths everywhere.
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "Node16",
|
||||||
|
"moduleResolution": "Node16"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"strict": false,
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"target": "ES2022",
|
||||||
|
"useUnknownInCatchVariables": false,
|
||||||
|
"lib": [
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable",
|
||||||
|
"ESNext"
|
||||||
|
],
|
||||||
|
"types": [
|
||||||
|
"vite/client"
|
||||||
|
],
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "vue",
|
||||||
|
"paths": {
|
||||||
|
"#src/*": ["./src/*"],
|
||||||
|
"#web/*": ["./src/web/*"],
|
||||||
|
"#layouts/*": ["./layouts/*"],
|
||||||
|
"#pages/*": ["./pages/*"],
|
||||||
|
"#assets/*": ["./assets/*"],
|
||||||
|
"#/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"types/*",
|
||||||
|
"src/**/*"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"dist",
|
||||||
|
"migrations",
|
||||||
|
"seeds",
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import path from 'path';
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import vike from 'vike/plugin'
|
||||||
|
import { UserConfig } from 'vite'
|
||||||
|
|
||||||
|
const config: UserConfig = {
|
||||||
|
plugins: [vue(), vike()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'#src': path.join(__dirname, 'src'),
|
||||||
|
'#web': path.join(__dirname, 'src', 'web'),
|
||||||
|
'#pages': path.join(__dirname, 'pages'),
|
||||||
|
'#components': path.join(__dirname, 'components'),
|
||||||
|
'#renderer': path.join(__dirname, 'renderer'),
|
||||||
|
'#assets': path.join(__dirname, 'assets'),
|
||||||
|
// '#': __dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
Loading…
Reference in New Issue