Merge branche webapps dans dev

This commit is contained in:
franck 2024-03-20 16:03:37 +01:00
parent 59bbfafbb7
commit 92c77c91e0
14 changed files with 919 additions and 6 deletions

View File

@ -0,0 +1,99 @@
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn inject_html_create_id() -> String {
String::from("
<div class='card'>
<div class='side-by-side'>
<h3>Create an Id</h3>
<div><a href='#'>Processes</a></div>
</div>
<form id='form4nk' action='#'>
<label for='password'>Password :</label>
<input type='password' id='password' /><hr/>
<input type='hidden' id='currentpage' value='creatid' />
<select id='selectProcess' class='custom-select'></select><hr/>
<div class='side-by-side'>
<button type='submit' id='submitButton' class='bg-primary'>Create</button>
<div>
<a href='#' id='displayrecover'>Recover</a>
</div>
</div>
</form><br/>
<div id='passwordalert' class='passwordalert'></div>
</div>
")
}
#[wasm_bindgen]
pub fn inject_html_recover() -> String {
String::from("
<div class='card'>
<div class='side-by-side'>
<h3>Recover my Id</h3>
<div><a href='#'>Processes</a></div>
</div>
<form id='form4nk' action='#'>
<label for='password'>Password :</label>
<input type='password' id='password' />
<input type='hidden' id='currentpage' value='recover' />
<select id='selectProcess' class='custom-select'></select><hr/>
<div class='side-by-side'>
<button type='submit' id='submitButton' class='recover bg-primary'>Recover</button>
<div>
<a href='#' id='displaycreateid'>Create an Id</a>
</div>
</div><hr/>
<a href='#' id='displayrevoke' class='btn'>Revoke</a>
</form><br/>
<div id='passwordalert' class='passwordalert'></div>
</div>
")
}
#[wasm_bindgen]
pub fn inject_html_revokeimage() -> String {
String::from("
<div class='card2'>
<form id='form4nk' action='#'>
<input type='hidden' id='currentpage' value='revokeimage' />
<button type='submit' id='submitButton'>
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'>
<path
d='M246.6 9.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 109.3V320c0 17.7 14.3 32 32 32s32-14.3 32-32V109.3l73.4 73.4c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-128-128zM64 352c0-17.7-14.3-32-32-32s-32 14.3-32 32v64c0 53 43 96 96 96H352c53 0 96-43 96-96V352c0-17.7-14.3-32-32-32s-32 14.3-32 32v64c0 17.7-14.3 32-32 32H96c-17.7 0-32-14.3-32-32V352z'
/>
</svg>
</button>
</form>
<div class='image-container'>
<label class='image-label bg-secondary'>Revoke image</label>
<img src='assets/revoke.jpeg' alt='' />
</div>
</div>
")
}
#[wasm_bindgen]
pub fn inject_html_revoke() -> String {
String::from("
<div class='card'>
<div class='side-by-side'>
<h3>Revoke an Id</h3>
<div>
<a href='#' id='displayrecover'>Recover</a>
</div>
</div>
<form id='form4nk' action='#'>
<label for='password'>Password :</label>
<input type='password' id='password' />
<hr/>
<div class='image-container'>
<label class='image-label'>Revoke image</label>
<img src='assets/revoke.jpeg' alt='' />
</div>
<hr/>
<button type='submit' id='submitButton' class='recover bg-primary'>Revoke</button>
</form>
</div>
")
}

View File

@ -1 +1,3 @@
pub mod api;
mod injecteurhtml;
mod process;

View File

@ -0,0 +1,10 @@
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn get_process() -> Vec<String> {
let mut data_process: Vec<String> = Vec::new();
data_process.push(String::from("process1"));
data_process.push(String::from("process2"));
data_process.push(String::from("process3"));
data_process
}

View File

@ -13,6 +13,7 @@
"author": "",
"license": "ISC",
"devDependencies": {
"copy-webpack-plugin": "^12.0.2",
"html-webpack-plugin": "^5.6.0",
"ts-loader": "^9.5.1",
"typescript": "^5.3.3",

BIN
src/assets/4nk_image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
src/assets/revoke.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

View File

@ -9,15 +9,15 @@ class Database {
options: {}
},
AnkUser: {
name: "4nkUser",
name: "user",
options: {}
},
AnkSession: {
name: "4nkSession",
name: "session",
options: {}
},
AnkProcess: {
name: "4nkProcess",
name: "process",
options: {}
}
}

View File

@ -2,9 +2,16 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="author" content="4NK">
<meta name="description" content="4NK Web5 Platform">
<meta name="keywords" content="4NK web5 bitcoin blockchain decentralize dapps relay contract">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>4NK Client</title>
<link rel="stylesheet" href="style/4nk.css">
<title>4NK Application</title>
</head>
<body>
<div id="containerId" class="container">
<!-- 4NK Web5 Solution -->
</div>
</body>
</html>
</html>

View File

@ -4,10 +4,18 @@ import IndexedDB from './database'
document.addEventListener('DOMContentLoaded', async () => {
try {
const services = await Services.getInstance();
const indexedDB = await IndexedDB.getInstance();
if ((await services.isNewUser())) {
services.displayCreateId();
}
else {
services.displayRecover()
}
const indexedDB = await IndexedDB.getInstance();
const db = indexedDB.getDb();
await indexedDB.writeObject(db, indexedDB.getStoreList().SpClient, services.new_sp_client(), "default");
} catch (error) {
console.error(error);
}

View File

@ -1,8 +1,11 @@
import IndexedDB from './database'
import Processstore from './store/processstore';
import Userstore from './store/userstore';
class Services {
private static instance: Services;
private sdkClient: any;
private static CURRENT_PROCESS = "currentprocess";
// Private constructor to prevent direct instantiation from outside
private constructor() {}
@ -44,6 +47,368 @@ class Services {
}
}
public async isNewUser(): Promise<boolean> {
let isNew = false;
try {
let listUserProcess = await Services.instance.getAllUserProces();
if (listUserProcess.length == 0) {
isNew = true;
}
} catch (error) {
console.error("Failed to retrieve isNewUser :", error);
}
return isNew;
}
public async displayCreateId(): Promise<void> {
Services.instance.injectHtml(Services.instance.get_html_create_id());
Services.instance.attachSubmitListener("form4nk", Services.instance.createId);
Services.instance.attachClickListener("displayrecover", Services.instance.displayRecover);
Services.instance.displayProcess(await Services.instance.getAllProcesAvailable());
}
public get_html_create_id(): string {
return this.sdkClient.inject_html_create_id();
}
public async createId(event: Event): Promise<void> {
event.preventDefault();
const passwordElement = document.getElementById("password") as HTMLInputElement;
const processElement = document.getElementById("selectProcess") as HTMLSelectElement;
if (!passwordElement || !processElement) {
console.error("One or more elements not found");
return;
}
const password = passwordElement.value;
const process = processElement.value;
console.log("JS password: " + password + " process: " + process);
// To comment if test
if (!Services.instance.isPasswordValid(password)) return;
// TODO get secretpart1 from User object
// const user = new this.sdkClient.User(password);
let secretpart1 = "LKHGKJJJ3H";
try {
let userstore = new Userstore;
userstore.secretpart1 = secretpart1;
userstore.process = process;
const indexedDb = await IndexedDB.getInstance();
const db = indexedDb.getDb();
await indexedDb.writeObject(db, indexedDb.getStoreList().AnkUser, userstore, process);
console.log("JS Userstore added");
await indexedDb.writeObject(db, indexedDb.getStoreList().AnkSession, process, Services.CURRENT_PROCESS);
console.log("JS Sessionstore added currentprocess");
} catch (error) {
console.error("Failed to write userstore object :", error);
}
await Services.instance.displayRevokeImage();
}
public async displayRecover(): Promise<void> {
Services.instance.injectHtml(Services.instance.get_html_recover());
Services.instance.attachSubmitListener("form4nk", Services.instance.recover);
Services.instance.attachClickListener("displaycreateid", Services.instance.displayCreateId);
Services.instance.attachClickListener("displayrevoke", Services.instance.displayRevoke);
Services.instance.attachClickListener("submitButtonRevoke", Services.instance.revoke);
Services.instance.displayProcess(await Services.instance.getAllUserProces());
}
public get_html_recover(): string {
return this.sdkClient.inject_html_recover();
}
public async recover(event: Event) {
event.preventDefault();
console.log("JS recover submit ");
const passwordElement = document.getElementById("password") as HTMLInputElement;
const processElement = document.getElementById("selectProcess") as HTMLSelectElement;
if (!passwordElement || !processElement) {
console.error("One or more elements not found");
return;
}
const password = passwordElement.value;
const process = processElement.value;
console.log("JS password: " + password + " process: " + process);
// To comment if test
if (!Services.instance.isPasswordValid(password)) return;
// TODO
alert("Recover submit to do ...");
}
public async displayRevokeImage(): Promise<void> {
const html = Services.instance.get_html_revokeimage();
Services.instance.injectHtml(html);
Services.instance.attachSubmitListener("form4nk", Services.instance.revokeimage);
}
public get_html_revokeimage(): string {
return this.sdkClient.inject_html_revokeimage();
}
public async revokeimage(event: Event): Promise<void> {
event.preventDefault();
console.log("JS revokeimage submit ");
// TODO
alert("Revokeimage submit to do ..., next page Update an id ...");
await Services.instance.displayUpdateAnId();
}
public async displayRevoke(): Promise<void> {
const html = Services.instance.get_html_revoke();
Services.instance.injectHtml(html);
Services.instance.attachClickListener("displayrecover", Services.instance.displayRecover);
Services.instance.attachSubmitListener("form4nk", Services.instance.revoke);
}
public get_html_revoke(): string {
return this.sdkClient.inject_html_revoke();
}
public async revoke(event: Event): Promise<void> {
event.preventDefault();
console.log("JS revoke click ");
// TODO
alert("revoke click to do ...");
}
public async displayUpdateAnId() {
let currentProcess = await this.getCurrentProcess();
let body = "";
let style = "";
let script = "";
let inputName = "";
try {
const indexedDB = await IndexedDB.getInstance();
const db = indexedDB.getDb();
try {
let processObject = await indexedDB.getObject<Processstore>(db, indexedDB.getStoreList().AnkProcess, currentProcess);
body = processObject.html;
style = processObject.style;
script = processObject.script;
inputName = processObject.inputName;
} catch (error) {
console.log("JS Processstore not exist ");
}
} catch (error) {
console.error("Failed to retrieve userstore object :", error);
}
Services.instance.injectUpdateAnIdHtml(body, style, script, inputName);
Services.instance.attachSubmitListener("form4nk", Services.instance.updateAnId);
}
public injectUpdateAnIdHtml(bodyToInject: string, styleToInject: string, scriptToInject: string, inputName: string) {
console.log("JS html : "+bodyToInject);
const body = document.getElementsByTagName('body')[0];
if (!body) {
console.error("No body tag");
return;
}
body.innerHTML = styleToInject + bodyToInject;
const script = document.createElement("script");
script.innerHTML = scriptToInject;
document.body.appendChild(script);
script.onload = () => {
console.log('Script loaded successfuly');
};
script.onerror = () => {
console.log('Error loading script');
};
}
public async updateAnId(event: Event): Promise<void> {
event.preventDefault();
// TODO get values
const firstNameElement = 'firstName';
const lastNameElement = 'lastName';
const firstName = document.getElementById(firstNameElement) as HTMLInputElement;
const lastName = document.getElementById(lastNameElement) as HTMLInputElement;
console.log("JS updateAnId submit ");
// TODO
alert("updateAnId submit to do ... Name : "+firstName.value + " "+lastName.value);
}
public displayProcess(processList: string[]): void {
console.log("JS processList : "+processList);
const selectProcess = document.getElementById("selectProcess");
if (selectProcess) {
processList.forEach((process) => {
let child = new Option(process, process);
if (!selectProcess.contains(child)) {
selectProcess.appendChild(child);
}
})
}
}
public async getAllUserProces(): Promise<string[]> {
let userProcessList: string[] = [];
try {
const indexedDB = await IndexedDB.getInstance();
const db = indexedDB.getDb();
let userListObject = await indexedDB.getAll<Userstore>(db, indexedDB.getStoreList().AnkUser);
userListObject.forEach(async (userObject) => {
const processName = userObject.process;
userProcessList.push(processName);
console.log("JS Userstore found");
})
} catch (error) {
console.log("JS Userstore not found");
}
return userProcessList;
}
public async getAllProcesAvailable(): Promise<string[]> {
let userProcessList = await Services.instance.getAllUserProces();
let processList = await Services.instance.getAllProces();
let availableProcessList = processList.filter(x => !userProcessList.includes(x));
return availableProcessList;
}
public async getAllProces(): Promise<string[]> {
// if indexedDB is empty, get list from wasm
let processList: string[] = [];
try {
const indexedDB = await IndexedDB.getInstance();
const db = indexedDB.getDb();
let processListObject = await indexedDB.getAll<Processstore>(db, indexedDB.getStoreList().AnkProcess);
processListObject.forEach(async (processObject) => {
const processName = processObject.process;
processList.push(processName);
console.log("JS Processstore found");
})
} catch (error) {
console.log("JS Processstore not found");
}
if (processList.length == 0) {
processList = await this.addProcessStore();
}
return processList;
}
public async addProcessStore(): Promise<string[]> {
const processList = this.sdkClient.get_process()
processList.forEach(async (process: string) => {
// TODO process mock
let processstore = new Processstore;
processstore.process = process;
const indexedDB = await IndexedDB.getInstance();
const db = indexedDB.getDb();
await indexedDB.writeObject(db, indexedDB.getStoreList().AnkProcess, processstore, process);
console.log("JS Processstore mock added");
})
return processList;
}
private async getSecretPart1(process: string): Promise<string> {
let secretpart1 = "";
try {
const indexedDB = await IndexedDB.getInstance();
const db = indexedDB.getDb();
try {
let userObject = await indexedDB.getObject<Userstore>(db, indexedDB.getStoreList().AnkUser, process);
secretpart1 = userObject.secretpart1;
console.log("JS Userstore exist secretpart1 : "+secretpart1);
} catch (error) {
console.log("JS Userstore not exist ");
}
} catch (error) {
console.error("Failed to retrieve userstore object :", error);
}
console.log("JS secretpart1 : "+secretpart1);
return secretpart1;
}
public attachClickListener(elementId: string, callback: (event: Event) => void): void {
const element = document.getElementById(elementId);
element?.removeEventListener("click", callback);
element?.addEventListener("click", callback);
}
public attachSubmitListener(elementId: string, callback: (event: Event) => void): void {
const element = document.getElementById(elementId);
element?.removeEventListener("submit", callback);
element?.addEventListener("submit", callback);
}
public injectHtml(html: string) {
console.log("JS html : "+html);
const container = document.getElementById('containerId');
if (!container) {
console.error("No html container");
return;
}
container.innerHTML = html;
}
public async getCurrentProcess(): Promise<string> {
let currentProcess = "";
try {
const indexedDB = await IndexedDB.getInstance();
const db = indexedDB.getDb();
currentProcess = await indexedDB.getObject<string>(db, indexedDB.getStoreList().AnkSession, Services.CURRENT_PROCESS);
} catch (error) {
console.error("Failed to retrieve currentprocess object :", error);
}
return currentProcess;
}
public isPasswordValid(password: string) {
var alertElem = document.getElementById("passwordalert");
var success = true;
var strength = 0;
if (password.match(/[a-z]+/)) {
var strength = 0;
strength += 1;
}
if (password.match(/[A-Z]+/)) {
strength += 1;
}
if (password.match(/[0-9]+/)) {
strength += 1;
}
if (password.match(/[$@#&!]+/)) {
strength += 1;
}
if (alertElem !== null) {
// TODO Passer à 18
if (password.length < 4) {
alertElem.innerHTML = "Password size is < 4";
success = false;
} else {
if (password.length > 30) {
alertElem.innerHTML = "Password size is > 30";
success = false;
} else {
if (strength < 4) {
alertElem.innerHTML = "Password need [a-z] [A-Z] [0-9]+ [$@#&!]+";
success = false;
}
}
}
}
return success;
}
}
export default Services;

237
src/store/processstore.ts Normal file
View File

@ -0,0 +1,237 @@
class Processstore {
process: string;
html: string;
style: string;
script: string;
inputName: string;
createDate: Date;
constructor() {
this.process = "";
this.html = getMockHtml();
this.style = getMockStyle();
this.script = getMockScript();
this.script = getMockScript();
this.inputName = getMockinputName();
this.createDate = new Date;
}
}
export default Processstore;
function getMockHtml(): string {
let html: string = `
<body>
<div class='container'>
<div>
<h3>Update an Id</h3>
</div>
<hr />
<form id='form4nk' action='#'>
<label for='firstName'>First Name:</label>
<input type='text' id='firstName' name='firstName' required />
<label for='lastName'>Last Name:</label>
<input type='text' id='lastName' name='lastName' required />
<label for='Birthday'>Birthday:</label>
<input type='date' id='Birthday' name='birthday' />
<label for='file'>File:</label>
<input type='file' id='fileInput' name='file' />
<label>Third parties:</label>
<div id='sp-address-block'>
<div class='side-by-side'>
<input
type='text'
name='sp-address'
id='sp-address'
placeholder='sp address'
form='no-form'
/>
<button
type='button'
class='circle-btn bg-secondary'
id='add-sp-address-btn'
>
+
</button>
</div>
</div>
<div class='div-text-area'>
<textarea
name='bio'
id=''
cols='30'
rows='10'
placeholder='Bio'
></textarea>
</div>
<button type='submit' class='bg-primary'>Update</button>
</form>
</div>
</body>
`;
return html;
}
function getMockStyle(): string {
let style: string = `
<style>
body {
margin: 0;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background-color: #f4f4f4;
font-family: 'Arial', sans-serif;
}
.container {
max-width: 400px;
width: 100%;
padding: 20px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
background-color: #ffffff;
border-radius: 8px;
text-align: left;
overflow: hidden;
}
form {
display: grid;
grid-template-columns: repeat(1fr, 2fr);
gap: 10px;
max-width: 400px;
margin: auto;
}
.bg-primary {
background-color: #1a61ed;
}
.bg-primary:hover {
background-color: #457be8;
}
.bg-secondary {
background-color: #2b81ed;
}
.bg-secondary:hover {
background-color: #5f9bff;
}
label {
text-align: left;
padding-right: 10px;
line-height: 2;
}
input {
width: 100%;
padding: 8px;
box-sizing: border-box;
}
button {
grid-column: span 2;
display: inline-block;
color: #fff;
border: none;
padding: 12px 17px;
border-radius: 4px;
cursor: pointer;
}
.div-text-area {
grid-column: span 2;
}
textarea {
width: 100%;
padding: 8px;
box-sizing: border-box;
}
.side-by-side {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 5px;
}
.circle-btn {
width: 25px;
height: 25px;
border-radius: 50%;
border: none;
color: white;
padding: 0px;
text-align: center;
}
#fileInput {
width: 100%;
padding: 8px;
padding-left: 0px;
box-sizing: border-box;
}
</style>`;
return style;
}
function getMockScript(): string {
let script: string = `
var addSpAddressBtn = document.getElementById('add-sp-address-btn');
var removeSpAddressBtn = document.querySelectorAll('.minus-sp-address-btn');
addSpAddressBtn.addEventListener('click', function (event) {
addDynamicField(this);
});
function addDynamicField(element) {
var addSpAddressBlock = document.getElementById('sp-address-block');
var spAddress = addSpAddressBlock.querySelector('#sp-address').value;
addSpAddressBlock.querySelector('#sp-address').value = '';
spAddress = spAddress.trim();
if (spAddress != '') {
var sideBySideDiv = document.createElement('div');
sideBySideDiv.className = 'side-by-side';
var inputElement = document.createElement('input');
inputElement.type = 'text';
inputElement.name = 'spAddresses[]';
inputElement.setAttribute('form', 'no-form');
inputElement.value = spAddress;
inputElement.disabled = true;
var buttonElement = document.createElement('button');
buttonElement.type = 'button';
buttonElement.className =
'circle-btn bg-secondary minus-sp-address-btn';
buttonElement.innerHTML = '-';
buttonElement.addEventListener('click', function (event) {
removeDynamicField(this.parentElement);
});
sideBySideDiv.appendChild(inputElement);
sideBySideDiv.appendChild(buttonElement);
addSpAddressBlock.appendChild(sideBySideDiv);
}
function removeDynamicField(element) {
element.remove();
}
}
`;
return script;
}
function getMockinputName(): string {
return "firstName";
}

15
src/store/userstore.ts Normal file
View File

@ -0,0 +1,15 @@
class Userstore {
process: string;
secretpart1: string;
password: string;
createDate: Date;
constructor() {
this.process = "";
this.secretpart1 = "";
this.password = "";
this.createDate = new Date;
}
}
export default Userstore;

162
src/style/4nk.css Normal file
View File

@ -0,0 +1,162 @@
body {
margin: 0;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background-color: #f4f4f4;
font-family: 'Arial', sans-serif;
}
.container {
text-align: center;
}
.card {
max-width: 400px;
width: 100%;
padding: 20px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
background-color: #ffffff;
border-radius: 8px;
text-align: left;
overflow: hidden;
}
form {
display: flex;
flex-direction: column;
/* flex-wrap: wrap; */
}
label {
font-weight: bold;
margin-bottom: 8px;
}
hr {
border: 0;
height: 1px;
background-color: #ddd;
margin: 10px 0;
}
input, select {
width: 100%;
padding: 10px;
margin: 8px 0;
box-sizing: border-box;
}
select {
padding: 10px;
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
}
button {
display: inline-block;
background-color: #4caf50;
color: #fff;
border: none;
padding: 12px 17px;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
.side-by-side {
display: flex;
align-items: center;
justify-content: space-between;
}
.side-by-side>* {
display: inline-block;
}
button.recover {
display: inline-block;
text-align: center;
text-decoration: none;
display: inline-block;
background-color: #4caf50;
color: #fff;
border: none;
padding: 12px 17px;
border-radius: 4px;
cursor: pointer;
}
button.recover:hover {
background-color: #45a049;
}
a.btn {
display: inline-block;
text-align: center;
text-decoration: none;
display: inline-block;
background-color: #4caf50;
color: #fff;
border: none;
padding: 12px 17px;
border-radius: 4px;
cursor: pointer;
}
a.btn:hover {
background-color: #45a049;
}
a {
text-decoration: none;
color: #78a6de;
}
.bg-secondary {
background-color: #2b81ed;
}
.bg-primary {
background-color: #1A61ED;
}
.bg-primary:hover {
background-color: #457be8;
}
.card2 {
display: flex;
flex-direction: column;
max-width: 400px;
width: 100%;
padding: 20px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
background-color: #ffffff;
border-radius: 8px;
text-align: center;
align-items: center;
overflow: hidden;
}
.card2 button {
max-width: 50px;
width: 100%;
background: none;
border: none;
cursor: pointer;
}
.card2 svg {
width: 100%;
height: auto;
fill: #333;
}
.image-label {
display: block;
color: #fff;
padding: 5px;
margin-top: 10px;
}
.image-container {
width: 400px;
height: 300px;
overflow: hidden;
}
.image-container img {
text-align: center;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center center;
}
.passwordalert {
color: #FF0000;
}

View File

@ -1,5 +1,6 @@
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
mode: 'development',
@ -32,6 +33,12 @@ module.exports = {
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
new CopyWebpackPlugin({
patterns: [
{ from: 'src/assets', to: './assets' },
{ from: 'src/style', to: './style' }
],
}),
],
devServer: {
static: './dist',