feature/new_look_default_theme #22

Merged
sergiotarxz merged 7 commits from feature/new_look_default_theme into master 2021-01-22 01:25:08 +01:00
14 changed files with 1012 additions and 156 deletions
Showing only changes of commit 9795d5856e - Show all commits

View File

@ -5,5 +5,6 @@
clients => 3,
proxy => 1,
pid_file => $ENV{PIDFILE} || '/var/run/peertube-dl-web.pid',
}
},
theme => 'new_look_default',
};

View File

@ -1,194 +1,274 @@
body {
height: 98vh;
display: flex;
flex-flow: column;
align-items: center;
justify-content: center;
background-color: #111827;
height: 99.9%;
margin: 0;
padding: 0;
}
.application-container {
display: flex;
flex-flow: column;
align-items: center;
justify-content: center;
border-radius: 0.3rem;
background-color: #1f2937;
padding: 1.5rem 3rem 1.5rem 3rem;
a {
color: blue;
}
.application-container form {
display: flex;
flex-flow: column;
justify-content: center;
width: 100%;
padding: 1rem;
a:hover,a:focus {
text-decoration: underline;
}
.application-container h2 {
color: #ffffff;
font-weight: 400;
font-size: 1.6rem;
}
.application-container input {
padding: 1rem;
color: #ffffff;
background-color: #374151;
border-radius: 0.2rem;
border: 1px solid transparent;
}
.application-container button {
margin-top: 1rem;
color: #ffffff;
font-weight: bold;
background-color: #059669;
border-radius: 0.2rem;
border: 1px solid transparent;
padding: 0.5rem 1rem 0.5rem 1rem;
}
.application-container button,
.application-container input {
font-size: 0.9rem;
}
.application-container.active {
display: flex;
}
#poping-notice {
position: absolute;
display: none;
padding: 3rem;
border-radius: 0.3rem;
background-color: #374151;
color: white;
}
#poping-notice-content a {
text-decoration: none;
color: #10b981;
}
#poping-notice-container-bar {
display: flex;
justify-content: center;
}
#close-poping-notice {
background-color: #059669;
padding: 0.5rem 5rem 0.5rem 5rem;
border-radius: 0.3rem;
text-decoration: none;
font-weight: bolder;
color: white;
#video {
width: 100%;
margin: 3px;
}
#modal-video-container {
position: absolute;
display: none;
display: none;
background: white;
position: fixed;
top: 50%;
left: 50%;
height: 100%;
width: 100%;
transform: translate(-50%, -50%);
border: black 1px solid;
}
height: 100vh;
width: 100%;
flex-flow: column;
align-items: center;
background-color: #111827;
#modal-video-container.active {
display: block;
}
.video-container-bar {
width: 95%;
height: 2rem;
padding: 1rem;
display: flex;
justify-content: end;
display: flex;
justify-content: right;
}
.video-container-bar a {
padding: 1rem;
border-radius: 0.3rem;
display: flex;
align-items: center;
justify-content: center;
background-color: #dc2626;
#close-and-reset-video-container {
margin-top: 0.25rem;
margin-right: 0.25rem;
border: 1px solid black;
}
#modal-video-container > #block {
display: flex;
flex-flow: column;
align-items: center;
justify-content: center;
#close-and-reset-video-container:hover,#close-and-reset-video-container:focus {
background: black;
color: white;
}
#download-video-container {
padding: 2rem;
display: flex;
align-items: center;
justify-content: center;
margin: 10px;
justify-content: center;
}
#download-video-prepare,
#download-video {
display: none;
border-radius: 0.3rem;
background-color: #059669;
text-decoration: none;
color: white;
user-select: none;
cursor: pointer;
#download-video-container a {
display: none;
padding-left: 5px;
padding-right: 5px;
padding-top: 5px;
padding-bottom: 5px;
width: 100%;
background: #0a0;
border-radius: 5px;
font-size: 30px;
color: white;
height: 30px;
}
#download-video-loading.active {
height: 2rem;
#download-video-container a.active {
display: block;
}
#download-video-container a.active,
#download-video-loading.active {
display: block;
padding: 0.5rem 2rem 0.5rem 2rem;
#download-video-container a embed {
height: 30px;
}
#modal-loading,
#modal-format-selector {
display: none;
#video-container {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: 100%;
}
#poping-notice.active,
#modal-video-container.active {
--tw-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000),
var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
.block {
display: block;
}
.application-container {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
text-align: center;
height: 100%;
background: #000;
color: white;
}
#download-form {
display: flex;
justify-content: center;
flex-direction: column;
}
#download-form-button {
margin-top: 5px;
height: 50px;
font-size: 1.5rem;
background: #fff;
color: black;
border: none;
}
h2 {
font-size: 2rem;
}
#modal-loading {
display: none;
position: fixed;
top: 50%;
left: 50%;
height: 100%;
width: 100%;
transform: translate(-50%, -50%);
border: black 1px solid;
justify-content: right;
align-items: center;
}
#modal-loading.active {
display: flex;
}
#modal-loading embed {
width: 10%;
}
#poping-notice {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border: black 1px solid;
width: 91%;
background: white;
padding: 10px;
border-radius: 15px;
max-height: 95%;
overflow-y: scroll;
}
#poping-notice.active {
display: block;
display: block;
}
#modal-video-container.active {
display: flex;
#poping-notice-container-bar {
display: flex;
justify-content: center;
font-size: 5rem;
}
@media (min-width: 812px) {
#poping-notice {
width: 40%;
}
#close-poping-notice {
width: 150px;
height: 150px;
align-items: center;
display: flex;
text-align: center;
justify-content: center;
border-radius: 50%;
background: #0f0;
color: black;
text-decoration: none;
}
#close-poping-notice:hover,#close-poping-notice:focus {
background: black;
color: white;
}
#modal-format-selector {
display: none;
background: white;
position: fixed;
top: 50%;
left: 50%;
height: 100%;
width: 100%;
transform: translate(-50%, -50%);
border: black 1px solid;
flex-direction: column;
overflow-y: scroll;
}
#modal-format-selector.active {
display: flex;
}
#modal-format-selector > h2 {
text-align: center;
}
#modal-format-selector > p {
margin-left: 2rem;
}
#modal-format-selector .format-list {
box-sizing: border-box;
background: #fff;
margin: 2rem;
}
#close-modal-format-selector {
margin-top: 0.50rem;
margin-right: 0.50rem;
border: 1px solid black;
background: grey;
color: white;
width: 25px;
height: 25px;
text-align: center;
font-size: 20px;
font-weight: bold;
}
#close-modal-format-selector:hover,#close-modal-format-selector:focus {
background: black;
}
.format-list > div {
width: 100%;
display: grid;
grid-auto-columns: 50%;
grid-template-areas: "a a";
}
.format-list > div > a {
border: 1px solid black;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
padding-right: 5%;
padding-left: 5%;
text-decoration: none;
color: black;
background: #eee;
overflow-wrap: anywhere;
}
.format-list > div > a:hover {
background: black;
color: white;
}
.format-list > div > a:after {
padding-bottom: 100%;
display: block;
content: "";
}
div.video-formats a {
background: #f00;
}
@media (min-width: 668px) {
#poping-notice {
width: 629px;
}
}

View File

@ -0,0 +1,252 @@
body {
height: 98vh;
display: flex;
flex-flow: column;
align-items: center;
justify-content: center;
background-color: #111827;
}
.application-container {
display: flex;
flex-flow: column;
align-items: center;
justify-content: center;
border-radius: 0.3rem;
background-color: #1f2937;
padding: 1.5rem 3rem 1.5rem 3rem;
}
.application-container form {
display: flex;
flex-flow: column;
justify-content: center;
width: 100%;
padding: 1rem;
}
.application-container h2 {
color: #ffffff;
font-weight: 400;
font-size: 1.6rem;
}
.application-container input {
padding: 1rem;
color: #ffffff;
background-color: #374151;
border-radius: 0.2rem;
border: 1px solid transparent;
}
.application-container button {
margin-top: 1rem;
color: #ffffff;
font-weight: bold;
background-color: #059669;
border-radius: 0.2rem;
border: 1px solid transparent;
padding: 0.5rem 1rem 0.5rem 1rem;
}
.application-container button:hover,.application-container button:focus {
background-color: #059;
}
.application-container button,
.application-container input {
font-size: 0.9rem;
}
.application-container.active {
display: flex;
}
#poping-notice {
position: absolute;
display: none;
padding: 3rem;
border-radius: 0.3rem;
background-color: #374151;
color: white;
}
#poping-notice-content a {
text-decoration: none;
color: #10b981;
}
#poping-notice-content a:hover,#poping-notice-content a:focus {
color: #0ae;
}
#poping-notice-container-bar {
display: flex;
justify-content: center;
}
#close-poping-notice {
background-color: #059669;
padding: 0.5rem 5rem 0.5rem 5rem;
border-radius: 0.3rem;
text-decoration: none;
font-weight: bolder;
color: white;
}
#close-poping-notice:hover,#close-poping-notice:focus {
background-color: #059;
}
#modal-video-container {
position: absolute;
display: none;
height: 100vh;
width: 100%;
flex-flow: column;
align-items: center;
background-color: #111827;
}
.video-container-bar {
width: 95%;
height: 2rem;
padding: 1rem;
display: flex;
justify-content: end;
}
.video-container-bar a {
padding: 1rem;
border-radius: 0.3rem;
display: flex;
align-items: center;
justify-content: center;
background-color: #dc2626;
}
.video-container-bar a:hover,.video-container-bar a:focus {
color: white;
background-color: grey;
}
#modal-video-container > #block {
display: flex;
flex-flow: column;
align-items: center;
justify-content: center;
}
#download-video-container {
padding: 2rem;
display: flex;
align-items: center;
justify-content: center;
}
.button-download {
display: none;
border-radius: 0.3rem;
background-color: #059669;
text-decoration: none;
color: white;
user-select: none;
cursor: pointer;
height: 30px;
padding: 0 2rem;
}
.button-download:hover,.button-download:focus {
background-color: #059;
}
.button-download embed {
height: 100%;
}
#download-video-loading.active {
height: 2rem;
}
#download-video-container .button-download.active {
display: flex;
align-items: center;
}
#modal-loading,
#modal-format-selector {
display: none;
}
#poping-notice.active,
#modal-video-container.active {
--tw-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000),
var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
#poping-notice.active {
display: block;
}
#modal-video-container.active {
display: flex;
}
#modal-loading {
display: none;
position: fixed;
top: 50%;
left: 50%;
height: 100%;
width: 100%;
transform: translate(-50%, -50%);
border: black 1px solid;
justify-content: right;
align-items: center;
}
#modal-loading.active {
display: flex;
}
#modal-loading embed {
width: 10%;
}
#video {
width: 100%;
}
#video-container {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
@media (min-width: 812px) {
#poping-notice {
width: 40%;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -0,0 +1,69 @@
<html>
<head>
<script src="js/peertube-dl-web.js"></script>
<link rel="stylesheet" href="css/index.css"></script>
<link rel="icon" type="image/png" href="img/peertube-dl-logo.png"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div class="application-container">
<h2>Peertube-dl Web Application</h2>
<form id="download-form">
<input class="block" type="text" id="download-form-url" placeholder="Introduce the url you want to download."/>
<button class="block" id="download-form-button" >Fetch from api</button>
</form>
</div>
<div id="modal-format-selector">
<div class="video-container-bar">
<a id="close-modal-format-selector">x</a>
</div>
<h2>Example video</h2>
<p>Example description</p>
<div class="format-list">
<h3>Video Formats.</h3>
<div class="video-formats">
</div>
<h3>Audio Formats.</h3>
<div class="audio-formats">
</div>
</div>
</div>
<div id="modal-loading">
<embed src="img/spinner.svg"/>
</div>
<div id="modal-video-container">
<div class="video-container-bar">
<a id="close-and-reset-video-container">x</a>
</div>
<div id="video-container">
<div class="block">
<video id="video" controls="controls"></video>
</div>
<div id="download-video-container" class="block">
<a id="download-video-prepare" class="button-download active">Prepare download</a>
<a id="download-video-loading" class="button-download"><embed src="img/spinner.svg"/></a>
<a id="download-video" class="button-download">Download</a>
</div>
</div>
</div>
<div id="poping-notice">
<div id="poping-notice-content">
<p>This webpage is free as in freedom software, it is offered to you with the hope it will be useful, but without any warranty,
you can find the source code at <a href="https://gitea.sergiotarxz.freemyip.com/sergiotarxz/Peertube-dl">my gitea</a> with docs to setup your own
webpage like this, this software is licensed under the AGPLv3 license which means you MUST convey the source code in a human readable form
if you distribute this software or use it as an service to users of service or distributees.<p>
<p>I hope that if you find a non supported url which should be supported, a bug, or a feature you would like this webpage to have you file an issue in
<a href="https://gitea.sergiotarxz.freemyip.com/sergiotarxz/Peertube-dl/issues">https://gitea.sergiotarxz.freemyip.com/sergiotarxz/Peertube-dl/issues</a>
to help this software improve since I find tracking users a pretty bad way to discover bugs and potential good features.</p>
<p>This webpage may load third party resources depending on the url you give to it which may put cookies in your browser, you are
encouraged to frecuently delete your browser cookies to avoid those third parties tracking you on internet, Firefox offers you an
option to delete cookies as soon as you close the browser which may be a good idea to enable in the orwellian internet of today.</p>
</div>
<div id="poping-notice-container-bar">
<a id="close-poping-notice" href="#">X</a>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,13 @@
{
"name": "default-theme",
"version": "1.0.0",
"private": true,
"license": "MIT",
"scripts": {
"build": "webpack"
},
"dependencies": {
"webpack": "^5.15.0",
"webpack-cli": "^4.3.1"
}
}

View File

@ -0,0 +1,106 @@
"use strict";
import { PopingNotice } from './view/poping_notice.js';
import { DownloadForm } from './view/download_form.js';
import { LoadingModal } from './view/loading_modal.js';
import { VideoContainer } from './view/video_container.js';
import { FormatSelector } from './view/format_selector.js';
class Application {
constructor() {
this.poping_notice = new PopingNotice();
this.download_form = new DownloadForm(this.onDownloadFormGot.bind(this));
this.loading_modal = new LoadingModal();
this.video_container = new VideoContainer();
this.format_selector = new FormatSelector();
}
init() {
this.popingNotice.setVisible(true);
}
onDownloadFormGot(url) {
this.dispatchURL(url);
}
dispatchURL(url, format) {
this.loadingModal.setVisible(true);
let error_str;
let success = this.queryAPI(url, format).then( (response) => {
if ( response.options !== undefined && response.options.list_formats !== undefined && response.options.list_formats ) {
if ( response.formats === undefined
|| response.formats.audio_formats === undefined
|| response.formats.video_formats === undefined ) {
throw 'Format object is not valid.';
}
this.formatSelector.prepareFormatSelector(
response.title, response.description,
response.formats.audio_formats, response.formats.video_formats,
( id ) => {
this.loadingModal.setVisible(true);
this.dispatchURL(url, id);
});
this.formatSelector.setVisible(true);
this.loadingModal.setVisible(false);
} else {
this.videoContainer.onCanPlay(this.onCanPlayVideoContainer.bind(this));
this.videoContainer.setURLVideo(response.url);
this.videoContainer.setFilename(response.filename);
}
}).catch( (error) => {
error_str = error.toString();
this.loadingModal.setVisible(false);
let input_url = document.createElement('a');
input_url.href = url;
input_url.innerText = url;
let issues_url = document.createElement('a');
issues_url.href = 'https://gitea.sergiotarxz.freemyip.com/sergiotarxz/Peertube-dl/issues';
issues_url.innerText = 'here';
this.popingNotice.setMessage( [ 'The url ', input_url, ' is not supported, the error was: ', error_str , ' if you think this is an error, report it ', issues_url, '.' ]);
this.popingNotice.setVisible(true);
});
}
onCanPlayVideoContainer() {
this.videoContainer.setVisible(true);
this.loadingModal.setVisible(false);
}
async queryAPI(url, format) {
let request = { url: url };
if (format !== undefined)
request.format = format;
const response = await fetch('/api', {
method: 'POST',
mode: 'cors',
cache: 'no-cache',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(request),
});
return response.json();
}
get formatSelector() {
return this.format_selector;
}
get videoContainer() {
return this.video_container;
}
get downloadForm() {
return this.download_form;
}
get popingNotice() {
return this.poping_notice;
}
get loadingModal() {
return this.loading_modal;
}
}
export { Application };

View File

@ -0,0 +1,10 @@
"use strict";
import { Application } from './application.js';
window.addEventListener('load', (event) => {
let application = new Application();
application.init();
});

View File

@ -0,0 +1,35 @@
"use strict";
class DownloadForm {
constructor(callback) {
this.query_selector = '#download-form';
this.callback = (event) => {
event.preventDefault();
callback(this.downloadFormUrl.value);
};
this.addEventListeners();
}
addEventListeners() {
this.downloadFormButton.addEventListener('click', this.callback);
this.element.addEventListener('submit', this.callback);
}
get downloadFormButton() {
return this.element.querySelector('#download-form-button');
}
get downloadFormUrl() {
return this.element.querySelector('#download-form-url');
}
get querySelector() {
return this.query_selector;
}
get element() {
return document.querySelector(this.querySelector);
}
}
export { DownloadForm };

View File

@ -0,0 +1,87 @@
"use strict";
class FormatSelector {
constructor() {
this.query_selector = '#modal-format-selector';
this.addEventListeners();
}
appendFormat(container, object, is_video, callback) {
let a = document.createElement('a');
if ( is_video ) {
a.innerText = 'Id: ' + object.id + "\n"
+ 'Format: ' + object.mimeType + "\n"
+ 'QualityLabel: ' + object.qualityLabel + "p\n"
+ 'Bitrate: ' + object.bitrate + "\n"
+ (
( object.audioSampleRate !== undefined ) ?
'AudioSampleRate: ' + object.audioSampleRate + ".\n" :
"No audio."
);
} else {
a.innerText = 'Id: ' + object.id + "\n"
+ 'Format: ' + object.mimeType + "\n"
+ 'AudioSampleRate: ' + object.audioSampleRate + "\n"
+ 'Bitrate: ' + object.bitrate + ".\n";
}
a.addEventListener( 'click', (event) => {
callback(object.id);
});
container.appendChild(a);
}
prepareFormatSelector(title, description, audio_formats, video_formats, callback) {
this.titleFormatSelector.innerText = title;
this.descriptionFormatSelector.innerText = description;
this.videoFormats.innerHTML = '';
this.audioFormats.innerHTML = '';
for ( let x of audio_formats) {
this.appendFormat(this.audioFormats, x, false, callback);
}
for ( let x of video_formats ) {
this.appendFormat(this.videoFormats, x, true, callback);
}
}
setVisible(option) {
if (option) {
this.element.classList.add('active');
} else {
this.element.classList.remove('active');
}
}
addEventListeners() {
this.closeFormatSelector.addEventListener('click', (event) => { this.setVisible(false); });
}
get videoFormats() {
return this.element.querySelector('.video-formats');
}
get audioFormats() {
return this.element.querySelector('.audio-formats');
}
get titleFormatSelector() {
return this.element.querySelector('h2');
}
get descriptionFormatSelector() {
return this.element.querySelector('p');
}
get closeFormatSelector() {
return this.element.querySelector('#close-modal-format-selector');
}
get element() {
return document.querySelector(this.querySelector);
}
get querySelector() {
return this.query_selector;
}
}
export { FormatSelector };

View File

@ -0,0 +1,25 @@
"use strict";
class LoadingModal {
constructor() {
this.query_selector = '#modal-loading';
}
setVisible(option) {
if (option) {
this.element.classList.add('active');
} else {
this.element.classList.remove('active');
}
}
get element() {
return document.querySelector(this.querySelector);
}
get querySelector() {
return this.query_selector;
}
}
export { LoadingModal };

View File

@ -0,0 +1,56 @@
"use strict";
class PopingNotice {
constructor() {
this.query_selector = '#poping-notice';
this.closePopingNotice.addEventListener('click', (event) => {
this.setVisible(false);
});
}
setVisible(option) {
if (option) {
this.element.classList.add('active');
} else {
this.element.classList.remove('active');
}
}
setMessage(message) {
if (!message instanceof Array)
throw 'Message is not instance of Array.';
let p = document.createElement('p');
for (let node of message) {
if (typeof node === "string"
|| node instanceof String) {
node = document.createTextNode(node);
p.appendChild(node);
} else if ( node instanceof Node) {
p.appendChild(node);
} else {
throw ('Node is not a instance of Node nor a String');
}
}
this.popingNoticeContent.innerHTML = '';
this.popingNoticeContent.appendChild(p);
}
get querySelector() {
return this.query_selector;
}
get element() {
return document.querySelector(this.querySelector);
}
get popingNoticeContent() {
return this.element.querySelector('#poping-notice-content');
}
get closePopingNotice() {
return this.element.querySelector('#close-poping-notice');
}
}
export { PopingNotice };

View File

@ -0,0 +1,112 @@
"use strict";
class VideoContainer {
constructor() {
this.query_selector = '#modal-video-container';
this.addEventListeners();
}
setVisible(option) {
if (option) {
this.element.classList.add('active');
} else {
this.element.classList.remove('active');
this.downloadVideoPrepare.classList.add('active');
this.downloadVideoLoading.classList.remove('active');
this.downloadVideo.classList.remove('active');
}
}
addEventListeners() {
this.downloadVideoPrepare.addEventListener('click', this.downloadPrepareHandler.bind(this));
this.closeAndResetVideoContainer.addEventListener('click', (event) => {
this.setVisible(false);
});
}
downloadPrepareHandler(event) {
this.downloadVideoPrepare.classList.remove('active');
this.downloadVideoLoading.classList.add('active');
this.generateBlobVideo(this.URLVideo).then( blob => {
this.downloadVideo.href = URL.createObjectURL(blob);
this.downloadVideo.download = this.filename;
this.downloadVideoLoading.classList.remove('active');
this.downloadVideo.classList.add('active');
});
}
async generateBlobVideo(url) {
const blob = await fetch(url, { mode: 'cors', })
.then(res => res.blob())
.catch( err => this.generateBlobVideoByProxy(url) );
return blob;
}
async generateBlobVideoByProxy(url) {
const blob = await fetch( '/proxy_to_get', {
method: 'POST',
mode: 'cors',
cache: 'no-cache',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({url: url}),
}
).then(res => res.blob());
return blob;
}
onCanPlay(callback) {
video.addEventListener('canplay', (event) => {
callback();
});
}
setURLVideo(url) {
this.url_video = url;
video.src = url;
}
setFilename(filename) {
this.filename = filename;
}
get closeAndResetVideoContainer() {
return this.element.querySelector('#close-and-reset-video-container');
}
get downloadVideo() {
return this.element.querySelector('#download-video');
}
get URLVideo() {
return this.url_video;
}
get closeAndResetVideoContainer() {
return this.element.querySelector('#close-and-reset-video-container');
}
get downloadVideoLoading() {
return this.element.querySelector('#download-video-loading');
}
get downloadVideoPrepare() {
return this.element.querySelector('#download-video-prepare');
}
get video() {
return this.element.querySelector('#video');
}
get element() {
return document.querySelector(this.querySelector);
}
get querySelector() {
return this.query_selector;
}
}
export { VideoContainer };

View File

@ -0,0 +1,10 @@
const path = require('path');
module.exports = {
entry: './src/index.js',
devtool: 'source-map',
output: {
filename: 'peertube-dl-web.js',
path: path.resolve(__dirname, 'dist/js'),
},
};