forked from olcxjas-softworks/LarpixClient
635 lines
20 KiB
JavaScript
635 lines
20 KiB
JavaScript
var prot = window.location.protocol;
|
|
|
|
async function updateProtocolAndUrl(host)
|
|
{
|
|
prot = window.location.protocol == "miarven:" ? "http:" : window.location.protocol;
|
|
if (prot == "http:") {
|
|
try {
|
|
JSON.parse(await fetchAsync(`${prot}//${host}/_larpix/serverinfo`));
|
|
}
|
|
catch (error) {
|
|
try {
|
|
JSON.parse(await fetchAsync(`https://${host}/_larpix/serverinfo`));
|
|
prot = "https:";
|
|
}
|
|
catch (error) {}
|
|
}
|
|
}
|
|
url = `${prot}//${host}/_larpix`;
|
|
}
|
|
|
|
console.log(prot);
|
|
|
|
var url = `${prot}//${window.location.hostname}/_larpix`;
|
|
var params = new URLSearchParams(window.location.search);
|
|
|
|
try {
|
|
|
|
|
|
var collapseDmsBtn = document.getElementById("collapse-dms");
|
|
var collapseGroupsBtn = document.getElementById("collapse-groups");
|
|
|
|
var addDmBtn = document.getElementById("add-dm-btn");
|
|
var addGroupBtn = document.getElementById("add-group-btn");
|
|
|
|
var sidebarHome = document.getElementById("sidebar-home");
|
|
var sidebarHomeButton = sidebarHome.children.item(1);
|
|
var sidebarHomeIndicator = sidebarHome.children.item(0);
|
|
|
|
var sidebarAdd = document.getElementById("sidebar-add");
|
|
var sidebarAddButton = sidebarAdd.children.item(1);
|
|
var sidebarAddIndicator = sidebarAdd.children.item(0);
|
|
|
|
var roomDetailsBar = document.getElementById("roomdetailsbar");
|
|
var roomContent = document.getElementsByTagName("roomcontent")[0];
|
|
var roomsBar = document.getElementById("roomsbar");
|
|
var sideBar = document.getElementsByTagName("sidebar")[0];
|
|
|
|
var roomContentMain = document.getElementsByTagName("roomcontent2")[0];
|
|
|
|
var roomContentBar = roomContent.children[0];
|
|
|
|
var roomTopBar = document.getElementsByTagName("roomtopbar")[1];
|
|
|
|
var sidebarProfile = document.getElementById("sidebar-profile");
|
|
var sidebarProfileButton = sidebarProfile.children.item(1);
|
|
var sidebarProfileIndicator = sidebarProfile.children.item(0);
|
|
|
|
var sidebarInbox = document.getElementById("sidebar-inbox");
|
|
var sidebarInboxButton = sidebarInbox.children.item(1);
|
|
var sidebarInboxIndicator = sidebarInbox.children.item(0);
|
|
|
|
|
|
|
|
} catch (e) {
|
|
}
|
|
|
|
|
|
function delay(time) {
|
|
return new Promise(resolve => setTimeout(resolve, time));
|
|
}
|
|
|
|
async function packetEncPass(pass, key, username) {
|
|
return await encryptWithNonce(pass, key, getNonce(username, key));
|
|
}
|
|
|
|
async function getNonce(username, key) {
|
|
|
|
let nonce;
|
|
let fetchRes = await (await fetch(`${url}/nextnonce?u=${username}`)).text();
|
|
|
|
try {
|
|
nonce = await decryptString(fetchRes, key);
|
|
} catch (err) {
|
|
nonce = await decryptString(fetchRes, "");
|
|
}
|
|
return nonce;
|
|
}
|
|
|
|
async function encryptWithNonce(value, key, nonce) {
|
|
return await encryptString(value, nonce + key);
|
|
}
|
|
|
|
async function calcCommunicationKeyClient(p, g, pubServer) {
|
|
const secretClientBytes = window.crypto.getRandomValues(new Uint8Array(512));
|
|
const secretClient = BigInt('0x' + Array.from(secretClientBytes).map(b => b.toString(16).padStart(2, '0')).join(''));
|
|
const pubClient = power(BigInt(g), secretClient, BigInt(p));
|
|
const sharedSecret = power(BigInt(pubServer), secretClient, BigInt(p));
|
|
|
|
let sharedSecretStr = sharedSecret.toString(16);
|
|
|
|
if (sharedSecretStr.length % 2 !== 0) {
|
|
sharedSecretStr = '0' + sharedSecretStr;
|
|
}
|
|
|
|
const sharedSecretBytes = new Uint8Array(sharedSecretStr.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
|
|
|
|
const hashBuffer = await window.crypto.subtle.digest('SHA-256', sharedSecretBytes);
|
|
const aesKey = new Uint8Array(hashBuffer);
|
|
|
|
return [pubClient.toString(), aesKey];
|
|
}
|
|
|
|
function keyDataFromServerJson(jsonFromServer) {
|
|
const data = JSON.parse(jsonFromServer);
|
|
|
|
const p = BigInt(data.p);
|
|
const g = BigInt(data.g);
|
|
const pubServer = BigInt(data.pubServer);
|
|
|
|
return [p, g, pubServer, data.idKey]
|
|
}
|
|
|
|
function base64ToUint8(base64) {
|
|
return Uint8Array.from(atob(base64), c => c.charCodeAt(0));
|
|
}
|
|
|
|
function uint8ToBase64(uint8) {
|
|
return fixBase64Padding(btoa(String.fromCharCode(...uint8)));
|
|
}
|
|
|
|
async function encrypt(plainText, keyBytes) {
|
|
const iv = window.crypto.getRandomValues(new Uint8Array(16));
|
|
const encoder = new TextEncoder();
|
|
const data = encoder.encode(plainText);
|
|
|
|
const cryptoKey = await window.crypto.subtle.importKey(
|
|
"raw", keyBytes, "AES-CBC", false, ["encrypt"]
|
|
);
|
|
|
|
const encryptedContent = await window.crypto.subtle.encrypt(
|
|
{name: "AES-CBC", iv: iv},
|
|
cryptoKey,
|
|
data
|
|
);
|
|
const result = new Uint8Array(iv.length + encryptedContent.byteLength);
|
|
result.set(iv);
|
|
result.set(new Uint8Array(encryptedContent), iv.length);
|
|
|
|
return uint8ToBase64(result);
|
|
}
|
|
|
|
async function decrypt(cipherTextBase64, keyBytes) {
|
|
const fullData = base64ToUint8(cipherTextBase64);
|
|
const iv = fullData.slice(0, 16);
|
|
const cipherData = fullData.slice(16);
|
|
|
|
const cryptoKey = await window.crypto.subtle.importKey(
|
|
"raw", keyBytes, "AES-CBC", false, ["decrypt"]
|
|
);
|
|
|
|
const decrypted = await window.crypto.subtle.decrypt(
|
|
{name: "AES-CBC", iv: iv},
|
|
cryptoKey,
|
|
cipherData
|
|
);
|
|
|
|
return new TextDecoder().decode(decrypted);
|
|
}
|
|
|
|
|
|
async function getCryptoKey(passphrase) {
|
|
const encoder = new TextEncoder();
|
|
const data = encoder.encode(passphrase);
|
|
const hash = await crypto.subtle.digest('SHA-256', data);
|
|
return await crypto.subtle.importKey('raw', hash, {name: 'AES-CBC'}, false, ['encrypt', 'decrypt']);
|
|
}
|
|
|
|
async function encryptBytes(plainBytes, passphrase) {
|
|
const key = await getCryptoKey(passphrase);
|
|
const iv = crypto.getRandomValues(new Uint8Array(16));
|
|
|
|
const encryptedContent = await crypto.subtle.encrypt(
|
|
{name: 'AES-CBC', iv: iv},
|
|
key,
|
|
plainBytes
|
|
);
|
|
|
|
const result = new Uint8Array(iv.length + encryptedContent.byteLength);
|
|
result.set(iv);
|
|
result.set(new Uint8Array(encryptedContent), iv.length);
|
|
return result;
|
|
}
|
|
|
|
async function decryptBytes(combinedBytes, passphrase) {
|
|
const key = await getCryptoKey(passphrase);
|
|
const iv = combinedBytes.slice(0, 16);
|
|
const data = combinedBytes.slice(16);
|
|
|
|
const decryptedContent = await crypto.subtle.decrypt(
|
|
{name: 'AES-CBC', iv: iv},
|
|
key,
|
|
data
|
|
);
|
|
|
|
return new Uint8Array(decryptedContent);
|
|
}
|
|
|
|
async function encryptString(plainText, passphrase) {
|
|
const encoder = new TextEncoder();
|
|
const data = encoder.encode(plainText);
|
|
const pwHash = await crypto.subtle.digest('SHA-256', encoder.encode(passphrase));
|
|
|
|
const key = await crypto.subtle.importKey(
|
|
'raw', pwHash, {name: 'AES-CBC'}, false, ['encrypt']
|
|
);
|
|
|
|
const iv = crypto.getRandomValues(new Uint8Array(16));
|
|
const encrypted = await crypto.subtle.encrypt(
|
|
{name: 'AES-CBC', iv: iv},
|
|
key,
|
|
data
|
|
);
|
|
|
|
const combined = new Uint8Array(iv.length + encrypted.byteLength);
|
|
combined.set(iv);
|
|
combined.set(new Uint8Array(encrypted), iv.length);
|
|
|
|
return fixBase64Padding(btoa(String.fromCharCode(...combined)));
|
|
}
|
|
|
|
async function decryptString(base64Text, passphrase) {
|
|
const encoder = new TextEncoder();
|
|
const combined = new Uint8Array(atob(base64Text).split("").map(c => c.charCodeAt(0)));
|
|
|
|
const pwHash = await crypto.subtle.digest('SHA-256', encoder.encode(passphrase));
|
|
const key = await crypto.subtle.importKey(
|
|
'raw', pwHash, {name: 'AES-CBC'}, false, ['decrypt']
|
|
);
|
|
|
|
const iv = combined.slice(0, 16);
|
|
const data = combined.slice(16);
|
|
|
|
const decrypted = await crypto.subtle.decrypt(
|
|
{name: 'AES-CBC', iv: iv},
|
|
key,
|
|
data
|
|
);
|
|
|
|
return new TextDecoder().decode(decrypted);
|
|
}
|
|
|
|
|
|
async function fetchPost(url, value) {
|
|
let response = await fetch(url, {
|
|
method: "POST",
|
|
credentials: "omit",
|
|
body: value,
|
|
headers: {
|
|
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(username, passwordHash))
|
|
}
|
|
});
|
|
let data = await response.text();
|
|
return data;
|
|
}
|
|
|
|
async function fetchPostEnc(url, value) {
|
|
let nonce = await getNonce(username, passwordHash);
|
|
let response = await fetch(url, {
|
|
method: "POST",
|
|
credentials: "omit",
|
|
body: await encryptWithNonce(value, passwordHash, nonce),
|
|
headers: {
|
|
"secret": await encryptWithNonce(passwordHash, passwordHash, nonce)
|
|
}
|
|
});
|
|
let data = await response.text();
|
|
return data;
|
|
}
|
|
|
|
async function fetchAsync(url) {
|
|
let response = await fetch(url, {
|
|
method: "GET",
|
|
credentials: "omit"
|
|
});
|
|
|
|
let data = await response.text();
|
|
return data;
|
|
}
|
|
|
|
async function fetchAsyncWAuth(url) {
|
|
let response = await fetch(url, {
|
|
method: "GET",
|
|
credentials: "omit",
|
|
headers: {
|
|
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(username, passwordHash))
|
|
}
|
|
});
|
|
|
|
let data = await response.text();
|
|
return data;
|
|
}
|
|
|
|
async function getServerInfo(host) {
|
|
console.log(`${prot}//${host}/_larpix/serverinfo`)
|
|
return JSON.parse(await fetchAsync(`${prot}//${host}/_larpix/serverinfo`));
|
|
}
|
|
|
|
async function Auth(username, password) {
|
|
let passwordHash = await hashSHA3_512(password);
|
|
let response = await fetch(`${url}/auth?u=${username}`, {
|
|
method: "GET",
|
|
credentials: "omit",
|
|
headers: {
|
|
"secret": await encryptWithNonce(passwordHash, passwordHash, await getNonce(username, passwordHash))
|
|
}
|
|
});
|
|
|
|
let data = await response.text();
|
|
return data;
|
|
|
|
}
|
|
|
|
|
|
async function fetchEncrypted(request, body) {
|
|
|
|
let nonce = await getNonce(username, passwordHash);
|
|
|
|
let response = await fetch(`${url}/encryptedrequest?u=${username}`, {
|
|
method: "POST",
|
|
credentials: "omit",
|
|
body: await encryptWithNonce(
|
|
JSON.stringify({
|
|
string1: request,
|
|
string2: body
|
|
})
|
|
|
|
, passwordHash, nonce),
|
|
headers: {
|
|
"secret": await encryptWithNonce(passwordHash, passwordHash, nonce)
|
|
}
|
|
});
|
|
let data = await response.text();
|
|
return decryptString(data, passwordHash);
|
|
}
|
|
|
|
function power(base, exponent, mod) {
|
|
let res = 1n;
|
|
base = base % mod;
|
|
while (exponent > 0n) {
|
|
if (exponent % 2n === 1n) res = (res * base) % mod;
|
|
base = (base * base) % mod;
|
|
exponent = exponent / 2n;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
function fixBase64Padding(base64String) {
|
|
let str = base64String.replace(/-/g, '+').replace(/_/g, '/');
|
|
const pad = str.length % 4;
|
|
|
|
if (pad) {
|
|
if (pad === 1) {
|
|
throw new Error("");
|
|
}
|
|
str += '='.repeat(4 - pad);
|
|
}
|
|
|
|
return str;
|
|
}
|
|
|
|
function showBlahNotification(blahmessage, duration = 3500) {
|
|
try {
|
|
let split = blahmessage.split(":");
|
|
showNotification(processBlah(blahmessage), split[0], duration);
|
|
} catch (e) {
|
|
showNotification(blahmessage, "info", duration);
|
|
}
|
|
}
|
|
|
|
function showNotification(message, type = 'info', duration = 3500) {
|
|
let container = document.getElementById('notification-container');
|
|
if (!container) {
|
|
container = document.createElement('div');
|
|
container.id = 'notification-container';
|
|
document.body.appendChild(container);
|
|
}
|
|
|
|
const notif = document.createElement('div');
|
|
notif.className = `notification ${type}`;
|
|
notif.textContent = message;
|
|
|
|
container.appendChild(notif);
|
|
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
notif.classList.add('show');
|
|
});
|
|
});
|
|
|
|
setTimeout(() => {
|
|
notif.classList.remove('show');
|
|
|
|
notif.addEventListener('transitionend', () => {
|
|
notif.remove();
|
|
});
|
|
}, duration);
|
|
}
|
|
|
|
|
|
function showAction(message, actionid) {
|
|
let container = document.getElementById('notification-container');
|
|
if (!container) {
|
|
container = document.createElement('div');
|
|
container.id = 'notification-container';
|
|
document.body.appendChild(container);
|
|
}
|
|
|
|
const notif = document.createElement('div');
|
|
notif.className = `notification action`;
|
|
notif.textContent = message;
|
|
notif.id = `notification-${actionid}`;
|
|
|
|
container.appendChild(notif);
|
|
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
notif.classList.add('show');
|
|
});
|
|
});
|
|
}
|
|
|
|
function clearAction(actionid) {
|
|
const notifications = document.querySelectorAll(`[id="notification-${actionid}"]`);
|
|
|
|
notifications.forEach(notif => {
|
|
notif.classList.remove('show');
|
|
|
|
notif.addEventListener('transitionend', () => {
|
|
notif.remove();
|
|
}, { once: true });
|
|
});
|
|
}
|
|
|
|
|
|
async function hashSHA3_512(input) {
|
|
const encoder = new TextEncoder();
|
|
const data = encoder.encode(input);
|
|
const hashBuffer = await crypto.subtle.digest('SHA-512', data); //-3 kiedys xddddddddddddddddd
|
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
return hashHex;
|
|
}
|
|
|
|
var blah;
|
|
|
|
async function initBlahs() {
|
|
lang = lang.toLowerCase();
|
|
let res;
|
|
|
|
let path = window.location.pathname.replace("/login/index.html", "/");
|
|
|
|
try { //try user lang first
|
|
res = await fetchAsync(`${path}blah/${lang}.json`);
|
|
} catch (e) { //fallback to en-us
|
|
res = await fetchAsync(`${path}blah/en-us.json`);
|
|
}
|
|
blah = JSON.parse(res);
|
|
}
|
|
|
|
function processBlah(blahmessage) {
|
|
try {
|
|
let split = blahmessage.split(":");
|
|
let values = [];
|
|
try {
|
|
values = split[2].split(";");
|
|
} catch (e) {
|
|
}
|
|
|
|
let message = blah[split[1]];
|
|
|
|
let valueslist = "";
|
|
for (let i = 0; i < values.length; i++) {
|
|
let value = values[i];
|
|
valueslist+=`${values[i]}, `;
|
|
processBlah(value);
|
|
|
|
message = message.replaceAll(`{${i}}`, value);
|
|
}
|
|
valueslist = valueslist.slice(0, -2);
|
|
if (message.includes('{all}')) {
|
|
|
|
message = message.replaceAll('{all}', valueslist);
|
|
}
|
|
|
|
return message;
|
|
}
|
|
catch (e)
|
|
{
|
|
return blahmessage;
|
|
}
|
|
}
|
|
|
|
function getLang() {
|
|
return (navigator.language || navigator.languages[0]);
|
|
}
|
|
|
|
var password = "";
|
|
var username = "";
|
|
var passwordHash = "";
|
|
var host = "";
|
|
var lang = getLang();
|
|
|
|
async function mainJS() {
|
|
await initBlahs();
|
|
|
|
passwordHash = await hashSHA3_512(password);
|
|
if (localStorage.getItem('lang') != null) {
|
|
lang = localStorage.getItem('lang');
|
|
}
|
|
}
|
|
username = localStorage.getItem('username');
|
|
password = localStorage.getItem('password');
|
|
host = localStorage.getItem('host');
|
|
mainJS();
|
|
|
|
collapseDmsBtn.addEventListener("click", () => {
|
|
collapseDmsBtn.classList.toggle("collapsed");
|
|
});
|
|
collapseGroupsBtn.addEventListener("click", () => {
|
|
collapseGroupsBtn.classList.toggle("collapsed");
|
|
});
|
|
|
|
addDmBtn.addEventListener("click", () => {
|
|
roomTopBar.innerHTML = "Invite to dm";
|
|
roomDetailsBar.style.display = "none";
|
|
roomContentMain.innerHTML =
|
|
`
|
|
<div style="display: flex;justify-content: center;align-items: center;height:100%;flex-direction: column;text-align: center;">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="3rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M120-160v-600q0-33 23.5-56.5T200-840h480q33 0 56.5 23.5T760-760v203q-10-2-20-2.5t-20-.5q-10 0-20 .5t-20 2.5v-203H200v400h283q-2 10-2.5 20t-.5 20q0 10 .5 20t2.5 20H240L120-160Zm160-440h320v-80H280v80Zm0 160h200v-80H280v80Zm400 280v-120H560v-80h120v-120h80v120h120v80H760v120h-80ZM200-360v-400 400Z"/></svg>
|
|
<herotitle>Add Chat</herotitle>
|
|
<p>Add a private, encrypted chat by entering a username</p>
|
|
<br>
|
|
<div class="input-group">
|
|
<label for="addchat-username">Username</label>
|
|
<input type="text" id="addchat-username" placeholder="username:serverhost" class="forminput">
|
|
</div>
|
|
<div class="input-group">
|
|
<button class="submit-button" onclick="addDm()">
|
|
Add chat
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
addGroupBtn.addEventListener("click", () => {
|
|
roomTopBar.innerHTML = "Create group";
|
|
roomDetailsBar.style.display = "none";
|
|
roomContentMain.innerHTML =
|
|
`
|
|
<div style="display: flex;justify-content: center;align-items: center;height:100%;flex-direction: column;text-align: center;">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="3rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M500-482q29-32 44.5-73t15.5-85q0-44-15.5-85T500-798q60 8 100 53t40 105q0 60-40 105t-100 53Zm220 322v-120q0-36-16-68.5T662-406q51 18 94.5 46.5T800-280v120h-80Zm80-280v-80h-80v-80h80v-80h80v80h80v80h-80v80h-80Zm-593-87q-47-47-47-113t47-113q47-47 113-47t113 47q47 47 47 113t-47 113q-47 47-113 47t-113-47ZM0-160v-112q0-34 17.5-62.5T64-378q62-31 126-46.5T320-440q66 0 130 15.5T576-378q29 15 46.5 43.5T640-272v112H0Zm320-400q33 0 56.5-23.5T400-640q0-33-23.5-56.5T320-720q-33 0-56.5 23.5T240-640q0 33 23.5 56.5T320-560ZM80-240h480v-32q0-11-5.5-20T540-306q-54-27-109-40.5T320-360q-56 0-111 13.5T100-306q-9 5-14.5 14T80-272v32Zm240-400Zm0 400Z"/></svg>
|
|
<herotitle>Create Group</herotitle>
|
|
<p>Create a private, encrypted group</p>
|
|
<br>
|
|
<div class="input-group">
|
|
<label for="creategroup-name">Name</label>
|
|
<input type="text" id="creategroup-name" placeholder="My group" class="forminput">
|
|
</div>
|
|
<div class="input-group">
|
|
<button class="submit-button" onclick="createGroup()">
|
|
Create group
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
|
|
sidebarHomeButton.addEventListener("mouseenter", () => {
|
|
sidebarHomeIndicator.classList.add("hover");
|
|
});
|
|
sidebarHomeButton.addEventListener("mouseleave", () => {
|
|
sidebarHomeIndicator.classList.remove("hover");
|
|
});
|
|
|
|
sidebarAddButton.addEventListener("mouseenter", () => {
|
|
sidebarAddIndicator.classList.add("hover");
|
|
});
|
|
sidebarAddButton.addEventListener("mouseleave", () => {
|
|
sidebarAddIndicator.classList.remove("hover");
|
|
});
|
|
|
|
sidebarProfileButton.addEventListener("mouseenter", () => {
|
|
sidebarProfileIndicator.classList.add("hover");
|
|
});
|
|
sidebarProfileButton.addEventListener("mouseleave", () => {
|
|
sidebarProfileIndicator.classList.remove("hover");
|
|
});
|
|
|
|
sidebarInboxButton.addEventListener("mouseenter", () => {
|
|
sidebarInboxIndicator.classList.add("hover");
|
|
});
|
|
sidebarInboxButton.addEventListener("mouseleave", () => {
|
|
sidebarInboxIndicator.classList.remove("hover");
|
|
});
|
|
|
|
|
|
function gotoSideProfilePopup() {
|
|
|
|
}
|
|
function gotoCreateSpace() {
|
|
roomTopBar.innerHTML = "Create space";
|
|
roomDetailsBar.style.display = "none";
|
|
roomContentMain.innerHTML =
|
|
`
|
|
<div style="display: flex;justify-content: center;align-items: center;height:100%;flex-direction: column;text-align: center;">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="3rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M80-120v-720h400v160h400v320h-80v-240H480v80h80v80h-80v80h80v80h-80v80h160v80H80Zm80-80h80v-80h-80v80Zm0-160h80v-80h-80v80Zm0-160h80v-80h-80v80Zm0-160h80v-80h-80v80Zm160 480h80v-80h-80v80Zm0-160h80v-80h-80v80Zm0-160h80v-80h-80v80Zm0-160h80v-80h-80v80ZM800-40v-80h-80v-80h80v-80h80v80h80v80h-80v80h-80ZM640-440v-80h80v80h-80Zm0 160v-80h80v80h-80Z"/></svg>
|
|
<herotitle>Create Space</herotitle>
|
|
<p>Create a space for your community</p>
|
|
<br>
|
|
<div class="input-group">
|
|
<label for="addgroup-name">Name</label>
|
|
<input type="text" id="createspace-name" placeholder="My space" class="forminput">
|
|
</div>
|
|
<div class="input-group">
|
|
<button class="submit-button" onclick="createGroup()">
|
|
Create space
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
function gotoHome() {
|
|
|
|
}
|