A heckin ton. Mostly hackish
1
frontend/webpage/static/res/cancel-hover.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><defs><style>.cls-1{fill:#aaa;}</style></defs><path class="cls-1" d="M1717.59861,392.24859l-65.17222-65.17219a81.91631,81.91631,0,0,0-115.84716,0L1020.675,842.98065,504.77075,327.07642a81.91631,81.91631,0,0,0-115.84717,0L323.7514,392.2486a81.91631,81.91631,0,0,0,0,115.84716L839.65564,1024,323.75141,1539.90429a81.91631,81.91631,0,0,0,0,115.84717l65.17216,65.17214a81.91632,81.91632,0,0,0,115.84716,0l515.90425-515.9043,515.90425,515.90431a81.91633,81.91633,0,0,0,115.84715,0l65.1722-65.17216a81.91631,81.91631,0,0,0,0-115.84718L1201.69434,1024l515.90428-515.90423A81.91632,81.91632,0,0,0,1717.59861,392.24859Z"/></svg>
|
After Width: | Height: | Size: 716 B |
1
frontend/webpage/static/res/cancel.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><defs><style>.cls-1{fill:#eee;}</style></defs><path class="cls-1" d="M1717.59861,392.24859l-65.17222-65.17219a81.91631,81.91631,0,0,0-115.84716,0L1020.675,842.98065,504.77075,327.07642a81.91631,81.91631,0,0,0-115.84717,0L323.7514,392.2486a81.91631,81.91631,0,0,0,0,115.84716L839.65564,1024,323.75141,1539.90429a81.91631,81.91631,0,0,0,0,115.84717l65.17216,65.17214a81.91632,81.91632,0,0,0,115.84716,0l515.90425-515.9043,515.90425,515.90431a81.91633,81.91633,0,0,0,115.84715,0l65.1722-65.17216a81.91631,81.91631,0,0,0,0-115.84718L1201.69434,1024l515.90428-515.90423A81.91632,81.91632,0,0,0,1717.59861,392.24859Z"/></svg>
|
After Width: | Height: | Size: 716 B |
1
frontend/webpage/static/res/completion.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000"><defs><style>.cls-1{fill:#44444c;}</style></defs><path class="cls-1" d="M500,0C223.8576,0,0,223.8576,0,500c0,276.14233,223.8576,500,500,500s500-223.85767,500-500C1000,223.8576,776.1424,0,500,0Zm0,955C248.71045,955,45,751.28955,45,500S248.71045,45,500,45,955,248.71045,955,500,751.28955,955,500,955Z"/></svg>
|
After Width: | Height: | Size: 404 B |
1
frontend/webpage/static/res/dropdown-hover.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><defs><style>.cls-1{fill:#aaa;}</style></defs><polygon class="cls-1" points="1717.02 0 1354.981 0 512 842.981 330.98 1024 1354.98 2048 1717.02 2048 1717.02 1685.961 1055.058 1024 1717.02 362.039 1717.02 0"/></svg>
|
After Width: | Height: | Size: 310 B |
1
frontend/webpage/static/res/dropdown.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><defs><style>.cls-1{fill:#eee;}</style></defs><polygon class="cls-1" points="1717.02 0 1354.981 0 512 842.981 330.98 1024 1354.98 2048 1717.02 2048 1717.02 1685.961 1055.058 1024 1717.02 362.039 1717.02 0"/></svg>
|
After Width: | Height: | Size: 310 B |
1
frontend/webpage/static/res/edit-hover.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><defs><style>.cls-1{fill:#aaa;}</style></defs><path class="cls-1" d="M1776.471,452.54834l-645.29239,645.29238a56.9113,56.9113,0,0,1-40.24239,16.66895H990.40167a56.91135,56.91135,0,0,1-56.91134-56.91134V957.06378a56.9113,56.9113,0,0,1,16.66895-40.24239L1595.45166,271.529Z"/><path class="cls-1" d="M1534.54675,1056.511v479.48889H512.00012V513.45325H991.48907l256.00007-256.00006H401.27344A145.273,145.273,0,0,0,256.00012,402.72656v1244a145.273,145.273,0,0,0,145.27332,145.27332h1244a145.27292,145.27292,0,0,0,145.27331-145.27332V800.511Z"/><path class="cls-1" d="M1738.98066,53.01934h203.9098a52.0902,52.0902,0,0,1,52.0902,52.0902v151.8196a52.0902,52.0902,0,0,1-52.0902,52.0902h-203.9098a0,0,0,0,1,0,0v-256a0,0,0,0,1,0,0Z" transform="translate(418.82598 1373.17402) rotate(-45)"/></svg>
|
After Width: | Height: | Size: 882 B |
1
frontend/webpage/static/res/edit.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><defs><style>.cls-1{fill:#eee;}</style></defs><path class="cls-1" d="M1776.471,452.54834l-645.29239,645.29238a56.9113,56.9113,0,0,1-40.24239,16.66895H990.40167a56.91135,56.91135,0,0,1-56.91134-56.91134V957.06378a56.9113,56.9113,0,0,1,16.66895-40.24239L1595.45166,271.529Z"/><path class="cls-1" d="M1534.54675,1056.511v479.48889H512.00012V513.45325H991.48907l256.00007-256.00006H401.27344A145.273,145.273,0,0,0,256.00012,402.72656v1244a145.273,145.273,0,0,0,145.27332,145.27332h1244a145.27292,145.27292,0,0,0,145.27331-145.27332V800.511Z"/><path class="cls-1" d="M1738.98066,53.01934h203.9098a52.0902,52.0902,0,0,1,52.0902,52.0902v151.8196a52.0902,52.0902,0,0,1-52.0902,52.0902h-203.9098a0,0,0,0,1,0,0v-256a0,0,0,0,1,0,0Z" transform="translate(418.82598 1373.17402) rotate(-45)"/></svg>
|
After Width: | Height: | Size: 882 B |
1
frontend/webpage/static/res/save-hover.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><defs><style>.cls-1{fill:#aaa;}</style></defs><path class="cls-1" d="M170.90729,1032.66212l72.83391-72.83391a76.49864,76.49864,0,0,1,108.18542,0l388.18146,388.18146,955.9653-955.9653a76.49864,76.49864,0,0,1,108.18542,0l72.83391,72.83391a76.49865,76.49865,0,0,1,0,108.18543L740.10808,1710.04834,170.90729,1140.84755A76.49865,76.49865,0,0,1,170.90729,1032.66212Z"/></svg>
|
After Width: | Height: | Size: 466 B |
1
frontend/webpage/static/res/save.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><defs><style>.cls-1{fill:#eee;}</style></defs><path class="cls-1" d="M170.90729,1032.66212l72.83391-72.83391a76.49864,76.49864,0,0,1,108.18542,0l388.18146,388.18146,955.9653-955.9653a76.49864,76.49864,0,0,1,108.18542,0l72.83391,72.83391a76.49865,76.49865,0,0,1,0,108.18543L740.10808,1710.04834,170.90729,1140.84755A76.49865,76.49865,0,0,1,170.90729,1032.66212Z"/></svg>
|
After Width: | Height: | Size: 466 B |
1
frontend/webpage/static/res/upload-hover.svg
Normal file
After Width: | Height: | Size: 19 KiB |
1
frontend/webpage/static/res/upload-invalid.svg
Normal file
After Width: | Height: | Size: 19 KiB |
1
frontend/webpage/static/res/upload.svg
Normal file
After Width: | Height: | Size: 19 KiB |
8
frontend/webpage/static/scripts/about.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
window.addEventListener("load", async (loadEvent) => {
|
||||
await loadCommon();
|
||||
|
||||
await commonTemplates();
|
||||
await template.expand();
|
||||
|
||||
connectNavbar();
|
||||
});
|
97
frontend/webpage/static/scripts/common.js
Normal 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}`);
|
||||
}
|
||||
}
|
||||
};
|
24
frontend/webpage/static/scripts/index.js
Normal 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();
|
||||
});
|
186
frontend/webpage/static/scripts/login.js
Normal 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();
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
278
frontend/webpage/static/scripts/profile.js
Normal 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();
|
||||
});
|
197
frontend/webpage/static/scripts/template.js
Normal 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]);
|
||||
})();
|
13
frontend/webpage/static/styles/about.css
Normal file
|
@ -0,0 +1,13 @@
|
|||
#about-page {
|
||||
max-width: 1600px;
|
||||
}
|
||||
|
||||
#about-text {
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
|
||||
font-size: 18px;
|
||||
color: var(--foreground);
|
||||
|
||||
background-color: var(--distinction);
|
||||
}
|
426
frontend/webpage/static/styles/common.css
Normal file
|
@ -0,0 +1,426 @@
|
|||
:root {
|
||||
--shadow-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
html, body {
|
||||
background-color: var(--background-dark);
|
||||
|
||||
margin: 0;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
#navbar {
|
||||
z-index: 1;
|
||||
|
||||
position: fixed;
|
||||
|
||||
background-color: var(--accent-value2);
|
||||
color: var(--foreground);
|
||||
|
||||
width: 100%;
|
||||
min-height: 76px;
|
||||
height: 5%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
box-shadow: 0px 0px 5px 10px var(--shadow-color);
|
||||
}
|
||||
|
||||
.navbar-section {
|
||||
width: max-content;
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbar-item {
|
||||
box-sizing: border-box;
|
||||
padding: 0px 20px;
|
||||
|
||||
width: max-content;
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
font-size: 24px;
|
||||
|
||||
user-select: none;
|
||||
|
||||
transition-property: background-color;
|
||||
transition-duration: 0.15s;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.navbar-item:hover {
|
||||
background-color: var(--accent-value3);
|
||||
}
|
||||
|
||||
.ap-button {
|
||||
color: var(--foreground);
|
||||
background-color: var(--accent-value2);
|
||||
|
||||
border-radius: 4px;
|
||||
|
||||
cursor: default;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
transition-property: background-color;
|
||||
transition-duration: 0.15s;
|
||||
}
|
||||
|
||||
.ap-button:hover {
|
||||
background-color: var(--accent-value3);
|
||||
}
|
||||
|
||||
.ap-button:active {
|
||||
background-color: var(--accent-value1);
|
||||
}
|
||||
|
||||
.ap-button.disabled {
|
||||
background-color: var(--accent-value1);
|
||||
color: var(--accent-value0);
|
||||
}
|
||||
|
||||
.ap-button.disabled:hover {
|
||||
background-color: var(--accent-value1);
|
||||
color: var(--accent-value0);
|
||||
}
|
||||
|
||||
.ap-button.disabled:active {
|
||||
background-color: var(--accent-value1);
|
||||
color: var(--accent-value0);
|
||||
}
|
||||
|
||||
#content-body {
|
||||
position: relative;
|
||||
|
||||
top: max(76px, 5%);
|
||||
|
||||
width: 100%;
|
||||
height: calc(100% - max(76px, 5%));
|
||||
|
||||
overflow-y: auto;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.page {
|
||||
box-sizing: border-box;
|
||||
|
||||
padding: 32px;
|
||||
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
min-height: 100%;
|
||||
|
||||
background-color: var(--background);
|
||||
box-shadow: 0px 0px 5px 10px var(--shadow-color);
|
||||
}
|
||||
|
||||
.page-subsection {
|
||||
box-sizing: border-box;
|
||||
margin: 32px;
|
||||
padding: 16px;
|
||||
|
||||
background-color: var(--background-dark);
|
||||
|
||||
border-radius: 8px;
|
||||
|
||||
box-shadow: inset 0px 0px 8px 8px var(--shadow-color);
|
||||
}
|
||||
|
||||
.page-subsection-wrapper {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.page-subsection-chunk {
|
||||
box-shadow: 0px 0px 8px 8px var(--shadow-color);
|
||||
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
box-sizing: border-box;
|
||||
|
||||
height: max-content;
|
||||
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.page-subheader {
|
||||
margin-bottom: 16px;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-subheader-flex {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-header-text,
|
||||
.page-subheader-text {
|
||||
margin: 0 0 0.25em;
|
||||
|
||||
color: var(--foreground);
|
||||
|
||||
cursor: default;
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.page-header-text.link,
|
||||
.page-subheader-text.link {
|
||||
transition-property: color;
|
||||
transition-duration: 0.15s;
|
||||
}
|
||||
|
||||
.page-header-text.link:hover,
|
||||
.page-subheader-text.link:hover {
|
||||
color: var(--accent-value4);
|
||||
}
|
||||
|
||||
.page-header-text {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.page-subheader-text,
|
||||
.page-subheader-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.page-subheader-icon {
|
||||
margin: 0 0 0.25em;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header-separator,
|
||||
.page-subheader-separator {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
|
||||
background-color: var(--accent-value3);
|
||||
}
|
||||
|
||||
.list-page-search {
|
||||
box-sizing: border-box;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.list-page-search > label,
|
||||
.list-page-search > input {
|
||||
box-sizing: border-box;
|
||||
padding: 12px 20px;
|
||||
|
||||
color: var(--foreground);
|
||||
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.list-page-search > label {
|
||||
background-color: var(--accent-value2);
|
||||
}
|
||||
|
||||
.list-page-search > label:hover {
|
||||
background-color: var(--accent-value3);
|
||||
}
|
||||
|
||||
.list-page-search > label:active {
|
||||
background-color: var(--accent-value1);
|
||||
|
||||
transition-property: background-color;
|
||||
transition-duration: 0.15s;
|
||||
}
|
||||
|
||||
.list-page-search > input {
|
||||
background-color: var(--distinction);
|
||||
|
||||
border: 0;
|
||||
|
||||
flex-grow: 1;
|
||||
|
||||
outline: none;
|
||||
|
||||
transition-property: background-color, color;
|
||||
transition-duration: 0.075s;
|
||||
}
|
||||
|
||||
.list-page-search > input:focus {
|
||||
background-color: var(--foreground);
|
||||
|
||||
color: var(--background);
|
||||
}
|
||||
|
||||
.list-page-partitions {
|
||||
box-sizing: border-box;
|
||||
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.list-page-filter-partition {
|
||||
width: 20%;
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
.list-page-filter-chunk {
|
||||
background-color: var(--distinction);
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.list-page-filter {
|
||||
box-sizing: border-box;
|
||||
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.list-page-filter-checkbox {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
|
||||
background-color: var(--foreground);
|
||||
|
||||
border: 3px solid var(--foreground);
|
||||
border-radius: 8px;
|
||||
|
||||
transition-property: background-color, border-color;
|
||||
transition-duration: 0.15s;
|
||||
}
|
||||
|
||||
.list-page-filter:hover > .list-page-filter-checkbox {
|
||||
background-color: var(--foreground);
|
||||
border-color: var(--selected-accent1);
|
||||
}
|
||||
|
||||
.list-page-filter.selected > .list-page-filter-checkbox {
|
||||
background-color: var(--selected-accent1);
|
||||
border-color: var(--selected-accent1);
|
||||
}
|
||||
|
||||
.list-page-filter.selected:hover > .list-page-filter-checkbox {
|
||||
background-color: var(--selected-accent0);
|
||||
border-color: var(--selected-accent1);
|
||||
}
|
||||
|
||||
.list-page-filter-name {
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
|
||||
color: var(--foreground);
|
||||
|
||||
font-size: 24px;
|
||||
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.list-page-list-partition {
|
||||
box-sizing: border-box;
|
||||
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.list-page-list {
|
||||
border-radius: 8px;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-page-header {
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
|
||||
background-color: var(--accent-value2);
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
color: var(--foreground);
|
||||
font-size: 24px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.list-page-entry {
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
|
||||
background-color: var(--distinction);
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
color: var(--foreground);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.list-page-entry-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.list-page-entry-text {
|
||||
box-sizing: border-box;
|
||||
|
||||
margin: 0;
|
||||
padding: 0 12px;
|
||||
height: 64px;
|
||||
line-height: 64px;
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
border-top: 1px solid var(--background);
|
||||
}
|
||||
|
||||
.list-page-header > .list-page-entry-text {
|
||||
border: 0;
|
||||
}
|
111
frontend/webpage/static/styles/index.css
Normal file
|
@ -0,0 +1,111 @@
|
|||
#index-page {
|
||||
max-width: 1600px;
|
||||
}
|
||||
|
||||
#list-page-search-filters {
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
}
|
||||
|
||||
#list-page-search-dropdown {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#search-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#list-page-search-pair {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#filter-dropdown-wrapper {
|
||||
box-sizing: border-box;
|
||||
height: 84px;
|
||||
width: 84px;
|
||||
}
|
||||
|
||||
#filter-dropdown-stack {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#filter-dropdown-stack.active {
|
||||
transform: rotateZ(-90deg);
|
||||
}
|
||||
|
||||
#filter-dropdown-button {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
height: 100%;
|
||||
|
||||
display: block;
|
||||
}
|
||||
|
||||
#filter-dropdown-stack:hover > #filter-dropdown-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#filter-dropdown-button-hover {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
height: 100%;
|
||||
|
||||
display: none;
|
||||
}
|
||||
|
||||
#filter-dropdown-stack:hover > #filter-dropdown-button-hover {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#list-page-filters-flex {
|
||||
display: none;
|
||||
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#list-page-filters-flex.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.list-page-filter-section {
|
||||
box-sizing: border-box;
|
||||
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#list-page-filters-background {
|
||||
background-color: var(--distinction);
|
||||
}
|
||||
|
||||
.list-page-entry-text.achievement-name {
|
||||
flex-grow: 3;
|
||||
flex-basis: 0;
|
||||
}
|
||||
|
||||
.list-page-entry-text.achievement-description {
|
||||
flex-grow: 6;
|
||||
flex-basis: 0;
|
||||
}
|
||||
|
||||
.list-page-entry-text.achievement-stages {
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
}
|
127
frontend/webpage/static/styles/login.css
Normal file
|
@ -0,0 +1,127 @@
|
|||
:root {
|
||||
--form-spacing: 48px;
|
||||
|
||||
--element-spacing: 12px;
|
||||
|
||||
--error: #F95959;
|
||||
}
|
||||
|
||||
#login-page {
|
||||
display: block;
|
||||
|
||||
max-width: 1280px;
|
||||
}
|
||||
|
||||
#login-flex {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#login-subsection {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#login-elements {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#login-header {
|
||||
box-sizing: border-box;
|
||||
|
||||
width: 50%;
|
||||
height: max-content;
|
||||
}
|
||||
|
||||
#login-form {
|
||||
box-sizing: border-box;
|
||||
|
||||
padding: 24px 0;
|
||||
|
||||
width: 50%;
|
||||
height: max-content;
|
||||
|
||||
background-color: var(--distinction);
|
||||
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
margin: var(--element-spacing) var(--form-spacing) 0;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
width: calc(100% - var(--form-spacing) * 2);
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.form-row.top {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.form-row.multiline {
|
||||
height: max-content;
|
||||
}
|
||||
|
||||
#error-message {
|
||||
display: none;
|
||||
|
||||
line-height: 40px;
|
||||
|
||||
color: var(--error);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.login-field {
|
||||
box-sizing: border-box;
|
||||
padding: 0 var(--element-spacing);
|
||||
font-size: 18px;
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.login-field.error {
|
||||
background-color: var(--error);
|
||||
}
|
||||
|
||||
.ap-button.login {
|
||||
height: 40px;
|
||||
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
#username,
|
||||
#confirm {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#button-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#create-user-button,
|
||||
#guest-login-button {
|
||||
width: calc(50% - var(--element-spacing) / 2);
|
||||
}
|
||||
|
||||
#login-button {
|
||||
width: calc(100% - var(--form-spacing * 2))
|
||||
}
|
||||
|
||||
#warning {
|
||||
color: var(--error);
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
#warning-message {
|
||||
margin-top: 0;
|
||||
color: var(--foreground);
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
}
|
401
frontend/webpage/static/styles/profile.css
Normal file
|
@ -0,0 +1,401 @@
|
|||
#profile-page {
|
||||
max-width: 1600px;
|
||||
}
|
||||
|
||||
.profile-list {
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
|
||||
border-radius: 8px;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
box-shadow: 0px 0px 8px 8px var(--shadow-color);
|
||||
}
|
||||
|
||||
.profile-entry {
|
||||
overflow: hidden;
|
||||
|
||||
height: 64px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
background-color: var(--distinction);
|
||||
}
|
||||
|
||||
.profile-entry-left {
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.profile-entry-right {
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.profile-entry-icon {
|
||||
width: 64px;
|
||||
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.profile-entry-text {
|
||||
box-sizing: border-box;
|
||||
|
||||
margin: 0;
|
||||
padding: 0px 16px;
|
||||
|
||||
height: 100%;
|
||||
line-height: 64px;
|
||||
|
||||
color: var(--foreground);
|
||||
|
||||
font-size: 24px;
|
||||
|
||||
border-top: 1px solid var(--background);
|
||||
|
||||
flex-basis: max-content;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.top > .profile-entry-text {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.profile-entry-text.platform-name {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#profile-section-1 {
|
||||
box-sizing: border-box;
|
||||
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
#profile-info {
|
||||
width: 50%;
|
||||
height: max-content;
|
||||
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
#profile-info-username-text.active,
|
||||
#profile-info-username-field {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#profile-info-username-field.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#profile-info-username-field {
|
||||
margin-right: 8px;
|
||||
padding: 4px;
|
||||
font-size: 24px;
|
||||
color: var(--background);
|
||||
background-color: var(--foreground);
|
||||
|
||||
border-radius: 8px;
|
||||
|
||||
border: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#profile-info-pfp-border {
|
||||
box-sizing: border-box;
|
||||
|
||||
padding: 24px;
|
||||
background-color: var(--distinction);
|
||||
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
|
||||
box-shadow: 0px 0px 8px 8px var(--shadow-color);
|
||||
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
#profile-info-pfp {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
border-radius: 8px;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#profile-info-pfp-img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
object-fit: contain;
|
||||
|
||||
background-color: var(--background);
|
||||
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
#profile-info-pfp-vignette {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
box-shadow: inset 0px 0px 30px 10px var(--shadow-color);
|
||||
|
||||
border-radius: 8px;
|
||||
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#profile-info-pfp-upload,
|
||||
#profile-info-pfp-upload-hover,
|
||||
#profile-info-pfp-upload-invalid {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
|
||||
border-radius: 8px;
|
||||
|
||||
background-color: var(--background);
|
||||
opacity: 0.8;
|
||||
|
||||
display: block;
|
||||
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#profile-info-pfp-upload {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#profile-info-pfp-upload-hover,
|
||||
#profile-info-pfp-upload-invalid {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
#profile-info-pfp #profile-info-pfp-upload.active,
|
||||
#profile-info-pfp.hover #profile-info-pfp-upload-hover.active,
|
||||
#profile-info-pfp.invalid #profile-info-pfp-upload-invalid.active {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
#profile-info-pfp.hover #profile-info-pfp-upload.active,
|
||||
#profile-info-pfp.invalid #profile-info-pfp-upload.active {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#profile-stats {
|
||||
flex-grow: 1;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#profile-stats-numeric {
|
||||
flex-grow: 1;
|
||||
max-width: 300px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#profile-completion-stack {
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#profile-completion-background {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#profile-completion-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#profile-completion-text {
|
||||
margin: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
color: var(--foreground);
|
||||
font-size: 64px;
|
||||
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#profile-perfect-text {
|
||||
margin: 0;
|
||||
|
||||
height: 48px;
|
||||
|
||||
color: var(--foreground);
|
||||
|
||||
font-size: 48px;
|
||||
line-height: 48px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#profile-section-2 {
|
||||
box-sizing: border-box;
|
||||
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
#profile-hardest {
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#profile-platforms {
|
||||
flex-grow: 1;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
#profile-platforms .profile-entry {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#profile-platforms .profile-entry-text {
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
#profile-platforms .profile-entry.connected,
|
||||
#profile-platforms .profile-entry.editing {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#profile-page .profile-edit-stack,
|
||||
#profile-page .profile-save-stack {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#profile-page.self .profile-edit-stack,
|
||||
#profile-page.self .profile-save-stack.active {
|
||||
display: block;
|
||||
width: max-content;
|
||||
height: max-content;
|
||||
}
|
||||
|
||||
#profile-page.self .profile-edit-stack.active,
|
||||
#profile-page.self .profile-save-stack {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.profile-edit-stack:hover > .profile-edit-hover,
|
||||
.profile-edit {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.profile-edit-stack:hover > .profile-edit,
|
||||
.profile-edit-hover {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.profile-save-stack:hover > .profile-save-hover,
|
||||
.profile-save {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.profile-save-stack:hover > .profile-save,
|
||||
.profile-save-hover {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.profile-entry .platform-remove-stack,
|
||||
.profile-entry .platform-add,
|
||||
.profile-entry .platform-unsupported,
|
||||
.profile-entry.connected.editing .platform-add {
|
||||
border-top: 1px solid var(--background);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.profile-entry.connected.editing .platform-remove-stack {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: max-content;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.platform-remove, .platform-remove-hover {
|
||||
box-sizing: border-box;
|
||||
padding: 12px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.profile-entry.connected.editing .platform-remove-stack .platform-remove,
|
||||
.profile-entry.connected.editing .platform-remove-stack:hover .platform-remove-hover {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.profile-entry.connected.editing .platform-remove-stack:hover .platform-remove,
|
||||
.profile-entry.connected.editing .platform-remove-stack .platform-remove-hover {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.profile-entry .platform-add {
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
padding: 16px 8px;
|
||||
}
|
||||
|
||||
.profile-entry.editing .platform-add {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.profile-entry.editing .platform-unsupported {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0% 2%;
|
||||
line-height: 63px;
|
||||
font-size: 24px;
|
||||
color: var(--foreground-disabled);
|
||||
}
|
||||
|
||||
#profile-ratings {
|
||||
flex-grow: 1;
|
||||
}
|
21
frontend/webpage/static/styles/theme.css
Normal file
|
@ -0,0 +1,21 @@
|
|||
:root {
|
||||
--background-dark: #111117;
|
||||
--background: #22222A;
|
||||
--foreground-disabled: #77777D;
|
||||
--foreground-dark: #AAAAAA;
|
||||
--foreground: #EEEEEE;
|
||||
--distinction: #44444C;
|
||||
|
||||
--accent-hue: 0;
|
||||
|
||||
--accent-value0: hsl(var(--accent-hue), 100%, 16%);
|
||||
--accent-value1: hsl(var(--accent-hue), 100%, 25%);
|
||||
--accent-value2: hsl(var(--accent-hue), 100%, 31%);
|
||||
--accent-value3: hsl(var(--accent-hue), 73%, 47%);
|
||||
--accent-value4: hsl(var(--accent-hue), 83%, 57%);
|
||||
--accent-value5: hsl(var(--accent-hue), 93%, 72%);
|
||||
--accent-pure: hsl(var(--accent-hue), 100%, 50%);
|
||||
|
||||
--selected-accent0: #2266CC;
|
||||
--selected-accent1: #3388FF;
|
||||
}
|