Skip to content
Snippets Groups Projects
Commit 80901d10 authored by Mehdi Mohamed Mahmoud's avatar Mehdi Mohamed Mahmoud
Browse files

Merge branch 'master' into 'main'

Push existing project to GitLab

See merge request !1
parents 04577841 2e5247f3
No related branches found
No related tags found
1 merge request!1Push existing project to GitLab
Showing
with 698 additions and 0 deletions
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (min-width: 1024px) {
body {
display: grid;
place-items: center;
}
}
<script setup>
import { ref, onBeforeMount } from 'vue'
import {sendExpression, retrieveExpressions} from "../utils/Api.js"
import { useTokenStore } from "../stores/TokenStore.js";
const equation = ref("");
const results = ref([]);
const allowed = ref(true);
const pages = ref([]);
const currentPage = ref(0);
const token = useTokenStore().jwtToken;
let listId = 0;
onBeforeMount(async () => {
if(!token){
console.log("Unauthenticated context");
} else{
await getExpressions(0);
}
})
async function getExpressions(pageNumber){
let response = await retrieveExpressions(pageNumber,token);
let list = response.data['content'];
for (let i = 0; i < list.length; i++) {
list[i] = list[i]['expression']+" = "+list[i]['answer'];
}
results.value = list;
let amtPages = response.data['totalPages'];
currentPage.value = response.data.pageable.pageNumber;
for (let i = 0; i < amtPages; i++) {
pages.value[i] = {id:listId++, page:i}
}
console.log(pages)
}
function append(c) {
allowed.value = true;
equation.value = equation.value.concat(c);
}
function clear(){
allowed.value = true;
equation.value = "";
}
function remove(){
allowed.value = true;
equation.value = equation.value.slice(0,-1);
}
async function equal(){
let response = await sendExpression(equation.value,token);
let answer = response.data['answer'];
if(answer !== "nan"){
let result = equation.value.concat(" = " + answer);
results.value.push(result);
equation.value = "";
await getExpressions(currentPage.value)
}else{
allowed.value = false;
}
}
function validateInput(event){
allowed.value = true;
let inputValue = event.target.value;
inputValue = inputValue.replace(/[^0-9+%*/. ]/g, "");
equation.value = inputValue;
}
</script>
<template>
<div id="container" v-if="token">
<section class="calculator">
<div id="info">
<h2>Calculator</h2>
<p>this is a simple calculator, don't try to misuse it or santa will know....</p>
</div>
<input v-model="equation" @input="validateInput" @keydown.enter="equal"/>
<button @click="clear()">AC</button>
<button @click="remove()">DEL</button>
<button @click="append('%')">mod</button>
<button @click="append('/')" class="fin">/</button>
<button @click="append('7')">7</button>
<button @click="append('8')">8</button>
<button @click="append('9')">9</button>
<button @click="append('*')" class="fin">x</button>
<button @click="append('4')">4</button>
<button @click="append('5')">5</button>
<button @click="append('6')">6</button>
<button @click="append('-')" class="fin">-</button>
<button @click="append('1')">1</button>
<button @click="append('2')">2</button>
<button @click="append('3')">3</button>
<button @click="append('+')" class="fin">+</button>
<button @click="append('0')" id="zero">0</button>
<button @click="append('.')">.</button>
<button @click="equal()" class="fin" id="equal">=</button>
</section>
<section class="log">
<h2>Log</h2>
<ul id="log-list">
<li v-for="result in results">{{result}}</li>
</ul>
<ul id="pages">
<li v-for="page in pages" :key="page.id">
<button @click="getExpressions(page.page)" >{{page.page}}</button>
</li>
</ul>
<div class="alert" v-if="!allowed">
This equation is not allowed
</div>
</section>
</div>
<div v-else>
<h2>Unauthorized!!!</h2>
</div>
</template>
<style scoped>
#container{
font-family: "Arial", sans-serif;
display: grid;
grid-template-columns: repeat(2,1fr);
justify-content: center;
}
.calculator{
grid-column: 1/2;
display: grid;
grid-template-columns: repeat(4,1fr);
grid-auto-rows: minmax(50px, auto);
gap: 5px;
}
#pages{
display: flex;
justify-content: center;
}
#info{
grid-column: 1/5;
text-align: center;
}
#zero{
grid-column: 1/3;
}
img {
width:100%;
border-radius: 5px;
}
input{
grid-column: 1/5;
font-size: 100%;
text-align: center;
border-radius: 5px;
background-color: #333;
color: white;
}
.log{
text-align: center;
margin-left: 5px;
border-radius: 5px;
}
ul{
text-align: left;
list-style: none;
}
button{
outline: none;
background-color: #F2F2F2;
border-radius: 5px;
}
button:hover{
background-color: yellow;
}
.fin{
background-color: orange;
color: white;
}
.alert {
padding: 20px;
background-color: orange;
color: white;
margin-bottom: 15px;
border-radius: 5px;
}
@media only screen and (max-width: 600px) {
#container{
display: grid;
grid-template-columns: 1fr;
}
.calculator{
grid-column: 1;
}
.log{
grid-column: 1;
margin-left:0;
margin-top: 4px;
}
}
</style>
\ No newline at end of file
<script setup>
import { ref, watch } from 'vue'
import { useFormStore } from "../stores/FormInfo.js";
import {storeUser} from "../utils/Api.js"
const isValid = ref(false);
const formStore = useFormStore();
const name = ref(formStore.name);
const email = ref(formStore.email);
const message = ref("");
async function submit(){
formStore.saveUserInStore(name.value,email.value);
let status = await storeUser(name.value,email.value,message.value);
alert("Your form has been submitted "+status.status);
}
function validEmail(email) {
let re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(email);
}
watch(name, (newName) => {
isValid.value = newName&&email.value&&message.value;
});
watch(message, (newMessage) => {
isValid.value = newMessage&&email.value&&name.value;
});
watch(email, (newEmail) => {
isValid.value = validEmail(newEmail)&&name.value&&message.value;
});
</script>
<template>
<form @submit.prevent="submit">
<div>
<label for="name">Enter your name </label>
<input type="text" name="name" id="name" v-model="name"/>
</div>
<div>
<label for="email">Enter your email </label>
<input type="email" name="email" id="email" v-model="email"/>
</div>
<div>
<label for="message">Enter your message </label>
<textarea id="message" name="message" v-model="message"></textarea>
</div>
<div class="form-example">
<input type="submit" value="Send message" :disabled="!isValid" />
</div>
</form>
</template>
<style scoped>
textarea{
resize: vertical;
}
label {
text-align:left;
margin-bottom:5px;
display: block;
font-weight: bold;
font-size: 0.9rem;
}
input[type="text"], input[type="email"], textarea {
border: 1px solid #ccc;
font-size: 1rem;
padding: 6px 10px;
border-radius: 4px;
width: 100%;
margin-bottom:5px;
}
body { display: block }
input[type="submit"] {
background-color: hsla(160, 100%, 37%, 1);
color: white;
font-size: 0.8rem;
border: none;
border-radius: 4px;
padding: 8px 12px;
font-weight: 500;
}
</style>
\ No newline at end of file
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Calculator from '@/components/Calculator.vue'
describe('Calculator', () => {
it('should perform basic calculator operations', async () => {
const wrapper = mount(Calculator);
await wrapper.find('input').setValue('5+5');
await wrapper.find('button[id="equal"]').trigger('click');
expect(wrapper.find('ul#log-list li').text()).toContain('5+5 = 10');
});
it('should not allow invalid equations', async () => {
const wrapper = mount(Calculator);
await wrapper.find('input').setValue('5/0');
await wrapper.find('button[id="equal"]').trigger('click');
expect(wrapper.find('.alert').exists()).toBe(true);
});
});
\ No newline at end of file
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import {storeUser} from '@/utils/Api.js'
import MyForm from '@/components/Form.vue'
import { setActivePinia, createPinia } from 'pinia'
describe('API', () => {
it('should get response from server', async () => {
let status = await storeUser("jane doe","jane@doe.com","test");
expect(status.status).toStrictEqual(201);
})
})
describe('Form', () => {
it('should enable submit when fields are valid', async () => {
setActivePinia(createPinia());
const wrapper = mount(MyForm);
await wrapper.find('input[id="email"]').setValue("pepe@gmail.com");
await wrapper.find('textarea[id="message"]').setValue("baaaah");
await wrapper.find('input[id="name"]').setValue("meemaaw");
const button = await wrapper.find('input[type="submit"]');
expect(button.attributes('disabled')).toBeUndefined();
})
it('should disable submit when any field is invalid', async () => {
setActivePinia(createPinia());
const wrapper = mount(MyForm);
await wrapper.find('input[id="email"]').setValue("pepe@gmail.com");
await wrapper.find('textarea[id="message"]').setValue(null);
await wrapper.find('input[id="name"]').setValue(null);
const button = await wrapper.find('input[type="submit"]');
expect(button.attributes('disabled')).toBeDefined();
})
})
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedState from "pinia-plugin-persistedstate"
import App from './App.vue'
import router from './router'
const app = createApp(App)
const pinia = createPinia();
pinia.use(piniaPluginPersistedState)
app.use(pinia)
app.use(router)
app.mount('#app')
import { createRouter, createWebHistory } from 'vue-router'
import CalculatorView from '@/views/CalculatorView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/Calculator',
name: 'calculator',
component: CalculatorView
},
{
path: '/Contact-form',
name: 'contact form',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/ContactFormView.vue')
},
{
path: '/',
name: 'login',
component: () => import('../views/Login.vue')
},
]
})
export default router
import { defineStore } from 'pinia'
import { useLocalStorage } from "@vueuse/core"
export const useFormStore = defineStore("form", {
state: () => {
return{
email: useLocalStorage('email',null),
name: useLocalStorage('name',null),
}
},
actions: {
saveUserInStore(name, email) {
try{
this.name = name;
this.email = email;
} catch (err){
console.log(err)
}
}
},
});
import { defineStore } from 'pinia'
export const useTokenStore = defineStore("tokenStore", {
state: () => {
return{
jwtToken: null,
}
},
actions: {
saveToken(jwtToken) {
try{
this.jwtToken = jwtToken;
} catch (err){
console.log(err)
}
}
},
persist: {
storage: sessionStorage, // note that data in sessionStorage is cleared when the page session ends
},
});
\ No newline at end of file
import axios from 'axios'
export async function storeUser(name,email,message){
try{
return await axios.post("http://localhost:3000/forms",
JSON.stringify({ name: name, email: email, message: message }));
}
catch (e) {
console.error('Error:', e);
}
}
export async function sendExpression(expression, token){
try{
return await axios.post("http://localhost:8080/calculator",
{ expression: expression},
{ headers:
{
"Content-Type": "application/json",
"Authorization" : "Bearer " + token
}
}
);
}
catch (e) {
console.error('Error:', e);
}
}
export async function retrieveExpressions(pagenumber, token){
try{
return await axios.get("http://localhost:8080/calculator?pageNumber="+pagenumber,
{ headers: { "Authorization" : "Bearer " + token } }
);
}
catch (e) {
console.error('Error:', e);
}
}
export async function checkUser(name,password){
try{
return await axios.post("http://localhost:8080/users/verify",
{ userName: name, password: password},
{ headers: { 'Content-Type': 'application/json' } }
);
}
catch (e) {
console.error('Error:', e);
}
}
export async function createUser(name,password){
try{
return await axios.post("http://localhost:8080/users/create",
{ userName: name, password: password},
{ headers: { 'Content-Type': 'application/json' } }
);
}
catch (e) {
console.error('Error:', e);
}
}
\ No newline at end of file
<script setup>
import Calculator from '../components/Calculator.vue'
</script>
<template>
<main>
<Calculator />
<RouterLink to="/Contact-form">Contact us</RouterLink>
<RouterLink to="/">Back to login</RouterLink>
</main>
</template>
<style scoped>
Calculator{
margin: auto;
width: 50%;
border: 3px solid green;
padding: 10px;
}
</style>
<script setup>
import Form from "../components/Form.vue"
</script>
<template>
<div class="about">
<h1>This is a contact form</h1>
<Form/>
<RouterLink to="/Calulator">Back to calculator</RouterLink>
</div>
</template>
<style>
</style>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { checkUser } from "../utils/Api.js"
import { createUser } from "../utils/Api.js"
import { useTokenStore } from "../stores/TokenStore.js";
const route = useRouter()
const name = ref('')
const password = ref('')
const store = useTokenStore()
async function login(){
//todo check response from server
let response = await checkUser(name.value,password.value);
if(response.data['token']){
store.saveToken(response.data['token'])
await route.push("/Calculator")
}else {
alert("You were not able to log in")
}
}
async function signIn(){
//todo check response from server
let response = await createUser(name.value,password.value);
if(response.data['token']){
store.saveToken(response.data['token'])
console.log(store.jwtToken)
await route.push("/Calculator")
}else {
alert("You were not able to sign in")
}
}
</script>
<template>
<main>
<form @submit.prevent="">
<div>
<label for="name">Enter your name </label>
<input type="text" name="name" id="name" v-model="name"/>
</div>
<div>
<label for="password">Enter your password </label>
<input type="password" name="password" id="password" v-model="password"/>
</div>
<div class="form-example">
<input type="submit" value="Log-in" @click="login()" />
<input type="submit" value="Sign-in" @click="signIn()"/>
</div>
</form>
</main>
</template>
<style>
</style>
\ No newline at end of file
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
vueJsx(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
//server: {
// host: '0.0.0.0',
//}
})
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment