A heckin ton. Mostly hackish

This commit is contained in:
Gnarwhal 2021-02-16 14:11:12 -05:00
parent 052052d76b
commit b229ff9a15
Signed by: Gnarwhal
GPG key ID: 0989A73D8C421174
70 changed files with 2226 additions and 881 deletions

View file

@ -0,0 +1,8 @@
window.addEventListener("load", async (loadEvent) => {
await loadCommon();
await commonTemplates();
await template.expand();
connectNavbar();
});

View file

@ -0,0 +1,97 @@
let root = null;
const loadRoot = () => {
const rootElement = document.documentElement;
root = {};
root.getProperty = (name) => window.getComputedStyle(document.documentElement).getPropertyValue(name);
root.setProperty = (name, value) => {
rootElement.style.setProperty(name, value);
}
};
let session = null;
const loadSession = async () => {
window.addEventListener('beforeunload', (beforeUnloadEvent) => {
if (session) {
window.sessionStorage.setItem('session', JSON.stringify(session));
} else {
window.sessionStorage.removeItem('session');
}
});
session = JSON.parse(window.sessionStorage.getItem('session'));
if (session) {
root.setProperty('--accent-hue', session.hue);
await fetch(`/api/auth/refresh`, {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ key: session.key })
})
.then(async response => ({ status: response.status, data: await response.json() }))
.then(response => {
if (response.status !== 200 && window.location.pathname != "/login") {
delete session.key;
window.location.href = "/login";
}
});
}
};
const loadCommon = async () => {
loadRoot();
await loadSession();
}
const commonTemplates = async () => {
template.apply("navbar").values([
{ section: "left" },
{ section: "right" }
]);
template.apply("navbar-section-left").values([
{ item: "project", title: "Project" },
{ item: "about", title: "About" }
]);
if (session) {
template.apply("navbar-section-right").values([
{ item: "profile", title: "Profile" },
{ item: "logout", title: "Logout" }
]);
} else {
template.apply("navbar-section-right").values([
{ item: "login", title: "Login" }
]);
}
};
const connectNavbar = () => {
const navItems = document.querySelectorAll(".navbar-item");
for (const item of navItems) {
if (item.dataset.pageName === "logout") {
item.addEventListener("click", (clickEvent) => {
fetch(`/api/auth/logout`, {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ key: session.key })
})
.then(response => {
session = undefined;
window.location.href = "/login";
});
});
} else if (item.dataset.pageName === "profile") {
item.addEventListener("click", (clickEvent) => window.location.href = `/profile/${session.id}`);
} else if (item.dataset.pageName === "project") {
item.addEventListener("click", (clickEvent) => window.location.href = `/`);
} else {
item.addEventListener("click", (clickEvent) => window.location.href = `/${item.dataset.pageName}`);
}
}
};

View file

@ -0,0 +1,24 @@
const expandTemplates = async () => {
await commonTemplates();
}
const loadFilters = () => {
const filtersButton = document.querySelector("#filter-dropdown-stack");
const filters = document.querySelector("#list-page-filters-flex");
filtersButton.addEventListener("click", (clickEvent) => {
filtersButton.classList.toggle("active");
filters.classList.toggle("active");
});
}
window.addEventListener("load", async (loadEvent) => {
loadRoot();
loadSession();
await expandTemplates();
await template.expand();
connectNavbar();
loadFilters();
});

View file

@ -0,0 +1,186 @@
window.addEventListener("load", async (loadEvent) => {
await loadCommon();
if (session && session.key) {
window.location.href = '/';
}
const fields = {
email: document.querySelector("#email" ),
username: document.querySelector("#username"),
password: document.querySelector("#password"),
confirm: document.querySelector("#confirm" )
};
const createUser = document.querySelector("#create-user-button");
const login = document.querySelector("#login-button");
const guest = document.querySelector("#guest-login-button");
const header = document.querySelector("#login-header-text");
const error = document.querySelector("#error-message");
if (session) {
error.style.display = "block";
error.textContent = "You have been signed out due to inactivity";
}
const raiseError = (errorFields, message) => {
for (const key in fields) {
if (errorFields.includes(key)) {
fields[key].classList.add("error");
} else {
fields[key].classList.remove("error");
}
}
error.style.display = "block";
error.textContent = message;
}
let frozen = false;
const freeze = () => {
frozen = true;
createUser.classList.add("disabled");
login.classList.add("disabled");
guest.classList.add("disabled");
};
const unfreeze = () => {
frozen = false;
createUser.classList.remove("disabled");
login.classList.remove("disabled");
guest.classList.remove("disabled");
};
const switchToCreateAction = (clickEvent) => {
if (!frozen) {
fields.username.style.display = "block";
fields.confirm.style.display = "block";
header.textContent = "Create Account";
createUser.removeEventListener("click", switchToCreateAction);
createUser.addEventListener("click", createUserAction);
login.removeEventListener("click", loginAction);
login.addEventListener("click", switchToLoginAction);
activeAction = createUserAction;
}
};
const createUserAction = (clickEvent) => {
if (!frozen) {
if (fields.email.value === '') {
raiseError([ "email" ], "Email cannot be empty");
} else if (fields.username.value === '') {
raiseError([ "username" ], "Username cannot be empty");
} else if (fields.password.value !== fields.confirm.value) {
raiseError([ "password", "confirm" ], "Password fields did not match");
} else if (fields.password.value === '') {
raiseError([ "password", "confirm" ], "Password cannot be empty");
} else {
freeze();
fetch(`/api/auth/create_user`, {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email: fields.email.value, username: fields.username.value, password: fields.password.value })
})
.then(async response => ({ status: response.status, data: await response.json() }))
.then(response => {
const data = response.data;
if (response.status === 201) {
session = data;
window.location.href = "/";
} else if (response.status === 500) {
raiseError([], "Internal server error :(");
} else {
if (data.code === 1) {
raiseError([ "email" ], "A user with that email is already registered");
fields.email.value = '';
} else if (data.code === 2) {
raiseError([ "email" ], "Invalid email address");
fields.email.value = '';
} else {
raiseError([ "email" ], "Server is bad :p");
fields.email.value = '';
}
}
})
.catch(error => {
console.error(error);
raiseError([], "Server error :(");
}).then(unfreeze);
}
}
};
createUser.addEventListener("click", switchToCreateAction);
const switchToLoginAction = (clickEvent) => {
if (!frozen) {
fields.username.style.display = "none";
fields.confirm.style.display = "none";
header.textContent = "Login";
createUser.removeEventListener("click", createUserAction);
createUser.addEventListener("click", switchToCreateAction);
login.removeEventListener("click", switchToLoginAction);
login.addEventListener("click", loginAction);
activeAction = loginAction;
}
};
const loginAction = (clickEvent) => {
if (!frozen) {
if (fields.email.value === '') {
raiseError([ "email" ], "Email cannot be empty");
} else if (fields.password.value === '') {
raiseError([ "password" ], "Password cannot be empty");
} else {
freeze();
fetch(`/api/auth/login`, {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email: fields.email.value, password: fields.password.value })
})
.then(async response => ({ status: response.status, data: await response.json() }))
.then(response => {
const data = response.data;
if (response.status === 200) {
session = data;
window.location.href = "/";
} else if (response.status === 500) {
raiseError([], "Internal server error :(");
} else {
raiseError([ "email", "password" ], "Email or password is incorrect");
fields.password.value = '';
}
})
.catch(error => {
console.error(error);
raiseError([], "Unknown error :(");
}).then(unfreeze);
}
}
};
login.addEventListener("click", loginAction);
guest.addEventListener("click", (clickEvent) => {
if (!frozen) {
window.location.href = '/';
}
});
let activeAction = loginAction;
for (const field in fields) {
fields[field].addEventListener("keydown", (keyEvent) => {
if (keyEvent.key === "Enter") {
activeAction();
}
})
}
});

View file

@ -0,0 +1,278 @@
let profileId = window.location.pathname.split('/').pop();
let isReturn = false;
let profileData = null;
const loadProfile = () => {
{
const lists = document.querySelectorAll(".profile-list");
for (const list of lists) {
if (list.querySelectorAll(".profile-entry").length === 0) {
list.parentElement.removeChild(list);
}
}
}
{
const validImageFile = (type) => {
return type === "image/apng"
|| type === "image/avif"
|| type === "image/gif"
|| type === "image/jpeg"
|| type === "image/png"
|| type === "image/svg+xml"
|| type === "image/webp"
}
const editProfileButton = document.querySelector("#info-edit-stack");
const saveProfileButton = document.querySelector("#info-save-stack");
const usernameText = document.querySelector("#profile-info-username-text");
const usernameField = document.querySelector("#profile-info-username-field");
const finalizeName = () => {
if (usernameField.value !== '') {
fetch(`/api/user/${profileId}/username`, {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json'
},
body: `{ "sessionKey": "${session.key}", "username": "${usernameField.value}" }`
}).then(response => {
if (response.status === 201) {
usernameText.textContent = usernameField.value.substring(0, 32);
}
});
}
};
usernameField.addEventListener("input", (inputEvent) => {
if (usernameField.value.length > 32) {
usernameField.value = usernameField.value.substring(0, 32);
}
});
const pfp = document.querySelector("#profile-info-pfp-img");
const pfpStack = document.querySelector("#profile-info-pfp");
const upload = document.querySelector("#profile-info-pfp-upload");
const uploadHover = document.querySelector("#profile-info-pfp-upload-hover");
const uploadInvalid = document.querySelector("#profile-info-pfp-upload-invalid");
const togglePlatformEdit = (clickEvent) => {
editProfileButton.classList.toggle("active");
saveProfileButton.classList.toggle("active");
usernameText.classList.toggle("active");
usernameField.classList.toggle("active");
upload.classList.toggle("active");
uploadHover.classList.toggle("active");
uploadInvalid.classList.toggle("active");
pfpStack.classList.remove("hover");
pfpStack.classList.remove("invalid");
};
editProfileButton.addEventListener("click", togglePlatformEdit);
editProfileButton.addEventListener("click", () => {
usernameField.value = usernameText.textContent;
});
saveProfileButton.addEventListener("click", togglePlatformEdit);
saveProfileButton.addEventListener("click", finalizeName);
pfpStack.addEventListener("drop", (dropEvent) => {
if (upload.classList.contains("active")) {
dropEvent.preventDefault();
pfpStack.classList.remove("hover");
pfpStack.classList.remove("invalid");
if (dropEvent.dataTransfer.files) {
const file = dropEvent.dataTransfer.files[0];
if (validImageFile(file.type)) {
const data = new FormData();
data.append('session', new Blob([`{ "key": "${session.key}" }`], { type: `application/json` }));
data.append('file', file);
fetch(`/api/user/${profileId}/image`, {
method: 'POST',
mode: 'cors',
body: data
}).then(response => {
if (upload.classList.contains("active")) {
if (response.status === 201) {
pfp.src = `/api/user/${profileId}/image?time=${Date.now()}`;
} else {
pfpStack.classList.add("invalid");
}
}
});
return;
}
}
pfpStack.classList.add("invalid");
}
});
pfpStack.addEventListener("dragover", (dragEvent) => {
if (upload.classList.contains("active")) {
dragEvent.preventDefault();
}
});
pfpStack.addEventListener("dragenter", (dragEvent) => {
if (upload.classList.contains("active")) {
pfpStack.classList.remove("hover");
pfpStack.classList.remove("invalid");
if (dragEvent.dataTransfer.types.includes("application/x-moz-file")) {
pfpStack.classList.add("hover");
} else {
pfpStack.classList.add("invalid");
}
}
});
pfpStack.addEventListener("dragleave", (dragEvent) => {
if (upload.classList.contains("active")) {
pfpStack.classList.remove("hover");
pfpStack.classList.remove("invalid");
}
});
}
{
const editPlatformsButton = document.querySelector("#platform-edit-stack");
const savePlatformsButton = document.querySelector("#platform-save-stack");
const platforms = document.querySelectorAll("#profile-platforms .profile-entry");
const togglePlatformEdit = (clickEvent) => {
editPlatformsButton.classList.toggle("active");
savePlatformsButton.classList.toggle("active");
for (const platform of platforms) {
platform.classList.toggle("editing");
}
};
editPlatformsButton.addEventListener("click", togglePlatformEdit);
savePlatformsButton.addEventListener("click", togglePlatformEdit);
const steamButtons = [
document.querySelector("#add-steam"),
document.querySelector("#platform-0"),
];
steamButtons[0].addEventListener("click", (clickEvent) => {
window.location.href = "/auth/steam";
});
steamButtons[1].addEventListener("click", (clickEvent) => {
fetch(`/api/user/${profileId}/platforms/remove`, {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ sessionKey: session.key, platformId: 0 })
});
steamButtons[1].parentElement.classList.remove("connected");
});
if (isReturn) {
editPlatformsButton.click();
}
}
// Canvasing
const completionCanvas = document.querySelector("#profile-completion-canvas");
const completionText = document.querySelector("#profile-completion-text");
const STROKE_WIDTH = 0.18;
const style = window.getComputedStyle(completionCanvas);
const context = completionCanvas.getContext('2d');
const drawCanvas = () => profileData.then(data => {
const width = Number(style.getPropertyValue('width').slice(0, -2));
const height = width;
context.canvas.width = width;
context.canvas.height = height;
context.clearRect(0, 0, width, height);
context.strokeStyle = root.getProperty('--accent-value3');
context.lineWidth = (width / 2) * STROKE_WIDTH;
context.beginPath();
context.arc(width / 2, height / 2, (width / 2) * (1 - STROKE_WIDTH / 2), -0.5 * Math.PI, (-0.5 + (data.average === null ? 0 : (data.average / 100) * 2)) * Math.PI);
context.stroke();
});
window.addEventListener('resize', drawCanvas);
drawCanvas();
if (profileId === session.id) {
document.querySelector("#profile-page").classList.add("self");
}
}
const expandTemplates = async () => {
await commonTemplates();
template.apply("profile-page").promise(profileData.then(data => ({
id: profileId,
username: data.username,
completed: data.completed,
average: data.average === null ? "N/A" : data.average + "%",
perfect: data.perfect,
})));
template.apply("profile-platforms-list").promise(profileData.then(data =>
data.platforms.map(platform => ({
platform_id: platform.id,
img: `<img class="profile-entry-icon" src="/api/platform/image/${platform.id}" alt="Steam Logo" />`,
name: platform.name,
connected: platform.connected ? "connected" : "",
add:
(platform.id === 0 ? `<img id="add-steam" class="platform-add" src="https://community.cloudflare.steamstatic.com/public/images/signinthroughsteam/sits_01.png" alt="Add" />` :
(platform.id === 1 ? `<p class="platform-unsupported">Coming soon...</p>` :
(platform.id === 2 ? `<p class="platform-unsupported">Coming soon...</p>` :
"")))
}))
));
}
window.addEventListener("load", async (loadEvent) => {
await loadCommon();
if (!/\d+/.test(profileId)) {
isReturn = true;
const platform = profileId;
if (!session) {
window.location.href = "/404";
} else {
profileId = session.lastProfile;
delete session.lastProfile;
}
if (platform === 'steam') {
const query = new URLSearchParams(window.location.search);
if (query.get('openid.mode') === 'cancel') {
} else {
// Regex courtesy of https://github.com/liamcurry/passport-steam/blob/master/lib/passport-steam/strategy.js
var steamId = /^https?:\/\/steamcommunity\.com\/openid\/id\/(\d+)$/.exec(query.get('openid.claimed_id'))[1];
await fetch("/api/user/platforms/add", {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ sessionKey: session.key, userId: profileId, platformId: 0, platformUserId: `${steamId}` })
});
}
}
window.history.replaceState({}, '', `/profile/${profileId}`);
} else if (/\d+/.test(profileId)) {
profileId = Number(profileId);
if (session) {
session.lastProfile = profileId;
}
} else {
// Handle error
}
profileData = fetch(`/api/user/${profileId}`, { method: 'GET', mode: 'cors' })
.then(response => response.json());
await expandTemplates();
await template.expand();
connectNavbar();
loadProfile();
});

View file

@ -0,0 +1,197 @@
var template = template || {};
(() => {
templateTypeEntryMap = new Map();
template.register = (type, callback) => {
if (typeof type !== 'string') {
console.error(`'type' must be a string, recieved: `, type);
} else {
const TYPE_REGEX = /^(\w+)\s*(<\s*\?(?:\s*,\s*\?)*\s*>)?\s*$/;
const result = type.match(TYPE_REGEX);
if (result === null) {
console.error(`'${type}' is not a valid type id`);
} else {
if (result[2] === undefined) {
result[2] = 0;
} else {
result[2] = result[2].split(/\s*,\s*/).length;
}
const completeType = result[1] + ':' + result[2];
if (templateTypeEntryMap.get(completeType) === undefined) {
templateTypeEntryMap.set(completeType, async function() {
await callback.apply(null, Array.from(arguments));
});
} else {
console.error(`${type} is already a registered template!`);
}
}
}
};
// Courtesy of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|\[\]\\]/g, '\\$&'); // $& means the whole matched string
}
/* Intrinsic Templates */
// Basic - Simple search and replace
template.register('Basic', (element, map) => {
let html = element.innerHTML;
function applyObject(object, path) {
for (const key in object) {
const regexKey = escapeRegExp(path + key);
html = html.replace(new RegExp(`(?:(?<!\\\\)\\\${${regexKey}})`, 'gm'), object[key]);
if (typeof object[key] === 'object') {
applyObject(object[key], path + key + '.');
}
}
}
applyObject(map, '');
html = html.replace('\\&', '&');
element.outerHTML = html.trim();
});
// Extern - Retrieve template from webserver
template.register('Extern', (element, name) => {
return fetch(`templates/${name}.html.template`, {
method: 'GET',
mode: 'no-cors',
headers: {
'Content-Type': 'text/plain'
}
}).then(response => response.text().then((data) => {
element.outerHTML = data;
})).catch(error => {
console.error(`failed to retrieve template '${name}': `, error);
});
});
// List - Iterate over list and emit copy of child for each iteration
template.register('List<?>', async (element, subtype, arrayMap) => {
let cumulative = '';
const temp = document.createElement('template');
for (const obj of arrayMap) {
temp.innerHTML = `<template></template>`;
const child = temp.content.children[0];
child.innerHTML = element.innerHTML;
const callback = templateTypeEntryMap.get(subtype.type);
if (callback === undefined) {
cumulative = '';
console.error(`'${subtype.type}' is not a registered template`);
} else {
await callback.apply(null, [ child, obj ]);
}
cumulative = cumulative + temp.innerHTML.trim();
}
element.outerHTML = cumulative;
});
templateEntryMap = new Map();
template.apply = function(pattern, promise) {
if (typeof pattern !== 'string') {
console.error('pattern must be a string, received: ', pattern);
} else {
return new template.apply.applicators(pattern);
}
};
template.apply.applicators = class {
constructor(pattern) {
this._pattern = pattern;
}
_apply(asyncArgs) {
templateEntryMap.set(RegExp('^' + this._pattern + '$'), asyncArgs);
}
values(...args) {
this._apply(async () => Array.from(args));
}
promise(promise) {
let args = null;
promise = promise.then(data => args = [ data ]);
this._apply(async () => args || promise);
}
fetch(dataProcessor, url, options) {
if (typeof dataProcessor === 'string') {
const path = dataProcessor;
dataProcessor = data => {
for (const id of path.split(/\./)) {
data = data[id];
if (data === undefined) {
throw `invalid path '${path}'`;
}
}
return data;
};
};
this.promise(
fetch(url, options || { method: 'GET', mode: 'cors' })
.then(response => response.json())
.then(data => dataProcessor(data))
);
}
};
const parseType = (type) => {
let result = type.match(/^\s*(\w+)\s*(?:<(.*)>)?\s*$/);
let id = result[1];
result = result[2] ? result[2].split(/\s*,\s*/).map(parseType) : [];
return { type: id + ':' + result.length, params: result };
};
const findTemplates = (element) =>
Array
.from(element.querySelectorAll('template'))
.filter(child => Boolean(child.dataset.template))
.map(child => {
const data = child.dataset.template.split(/\s*:\s*/);
return {
id: data[0],
typeCapture: parseType(data[1] || 'Basic'),
element: child
};
});
const expand = async (element) => {
let children = findTemplates(element);
let promises = [];
let parents = new Set();
for (const child of children) {
for (const [pattern, argsCallback] of templateEntryMap) {
await argsCallback().then(args => {
if (pattern.test(child.id)) {
const callback = templateTypeEntryMap.get(child.typeCapture.type);
if (typeof callback !== 'function') {
console.error(`'${child.typeCapture.type}' is not a registered template`);
} else {
let params = Array.from(args)
for (const subtype of child.typeCapture.params) {
params.unshift(subtype);
}
params.unshift(child.element);
let parent = child.element.parentElement;
if (!parents.has(parent)) {
parents.add(parent);
}
promises.push(callback.apply(null, params));
}
}
}).catch(error => {
console.error('failed to retrieve arguments: ', error);
});
}
}
await Promise.all(promises);
promises = [];
for (const parent of parents) {
promises.push(expand(parent));
}
await Promise.all(promises);
};
template.expand = async () => expand(document.children[0]);
})();