Browse Source

App.vue: Formatting

pull/926/head
Daniil Isakov 3 years ago
parent
commit
8f0bf2727c
  1. 266
      webui/src/App.vue

266
webui/src/App.vue

@ -1,12 +1,12 @@
<script> <script>
import { ref, reactive } from "vue"; import { ref, reactive } from 'vue';
class API { class API {
async call({ method, path, body }) { async call({ method, path, body }) {
const res = await fetch(`./api${path}`, { const res = await fetch(`./api${path}`, {
method, method,
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
body: body ? JSON.stringify(body) : undefined, body: body ? JSON.stringify(body) : undefined,
}); });
@ -26,79 +26,82 @@ class API {
async getRelease() { async getRelease() {
return this.call({ return this.call({
method: "get", method: 'get',
path: "/release", path: '/release',
}); });
} }
async getSession() { async getSession() {
return this.call({ return this.call({
method: "get", method: 'get',
path: "/session", path: '/session',
}); });
} }
async createSession({ password }) { async createSession({ password }) {
return this.call({ return this.call({
method: "post", method: 'post',
path: "/session", path: '/session',
body: { password }, body: { password },
}); });
} }
async deleteSession() { async deleteSession() {
return this.call({ return this.call({
method: "delete", method: 'delete',
path: "/session", path: '/session',
}); });
} }
async getClients() { async getClients() {
return this.call({ return this.call({
method: "get", method: 'get',
path: "/wireguard/client", path: '/wireguard/client',
}).then((clients) => }).then((clients) =>
clients.map((client) => ({ clients.map((client) => ({
...client, ...client,
createdAt: new Date(client.createdAt), createdAt: new Date(client.createdAt),
updatedAt: new Date(client.updatedAt), updatedAt: new Date(client.updatedAt),
latestHandshakeAt: client.latestHandshakeAt !== null ? new Date(client.latestHandshakeAt) : null, latestHandshakeAt:
client.latestHandshakeAt !== null
? new Date(client.latestHandshakeAt)
: null,
})) }))
); );
} }
async createClient({ name }) { async createClient({ name }) {
return this.call({ return this.call({
method: "post", method: 'post',
path: "/wireguard/client", path: '/wireguard/client',
body: { name }, body: { name },
}); });
} }
async deleteClient({ clientId }) { async deleteClient({ clientId }) {
return this.call({ return this.call({
method: "delete", method: 'delete',
path: `/wireguard/client/${clientId}`, path: `/wireguard/client/${clientId}`,
}); });
} }
async enableClient({ clientId }) { async enableClient({ clientId }) {
return this.call({ return this.call({
method: "post", method: 'post',
path: `/wireguard/client/${clientId}/enable`, path: `/wireguard/client/${clientId}/enable`,
}); });
} }
async disableClient({ clientId }) { async disableClient({ clientId }) {
return this.call({ return this.call({
method: "post", method: 'post',
path: `/wireguard/client/${clientId}/disable`, path: `/wireguard/client/${clientId}/disable`,
}); });
} }
async updateClientName({ clientId, name }) { async updateClientName({ clientId, name }) {
return this.call({ return this.call({
method: "put", method: 'put',
path: `/wireguard/client/${clientId}/name/`, path: `/wireguard/client/${clientId}/name/`,
body: { name }, body: { name },
}); });
@ -106,7 +109,7 @@ class API {
async updateClientAddress({ clientId, address }) { async updateClientAddress({ clientId, address }) {
return this.call({ return this.call({
method: "put", method: 'put',
path: `/wireguard/client/${clientId}/address/`, path: `/wireguard/client/${clientId}/address/`,
body: { address }, body: { address },
}); });
@ -124,7 +127,7 @@ export default {
const clientsPersist = reactive({}); const clientsPersist = reactive({});
const clientDelete = ref(null); const clientDelete = ref(null);
const clientCreate = ref(null); const clientCreate = ref(null);
const clientCreateName = ref(""); const clientCreateName = ref('');
const clientEditName = ref(null); const clientEditName = ref(null);
const clientEditNameId = ref(null); const clientEditNameId = ref(null);
const clientEditAddress = ref(null); const clientEditAddress = ref(null);
@ -138,8 +141,8 @@ export default {
const chartOptions = reactive({ const chartOptions = reactive({
chart: { chart: {
background: "transparent", background: 'transparent',
type: "bar", type: 'bar',
stacked: false, stacked: false,
toolbar: { toolbar: {
show: false, show: false,
@ -149,8 +152,8 @@ export default {
}, },
}, },
colors: [ colors: [
"#DDDDDD", // rx '#DDDDDD', // rx
"#EEEEEE", // tx '#EEEEEE', // tx
], ],
dataLabels: { dataLabels: {
enabled: false, enabled: false,
@ -206,11 +209,11 @@ export default {
const dateTime = (value) => { const dateTime = (value) => {
return new Intl.DateTimeFormat(undefined, { return new Intl.DateTimeFormat(undefined, {
year: "numeric", year: 'numeric',
month: "short", month: 'short',
day: "numeric", day: 'numeric',
hour: "numeric", hour: 'numeric',
minute: "numeric", minute: 'numeric',
}).format(value); }).format(value);
}; };
@ -219,9 +222,11 @@ export default {
const clientsData = await api.getClients(); const clientsData = await api.getClients();
clients.value = clientsData.map((client) => { clients.value = clientsData.map((client) => {
if (client.name.includes("@") && client.name.includes(".")) { if (client.name.includes('@') && client.name.includes('.')) {
// FIXME: add "sha512" // FIXME: add "sha512"
client.avatar = `https://www.gravatar.com/avatar/${sha512(client.name)}?d=blank`; client.avatar = `https://www.gravatar.com/avatar/${sha512(
client.name
)}?d=blank`;
} }
if (!clientsPersist[client.id]) { if (!clientsPersist[client.id]) {
@ -244,10 +249,14 @@ export default {
client.transferTx - clientsPersist[client.id].transferTxPrevious; client.transferTx - clientsPersist[client.id].transferTxPrevious;
clientsPersist[client.id].transferTxPrevious = client.transferTx; clientsPersist[client.id].transferTxPrevious = client.transferTx;
clientsPersist[client.id].transferRxHistory.push(clientsPersist[client.id].transferRxCurrent); clientsPersist[client.id].transferRxHistory.push(
clientsPersist[client.id].transferRxCurrent
);
clientsPersist[client.id].transferRxHistory.shift(); clientsPersist[client.id].transferRxHistory.shift();
clientsPersist[client.id].transferTxHistory.push(clientsPersist[client.id].transferTxCurrent); clientsPersist[client.id].transferTxHistory.push(
clientsPersist[client.id].transferTxCurrent
);
clientsPersist[client.id].transferTxHistory.shift(); clientsPersist[client.id].transferTxHistory.shift();
} }
@ -256,7 +265,10 @@ export default {
client.transferTxHistory = clientsPersist[client.id].transferTxHistory; client.transferTxHistory = clientsPersist[client.id].transferTxHistory;
client.transferRxHistory = clientsPersist[client.id].transferRxHistory; client.transferRxHistory = clientsPersist[client.id].transferRxHistory;
client.transferMax = Math.max(...client.transferTxHistory, ...client.transferRxHistory); client.transferMax = Math.max(
...client.transferTxHistory,
...client.transferRxHistory
);
client.hoverTx = clientsPersist[client.id].hoverTx; client.hoverTx = clientsPersist[client.id].hoverTx;
client.hoverRx = clientsPersist[client.id].hoverRx; client.hoverRx = clientsPersist[client.id].hoverRx;
@ -272,7 +284,8 @@ export default {
if (authenticating.value) return; if (authenticating.value) return;
authenticating.value = true; authenticating.value = true;
api.createSession({ api
.createSession({
password: password.value, password: password.value,
}) })
.then(async () => { .then(async () => {
@ -293,7 +306,8 @@ export default {
const logout = (e) => { const logout = (e) => {
e.preventDefault(); e.preventDefault();
api.deleteSession() api
.deleteSession()
.then(() => { .then(() => {
authenticated.value = false; authenticated.value = false;
clients.value = null; clients.value = null;
@ -307,48 +321,54 @@ export default {
const name = clientCreateName.value; const name = clientCreateName.value;
if (!name) return; if (!name) return;
api.createClient({ name }) api
.createClient({ name })
.catch((err) => alert(err.message || err.toString())) .catch((err) => alert(err.message || err.toString()))
.finally(() => refresh().catch(console.error)); .finally(() => refresh().catch(console.error));
}; };
const deleteClient = (client) => { const deleteClient = (client) => {
api.deleteClient({ clientId: client.id }) api
.deleteClient({ clientId: client.id })
.catch((err) => alert(err.message || err.toString())) .catch((err) => alert(err.message || err.toString()))
.finally(() => refresh().catch(console.error)); .finally(() => refresh().catch(console.error));
}; };
const enableClient = (client) => { const enableClient = (client) => {
api.enableClient({ clientId: client.id }) api
.enableClient({ clientId: client.id })
.catch((err) => alert(err.message || err.toString())) .catch((err) => alert(err.message || err.toString()))
.finally(() => refresh().catch(console.error)); .finally(() => refresh().catch(console.error));
}; };
const disableClient = (client) => { const disableClient = (client) => {
api.disableClient({ clientId: client.id }) api
.disableClient({ clientId: client.id })
.catch((err) => alert(err.message || err.toString())) .catch((err) => alert(err.message || err.toString()))
.finally(() => refresh().catch(console.error)); .finally(() => refresh().catch(console.error));
}; };
const updateClientName = (client, name) => { const updateClientName = (client, name) => {
api.updateClientName({ clientId: client.id, name }) api
.updateClientName({ clientId: client.id, name })
.catch((err) => alert(err.message || err.toString())) .catch((err) => alert(err.message || err.toString()))
.finally(() => refresh().catch(console.error)); .finally(() => refresh().catch(console.error));
}; };
const updateClientAddress = (client, address) => { const updateClientAddress = (client, address) => {
api.updateClientAddress({ clientId: client.id, address }) api
.updateClientAddress({ clientId: client.id, address })
.catch((err) => alert(err.message || err.toString())) .catch((err) => alert(err.message || err.toString()))
.finally(() => refresh().catch(console.error)); .finally(() => refresh().catch(console.error));
}; };
const toggleTheme = () => { const toggleTheme = () => {
if (isDark.value) { if (isDark.value) {
localStorage.theme = "light"; localStorage.theme = 'light';
document.documentElement.classList.remove("dark"); document.documentElement.classList.remove('dark');
} else { } else {
localStorage.theme = "dark"; localStorage.theme = 'dark';
document.documentElement.classList.add("dark"); document.documentElement.classList.add('dark');
} }
isDark.value = !isDark.value; isDark.value = !isDark.value;
}; };
@ -394,7 +414,7 @@ export default {
}, },
mounted() { mounted() {
this.isDark = false; this.isDark = false;
if (localStorage.theme === "dark") { if (localStorage.theme === 'dark') {
this.isDark = true; this.isDark = true;
} }
@ -423,13 +443,17 @@ export default {
Promise.resolve() Promise.resolve()
.then(async () => { .then(async () => {
const currentReleaseData = await this.api.getRelease(); const currentReleaseData = await this.api.getRelease();
const latestReleaseData = await fetch("https://wg-easy.github.io/wg-easy/changelog.json") const latestReleaseData = await fetch(
'https://wg-easy.github.io/wg-easy/changelog.json'
)
.then((res) => res.json()) .then((res) => res.json())
.then((releases) => { .then((releases) => {
const releasesArray = Object.entries(releases).map(([version, changelog]) => ({ const releasesArray = Object.entries(releases).map(
([version, changelog]) => ({
version: parseInt(version, 10), version: parseInt(version, 10),
changelog, changelog,
})); })
);
releasesArray.sort((a, b) => { releasesArray.sort((a, b) => {
return b.version - a.version; return b.version - a.version;
}); });
@ -484,7 +508,11 @@ export default {
</svg> </svg>
</span> </span>
<h1 class="text-4xl dark:text-neutral-200 font-medium mt-2 mb-2"> <h1 class="text-4xl dark:text-neutral-200 font-medium mt-2 mb-2">
<img src="./assets/img/logo.png" width="32" class="inline align-middle dark:bg" /> <img
src="./assets/img/logo.png"
width="32"
class="inline align-middle dark:bg"
/>
<span class="align-middle">WireGuard</span> <span class="align-middle">WireGuard</span>
</h1> </h1>
<h2 class="text-sm text-gray-400 dark:text-neutral-400 mb-10"></h2> <h2 class="text-sm text-gray-400 dark:text-neutral-400 mb-10"></h2>
@ -510,7 +538,9 @@ export default {
</div> </div>
</div> </div>
<div class="shadow-md rounded-lg bg-white dark:bg-neutral-700 overflow-hidden"> <div
class="shadow-md rounded-lg bg-white dark:bg-neutral-700 overflow-hidden"
>
<div <div
class="flex flex-row flex-auto items-center p-3 px-5 border-b-2 border-gray-100 dark:border-neutral-600" class="flex flex-row flex-auto items-center p-3 px-5 border-b-2 border-gray-100 dark:border-neutral-600"
> >
@ -575,7 +605,9 @@ export default {
</apexchart> </apexchart>
</div> --> </div> -->
<div class="relative p-5 z-10 flex flex-col md:flex-row justify-between"> <div
class="relative p-5 z-10 flex flex-col md:flex-row justify-between"
>
<div class="flex items-center pb-2 md:pb-0"> <div class="flex items-center pb-2 md:pb-0">
<div class="h-10 w-10 mr-5 rounded-full bg-gray-50 relative"> <div class="h-10 w-10 mr-5 rounded-full bg-gray-50 relative">
<svg <svg
@ -599,7 +631,8 @@ export default {
<div <div
v-if=" v-if="
client.latestHandshakeAt && client.latestHandshakeAt &&
new Date() - new Date(client.latestHandshakeAt) < 1000 * 60 * 10 new Date() - new Date(client.latestHandshakeAt) <
1000 * 60 * 10
" "
> >
<div <div
@ -615,7 +648,9 @@ export default {
<!-- Name --> <!-- Name -->
<div <div
class="text-gray-700 dark:text-neutral-200 group" class="text-gray-700 dark:text-neutral-200 group"
:title="'Created on ' + dateTime(new Date(client.createdAt))" :title="
'Created on ' + dateTime(new Date(client.createdAt))
"
> >
<!-- Show --> <!-- Show -->
<input <input
@ -646,7 +681,10 @@ export default {
clientEditName = client.name; clientEditName = client.name;
clientEditNameId = client.id; clientEditNameId = client.id;
setTimeout( setTimeout(
() => $refs['client-' + client.id + '-name'][0].select(), () =>
$refs[
'client-' + client.id + '-name'
][0].select(),
1 1
); );
" "
@ -702,7 +740,10 @@ export default {
clientEditAddress = client.address; clientEditAddress = client.address;
clientEditAddressId = client.id; clientEditAddressId = client.id;
setTimeout( setTimeout(
() => $refs['client-' + client.id + '-address'][0].select(), () =>
$refs[
'client-' + client.id + '-address'
][0].select(),
1 1
); );
" "
@ -774,7 +815,10 @@ export default {
<!-- Last seen --> <!-- Last seen -->
<span <span
v-if="client.latestHandshakeAt" v-if="client.latestHandshakeAt"
:title="'Last seen on ' + dateTime(new Date(client.latestHandshakeAt))" :title="
'Last seen on ' +
dateTime(new Date(client.latestHandshakeAt))
"
> >
<!-- FIXME: add "timeago" --> <!-- FIXME: add "timeago" -->
<!-- · {{ new Date(client.latestHandshakeAt) | timeago }} --> <!-- · {{ new Date(client.latestHandshakeAt) | timeago }} -->
@ -810,7 +854,9 @@ export default {
<button <button
class="align-middle bg-gray-100 dark:bg-neutral-600 dark:text-neutral-300 hover:bg-red-800 dark:hover:bg-red-800 hover:text-white dark:hover:text-white p-2 rounded transition" class="align-middle bg-gray-100 dark:bg-neutral-600 dark:text-neutral-300 hover:bg-red-800 dark:hover:bg-red-800 hover:text-white dark:hover:text-white p-2 rounded transition"
title="Show QR Code" title="Show QR Code"
@click="qrcode = `./api/wireguard/client/${client.id}/qrcode.svg`" @click="
qrcode = `./api/wireguard/client/${client.id}/qrcode.svg`
"
> >
<svg <svg
class="w-5" class="w-5"
@ -830,7 +876,9 @@ export default {
<!-- Download Config --> <!-- Download Config -->
<a <a
:href="'./api/wireguard/client/' + client.id + '/configuration'" :href="
'./api/wireguard/client/' + client.id + '/configuration'
"
download download
class="align-middle inline-block bg-gray-100 dark:bg-neutral-600 dark:text-neutral-300 hover:bg-red-800 dark:hover:bg-red-800 hover:text-white dark:hover:text-white p-2 rounded transition" class="align-middle inline-block bg-gray-100 dark:bg-neutral-600 dark:text-neutral-300 hover:bg-red-800 dark:hover:bg-red-800 hover:text-white dark:hover:text-white p-2 rounded transition"
title="Download Configuration" title="Download Configuration"
@ -875,7 +923,9 @@ export default {
</div> </div>
</div> </div>
<div v-if="clients && clients.length === 0"> <div v-if="clients && clients.length === 0">
<p class="text-center m-10 text-gray-400 dark:text-neutral-400 text-sm"> <p
class="text-center m-10 text-gray-400 dark:text-neutral-400 text-sm"
>
There are no clients yet.<br /><br /> There are no clients yet.<br /><br />
<button <button
@click=" @click="
@ -903,7 +953,10 @@ export default {
</button> </button>
</p> </p>
</div> </div>
<div v-if="clients === null" class="text-gray-200 dark:text-red-300 p-5"> <div
v-if="clients === null"
class="text-gray-200 dark:text-red-300 p-5"
>
<svg <svg
class="w-5 animate-spin mx-auto" class="w-5 animate-spin mx-auto"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -974,11 +1027,15 @@ export default {
To: "opacity-0" To: "opacity-0"
--> -->
<div class="fixed inset-0 transition-opacity" aria-hidden="true"> <div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div class="absolute inset-0 bg-gray-500 dark:bg-black opacity-75 dark:opacity-50"></div> <div
class="absolute inset-0 bg-gray-500 dark:bg-black opacity-75 dark:opacity-50"
></div>
</div> </div>
<!-- This element is to trick the browser into centering the modal contents. --> <!-- This element is to trick the browser into centering the modal contents. -->
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true" <span
class="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true"
>&#8203;</span >&#8203;</span
> >
<!-- <!--
@ -997,7 +1054,9 @@ export default {
aria-modal="true" aria-modal="true"
aria-labelledby="modal-headline" aria-labelledby="modal-headline"
> >
<div class="bg-white dark:bg-neutral-700 px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> <div
class="bg-white dark:bg-neutral-700 px-4 pt-5 pb-4 sm:p-6 sm:pb-4"
>
<div class="sm:flex sm:items-start"> <div class="sm:flex sm:items-start">
<div <div
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-800 sm:mx-0 sm:h-10 sm:w-10" class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-800 sm:mx-0 sm:h-10 sm:w-10"
@ -1018,7 +1077,9 @@ export default {
/> />
</svg> </svg>
</div> </div>
<div class="flex-grow mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"> <div
class="flex-grow mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"
>
<h3 <h3
class="text-lg leading-6 font-medium text-gray-900 dark:text-neutral-200" class="text-lg leading-6 font-medium text-gray-900 dark:text-neutral-200"
id="modal-headline" id="modal-headline"
@ -1038,7 +1099,9 @@ export default {
</div> </div>
</div> </div>
</div> </div>
<div class="bg-gray-50 dark:bg-neutral-700 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse"> <div
class="bg-gray-50 dark:bg-neutral-700 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse"
>
<button <button
v-if="clientCreateName.length" v-if="clientCreateName.length"
type="button" type="button"
@ -1085,11 +1148,15 @@ export default {
To: "opacity-0" To: "opacity-0"
--> -->
<div class="fixed inset-0 transition-opacity" aria-hidden="true"> <div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div class="absolute inset-0 bg-gray-500 dark:bg-black opacity-75 dark:opacity-50"></div> <div
class="absolute inset-0 bg-gray-500 dark:bg-black opacity-75 dark:opacity-50"
></div>
</div> </div>
<!-- This element is to trick the browser into centering the modal contents. --> <!-- This element is to trick the browser into centering the modal contents. -->
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true" <span
class="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true"
>&#8203;</span >&#8203;</span
> >
<!-- <!--
@ -1108,7 +1175,9 @@ export default {
aria-modal="true" aria-modal="true"
aria-labelledby="modal-headline" aria-labelledby="modal-headline"
> >
<div class="bg-white dark:bg-neutral-700 px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> <div
class="bg-white dark:bg-neutral-700 px-4 pt-5 pb-4 sm:p-6 sm:pb-4"
>
<div class="sm:flex sm:items-start"> <div class="sm:flex sm:items-start">
<div <div
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10" class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10"
@ -1139,14 +1208,17 @@ export default {
</h3> </h3>
<div class="mt-2"> <div class="mt-2">
<p class="text-sm text-gray-500 dark:text-neutral-300"> <p class="text-sm text-gray-500 dark:text-neutral-300">
Are you sure you want to delete <strong>{{ clientDelete.name }}</strong Are you sure you want to delete
<strong>{{ clientDelete.name }}</strong
>? This action cannot be undone. >? This action cannot be undone.
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-gray-50 dark:bg-neutral-600 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse"> <div
class="bg-gray-50 dark:bg-neutral-600 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse"
>
<button <button
type="button" type="button"
@click=" @click="
@ -1171,7 +1243,11 @@ export default {
</div> </div>
<div v-if="authenticated === false"> <div v-if="authenticated === false">
<h1 class="text-4xl font-medium my-16 text-gray-700 dark:text-neutral-200 text-center">WireGuard</h1> <h1
class="text-4xl font-medium my-16 text-gray-700 dark:text-neutral-200 text-center"
>
WireGuard
</h1>
<form <form
@submit="login" @submit="login"
@ -1243,14 +1319,24 @@ export default {
</form> </form>
</div> </div>
<div v-if="authenticated === null" class="text-gray-300 dark:text-red-300 pt-24 pb-12"> <div
v-if="authenticated === null"
class="text-gray-300 dark:text-red-300 pt-24 pb-12"
>
<svg <svg
class="w-5 animate-spin mx-auto" class="w-5 animate-spin mx-auto"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="currentColor" fill="currentColor"
> >
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path <path
class="opacity-75" class="opacity-75"
fill="currentColor" fill="currentColor"
@ -1260,11 +1346,31 @@ export default {
</div> </div>
</div> </div>
<p v-cloak class="text-center m-10 text-gray-300 dark:text-neutral-600 text-xs"> <p
v-cloak
class="text-center m-10 text-gray-300 dark:text-neutral-600 text-xs"
>
Made by Made by
<a target="_blank" class="hover:underline" href="https://emilenijssen.nl/?ref=wg-easy">Emile Nijssen</a> · <a
<a class="hover:underline" href="https://github.com/sponsors/WeeJeWel" target="_blank">Donate</a> · target="_blank"
<a class="hover:underline" href="https://github.com/wg-easy/wg-easy" target="_blank">GitHub</a> class="hover:underline"
href="https://emilenijssen.nl/?ref=wg-easy"
>Emile Nijssen</a
>
·
<a
class="hover:underline"
href="https://github.com/sponsors/WeeJeWel"
target="_blank"
>Donate</a
>
·
<a
class="hover:underline"
href="https://github.com/wg-easy/wg-easy"
target="_blank"
>GitHub</a
>
</p> </p>
</main> </main>
</template> </template>

Loading…
Cancel
Save