Final Product

This commit is contained in:
Gnarwhal 2021-02-19 15:49:24 -05:00
parent a8cf583569
commit a9f44c29af
Signed by: Gnarwhal
GPG key ID: 0989A73D8C421174
48 changed files with 3908 additions and 581 deletions

View file

@ -0,0 +1,163 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Achievements Project</title>
<link rel="stylesheet" href="/static/styles/theme.css" />
<link rel="stylesheet" href="/static/styles/common.css" />
<link rel="stylesheet" href="/static/styles/achievement.css" />
</head>
<body>
<div id="navbar">
<template data-template="navbar: List<Basic>">
<div id="navbar-section-${section}" class="navbar-section">
<template data-template="navbar-section-${section}: List<Basic>">
<div id="navbar-item-${item}" class="navbar-item" data-page-name="${item}">
${title}
</div>
</template>
</div>
</template>
</div>
<div id="content-body">
<div id="achievement-page" class="page">
<div class="page-subsection">
<div class="page-header">
<p class="page-header-text">Achievement</p>
<div class="page-header-separator"></div>
</div>
</div>
<div id="importing">
<p id="importing-text">Contemplating...</p>
<img id="importing-loading" class="ap-loading" src="/static/res/loading.svg" alt="Loading Symbol" />
</div>
<template data-template="achievement-page">
<div id="achievement-section-0">
<div id="achievement-info" class="page-subsection">
<div class="page-subsection-wrapper">
<div id="achievement-info-subheader" class="page-subheader">
<div id="achievement-info-flex" class="page-subheader-flex">
<p id="achievement-name-text" class="page-subheader-text">${name}</p>
<img id="achievement-icon-img" class="lazy-img" data-src="/api/achievement/${id}/image" alt="Achievement Icon" />
</div>
<div class="page-subheader-separator"></div>
</div>
<p id="achievement-description-text" class="page-subsection-chunk ap-text">${description}</p>
</div>
</div>
</div>
<div id="achievement-section-1">
<div id="achievement-stats" class="page-subsection">
<div id="achievement-stats-numeric">
<div id="achievement-completion" class="page-subsection-wrapper">
<div class="page-subheader">
<p class="page-subheader-text">Completion Rate</p>
<div class="page-subheader-separator"></div>
</div>
<div id="achievement-completion-stack">
<img id="achievement-completion-background" src="/static/res/completion.svg">
<canvas id="achievement-completion-canvas"></canvas>
<p id="achievement-completion-text">${completion}</p>
</div>
</div>
<div id="achievement-difficulty" class="page-subsection-wrapper">
<div class="page-subheader">
<p class="page-subheader-text">Difficulty</p>
<div class="page-subheader-separator"></div>
</div>
<p id="achievement-difficulty-text">${difficulty}</p>
</div>
<div id="achievement-quality" class="page-subsection-wrapper">
<div class="page-subheader">
<p class="page-subheader-text">Quality</p>
<div class="page-subheader-separator"></div>
</div>
<p id="achievement-quality-text">${quality}</p>
</div>
</div>
</div>
<div id="achievement-rating" class="page-subsection">
<div class="page-subsection-wrapper">
<div class="page-subheader">
<div class="page-subheader-flex">
<p class="page-subheader-text">My Rating</p>
<span id="rating-save-stack" class="achievement-save-stack">
<img class="achievement-save page-subheader-icon" src="/static/res/save.svg" alt="Save Platforms" />
<img class="achievement-save-hover page-subheader-icon" src="/static/res/save-hover.svg" alt="Save Platforms Hovered" />
</span>
</div>
<div class="page-subheader-separator"></div>
</div>
<div id="achievement-rating-subsection" class="page-subsection-chunk">
<div id="achievement-rating-numeric">
<div id="achievement-difficulty-rating" class="page-subsection-wrapper">
<div class="page-subheader">
<p class="page-subheader-text">Difficulty</p>
<div class="page-subheader-separator"></div>
</div>
<div class="achievement-rating-text-flex">
<input type="text" id="achievement-difficulty-rating-text" class="achievement-rating-text" value="${my_difficulty}"></input>
<p class="achievement-rating-max-text">/ 10</p>
</div>
</div>
<div id="achievement-quality-rating" class="page-subsection-wrapper">
<div class="page-subheader">
<p class="page-subheader-text">Quality</p>
<div class="page-subheader-separator"></div>
</div>
<div class="achievement-rating-text-flex">
<input type="text" id="achievement-quality-rating-text" class="achievement-rating-text" value="${my_quality}"></input>
<p class="achievement-rating-max-text">/ 10</p>
</div>
</div>
</div>
<div id="achievement-description-rating" class="page-subsection-wrapper">
<div class="page-subheader">
<p class="page-subheader-text">Review</p>
<div class="page-subheader-separator"></div>
</div>
<textarea id="achievement-review-rating-text">${my_review}</textarea>
</div>
</div>
</div>
</div>
</div>
<div id="achievement-section-2">
<div id="achievement-ratings" class="page-subsection">
<div class="page-subsection-wrapper">
<div class="page-subheader">
<p class="page-subheader-text">Ratings</p>
<div class="page-subheader-separator"></div>
</div>
<div class="page-subsection-chunk">
<div class="list-page-list">
<div class="list-page-header">
<p class="list-page-entry-icon"></p>
<p class="list-page-entry-text rating-username">Username</p>
<p class="list-page-entry-text rating-difficulty">Difficulty</p>
<p class="list-page-entry-text rating-quality">Quality</p>
<p class="list-page-entry-text rating-review">Review</p>
</div>
<template data-template="rating-list: List<Basic>">
<div class="list-page-entry rating" data-id="${user_id}">
<img class="list-page-entry-icon lazy-img" data-src="/api/user/${user_id}/image" alt="User Image"></img>
<p class="list-page-entry-text rating-username">${user_username}</p>
<p class="list-page-entry-text rating-difficulty">${user_difficulty}</p>
<p class="list-page-entry-text rating-quality">${user_quality}</p>
<p class="list-page-entry-text rating-review">${user_review}</p>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
<script src="/static/scripts/template.js"></script>
<script src="/static/scripts/common.js"></script>
<script src="/static/scripts/achievement.js"></script>
</body>
</html>

View file

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Achievements Project | Import</title>
<link rel="stylesheet" href="/static/styles/theme.css" />
<link rel="stylesheet" href="/static/styles/common.css" />
<link rel="stylesheet" href="/static/styles/import.css" />
</head>
<body>
<div id="navbar"></div>
<div id="content-body">
<div id="import-page" class="page">
<div class="page-subsection">
<div class="page-header">
<p class="page-header-text">Import</p>
<div class="page-header-separator"></div>
</div>
</div>
<div id="import-dropzone" class="page-subsection">
<div id="import-dropzone-wrapper" class="page-subsection-wrapper">
<div id="upload-wrapper">
<div id="upload-icon-stack">
<img id="import-icon-base" src="/static/res/import.svg" alt="Import Icon">
<img id="import-icon-hover" src="/static/res/import-hover.svg" alt="Import Icon Hover">
</div>
</div>
<div id="import-console">
</div>
</div>
</div>
</div>
</div>
<script src="/static/scripts/template.js"></script>
<script src="/static/scripts/common.js"></script>
<script src="/static/scripts/import.js"></script>
</body>
</html>

View file

@ -119,7 +119,7 @@
<p id="achievement-header-quality" class="list-page-entry-text achievement-quality">Quality</p>
</div>
<template id="achievement-list-template" data-template="achievements-page-list: List<Basic>">
<div id="achievement-entry-${achievement_id}" class="list-page-entry achievement">
<div class="list-page-entry achievement" data-id="${achievement_id}">
<img class="list-page-entry-icon lazy-img" data-src="/api/achievement/${achievement_id}/image" alt="Achievement Icon"></img>
<p class="list-page-entry-text achievement-game-name">${game_name}</p>
<p class="list-page-entry-text achievement-name">${achievement_name}</p>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View file

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

View file

@ -0,0 +1,167 @@
let achievementId = window.location.pathname.split('/').pop();
let isReturn = false;
let achievementData = null;
let myRating = {};
const loadAchievement = () => {
if (myRating.invalid) {
document.querySelector("#achievement-rating").remove();
}
const description = document.querySelector("#achievement-description-text");
if (description.textContent === '') {
description.remove();
}
// Canvasing
const completionCanvas = document.querySelector("#achievement-completion-canvas");
const STROKE_WIDTH = 0.18;
const style = window.getComputedStyle(completionCanvas);
const context = completionCanvas.getContext('2d');
const drawCanvas = () => achievementData.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.completion === null ? 0 : (data.completion / 100) * 2)) * Math.PI);
context.stroke();
});
window.addEventListener('resize', drawCanvas);
drawCanvas();
if (!myRating.invalid) {
const saveReview = document.querySelector("#rating-save-stack");
const myDifficulty = document.querySelector("#achievement-difficulty-rating-text");
const myQuality = document.querySelector("#achievement-quality-rating-text");
const myReview = document.querySelector("#achievement-review-rating-text");
const reviewInput = () => {
saveReview.style.display = 'block';
}
myDifficulty.addEventListener('input', reviewInput);
myQuality.addEventListener('input', reviewInput);
myReview.addEventListener('input', reviewInput);
const saveInputOnEnter = (keyEvent) => {
if (keyEvent.key === 'Enter') {
saveReview.click();
}
}
myDifficulty.addEventListener('keydown', saveInputOnEnter);
myQuality.addEventListener('keydown', saveInputOnEnter);
saveReview.addEventListener('click', (clickEvent) => {
let successful = true;
const difficulty = Number(myDifficulty.value);
const quality = Number(myQuality.value );
if ((Number.isNaN(difficulty) && myDifficulty.value !== '') || difficulty < 0 || difficulty > 10) {
myDifficulty.style.backgroundColor = 'var(--error)';
successful = false;
}
if ((Number.isNaN(quality) && myQuality.value !== '') || quality < 0 || quality > 10) {
myQuality.style.backgroundColor = 'var(--error)';
successful = false;
}
if (successful) {
myDifficulty.style.backgroundColor = 'var(--foreground)';
myQuality.style.backgroundColor = 'var(--foreground)';
saveReview.style.display = 'none';
fetch(`/api/achievement/${achievementId}/rating/${session.id}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionKey: session.key,
difficulty: difficulty,
quality: quality,
review: myReview.value
})
})
.then(response => {
if (response.status === 401) {
responese.json().then(data => {
myDifficulty.value = data.difficulty ? data.difficulty : '';
myQuality.value = data.quality ? data.quality : '';
myReview.value = data.review ? data.review : '';
});
}
});
}
});
}
{
const ratings = document.querySelectorAll(".list-page-entry.rating");
for (const rating of ratings) {
rating.addEventListener("click", (clickEvent) => {
window.location.href = `/user/${rating.dataset.id}`;
});
}
}
}
const expandTemplates = async () => {
await commonTemplates();
if (session.key) {
myRating = await fetch(`/api/achievement/${achievementId}/rating/${session.id}`, { method: 'GET' })
.then(response => {
if (response.status !== 200) {
return { invalid: true };
} else {
return response.json();
}
});
} else {
myRating = { invalid: true };
}
template.apply("achievement-page").promise(achievementData.then(data => ({
id: achievementId,
name: data.name,
description: data.description ? data.description : '',
completion: data.completion === null ? "N/A" : `${data.completion}%`,
difficulty: data.difficulty === null ? "N/A" : `${data.difficulty} / 10`,
quality: data.quality === null ? "N/A" : `${data.quality} / 10`,
my_difficulty: myRating.difficulty ? myRating.difficulty : '',
my_quality: myRating.quality ? myRating.quality : '',
my_review: myRating.review ? myRating.review : '',
})));
template.apply("rating-list").promise(achievementData.then(data => data.ratings.map(data => ({
user_id: data.userId,
user_username: data.username,
user_difficulty: data.difficulty,
user_quality: data.quality,
user_review: data.review
}))));
}
window.addEventListener("load", async (loadEvent) => {
await loadCommon();
var importing = document.querySelector("#importing");
if (/\d+/.test(achievementId)) {
achievementId = Number(achievementId);
} else {
// Handle error
}
importing.remove();
achievementData = fetch(`/api/achievement/${achievementId}`, { method: 'GET' })
.then(response => response.json());
await expandTemplates();
await template.expand();
loadLazyImages();
connectNavbar();
loadAchievement();
});

View file

@ -9,37 +9,51 @@ const loadRoot = () => {
}
};
let session = null;
let session = { id: null };
const clearSession = () => session = { id: null };
const loadSession = async () => {
window.addEventListener('beforeunload', (beforeUnloadEvent) => {
if (session) {
window.sessionStorage.setItem('session', JSON.stringify(session));
} else {
window.sessionStorage.removeItem('session');
}
window.sessionStorage.setItem('session', JSON.stringify(session));
});
session = JSON.parse(window.sessionStorage.getItem('session'));
if (session) {
session = JSON.parse(window.sessionStorage.getItem('session')) || { id: -1 };
if (session.hue) {
root.setProperty('--accent-hue', session.hue);
}
if (session.id !== null) {
await fetch(`/api/auth/refresh`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ key: session.key })
body: JSON.stringify({ key: session.key, id: session.id })
})
.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";
if (response.status !== 200 && window.location.pathname !== "/login") {
session.id = null;
session.key = null;
if (session.id !== -1) {
window.location.href = "/login";
}
} else {
session.key = response.data.key;
session.id = response.data.id;
if (session.id === -1 && window.location.pathname !== '/import') {
window.location.href = '/import';
}
}
});
}
};
const authenticate = (obj) => {
obj.sessionKey = session.key;
obj.userId = session.id;
return obj;
}
const loadCommon = async () => {
loadRoot();
await loadSession();
@ -56,7 +70,7 @@ const commonTemplates = async () => {
{ item: "games", title: "Games" },
{ item: "import", title: "Import" }
]);
if (session) {
if (session.id !== -1 && session.id !== null) {
template.apply("navbar-section-right").values([
{ item: "profile", title: "Profile" },
{ item: "logout", title: "Logout" }
@ -76,29 +90,31 @@ const loadLazyImages = () => {
}
const connectNavbar = () => {
const navItems = document.querySelectorAll(".navbar-item");
if (session.id !== -1) {
const navItems = document.querySelectorAll(".navbar-item");
if (!session || !session.admin) {
document.querySelector("#navbar-item-import").remove();
}
if (!session.admin) {
document.querySelector("#navbar-item-import").remove();
}
for (const item of navItems) {
if (item.dataset.pageName === "logout") {
item.addEventListener("click", (clickEvent) => {
fetch(`/api/auth/logout`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ key: session.key })
for (const item of navItems) {
if (item.dataset.pageName === "logout") {
item.addEventListener("click", (clickEvent) => {
fetch(`/api/auth/logout`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ key: session.key })
});
clearSession();
window.location.href = "/login";
});
session = undefined;
window.location.href = "/login";
});
} else if (item.dataset.pageName === "profile") {
item.addEventListener("click", (clickEvent) => window.location.href = `/user/${session.id}`);
} else {
item.addEventListener("click", (clickEvent) => window.location.href = `/${item.dataset.pageName}`);
} else if (item.dataset.pageName === "profile") {
item.addEventListener("click", (clickEvent) => window.location.href = `/user/${session.id}`);
} else {
item.addEventListener("click", (clickEvent) => window.location.href = `/${item.dataset.pageName}`);
}
}
}
};

View file

@ -0,0 +1,105 @@
let consoleTop = true;
let importConsole = null;
const appendLine = (line) => {
const template = document.createElement("template");
template.innerHTML = `<p class="console-entry ${consoleTop ? 'top' : ''}">${line}</p>`
importConsole.appendChild(template.content.firstElementChild);
consoleTop = false;
};
const loadConsole = () => {
importConsole = document.querySelector("#import-console");
const dropzone = document.querySelector("#import-dropzone");
const uploadWrapper = document.querySelector("#upload-wrapper");
const upload = (dropEvent) => {
dropEvent.preventDefault();
dropzone.classList.remove('active');
if (dropEvent.dataTransfer.files) {
const file = dropEvent.dataTransfer.files[0];
if (file.type === 'application/json') {
importConsole.style.display = 'block';
uploadWrapper.style.display = 'none';
file.text().then(data => JSON.parse(data)).then(data => {
let uploads = Promise.resolve();
for (let i = 0; i < data.platforms.length; ++i) {
const platform = data.platforms[i];
uploads = uploads
.then(() => {
appendLine(`(${i + 1}/${data.platforms.length}) Creating platform: ${platform.name}`);
}).then(() => fetch(
'/api/import/platform', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(authenticate(platform))
}
)
);
}
for (let i = 0; i < data.users.length; ++i) {
const user = data.users[i];
const userPlatforms = user.platforms;
delete user.platforms;
uploads = uploads
.then(() => {
appendLine(`(${i + 1}/${data.users.length}) Creating user: ${user.username}`);
}).then(() => fetch(
'/api/import/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(authenticate(user))
}
)
);
for (let j = 0; j < userPlatforms.length; ++j) {
const platform = userPlatforms[j];
platform.userEmail = user.email;
uploads = uploads
.then(() => {
appendLine(`&nbsp;&nbsp;&nbsp;&nbsp;(${j + 1}/${userPlatforms.length}) Importing platform data: ${data.platforms[platform.platformId].name}`);
}).then(() => fetch(
'/api/import/user/platform', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(authenticate(platform))
}
)
);
}
}
uploads = uploads.then(() => {
if (session.id === -1) {
clearSession();
window.location.href = '/login';
} else {
importConsole.innerHTML = '';
importConsole.style.display = 'none';
uploadWrapper.style.display = 'block';
}
});
});
}
}
};
dropzone.addEventListener("drop", upload);
dropzone.addEventListener("dragover", (dragEvent) => {
dragEvent.preventDefault();
});
};
window.addEventListener("load", async (loadEvent) => {
await loadCommon();
await commonTemplates();
await template.expand();
connectNavbar();
loadConsole();
});

View file

@ -1,7 +1,7 @@
window.addEventListener("load", async (loadEvent) => {
await loadCommon();
if (session && session.key) {
if (session.key) {
window.location.href = '/';
}
@ -20,7 +20,7 @@ window.addEventListener("load", async (loadEvent) => {
const header = document.querySelector("#login-header-text");
const error = document.querySelector("#error-message");
if (session) {
if (!session.key && session.id) {
error.style.display = "block";
error.textContent = "You have been signed out due to inactivity";
}

View file

@ -88,6 +88,13 @@ const loadAchievementSearch = () => {
loading.style.display = 'none';
canSearch = true;
loadLazyImages();
const entries = document.querySelectorAll(".list-page-entry.achievement");
for (const entry of entries) {
entry.addEventListener("click", (clickEvent) => {
window.location.href = `/achievement/${entry.dataset.id}`;
});
}
});
const headers = {

View file

@ -85,7 +85,6 @@ const loadGameSearch = () => {
}))));
await template.expand();
data.then(data => {
console.log(data);
loading.style.display = 'none';
canSearch = true;
loadLazyImages();

View file

@ -60,6 +60,11 @@ const loadProfile = () => {
usernameField.value = usernameField.value.substring(0, 32);
}
});
usernameField.addEventListener("keydown", (keyEvent) => {
if (keyEvent.key === "Enter") {
saveProfileButton.click();
}
})
const pfp = document.querySelector("#profile-info-pfp-img");
const pfpStack = document.querySelector("#profile-info-pfp");
@ -188,7 +193,6 @@ const loadProfile = () => {
// 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);
@ -214,6 +218,24 @@ const loadProfile = () => {
if (profileId === session.id) {
document.querySelector("#profile-page").classList.add("self");
}
{
const noteworthy = document.querySelectorAll(".list-page-entry.achievement");
for (const achievement of noteworthy) {
achievement.addEventListener("click", (clickEvent) => {
window.location.href = `/achievement/${achievement.dataset.id}`;
});
}
}
{
const ratings = document.querySelectorAll(".list-page-entry.rating");
for (const rating of ratings) {
rating.addEventListener("click", (clickEvent) => {
window.location.href = `/achievement/${rating.dataset.id}`;
});
}
}
}
const expandTemplates = async () => {
@ -225,6 +247,18 @@ const expandTemplates = async () => {
average: data.average === null ? "N/A" : data.average + "%",
perfect: data.perfect,
})));
template.apply("profile-noteworthy-list").promise(
fetch(`/api/user/${profileId}/noteworthy`, {
method: 'GET'
})
.then(response => response.json())
.then(data => data.map(data => ({
achievement_id: data.ID,
achievement_name: data.name,
completion: data.completion
}))
)
);
template.apply("profile-platforms-list").promise(profileData.then(data =>
data.platforms.map(platform => ({
platform_id: platform.id,
@ -238,6 +272,13 @@ const expandTemplates = async () => {
"")))
}))
));
template.apply("rating-list").promise(profileData.then(data => data.ratings.map(data => ({
achievement_id: data.achievementId,
rating_achievement: data.name,
rating_difficulty: data.difficulty,
rating_quality: data.quality,
rating_review: data.review
}))));
}
window.addEventListener("load", async (loadEvent) => {
@ -247,7 +288,7 @@ window.addEventListener("load", async (loadEvent) => {
if (!/\d+/.test(profileId)) {
isReturn = true;
const platform = profileId;
if (!session) {
if (!session.key) {
window.location.href = "/404";
} else {
profileId = session.lastProfile;
@ -278,7 +319,7 @@ window.addEventListener("load", async (loadEvent) => {
window.history.replaceState({}, '', `/profile/${profileId}`);
} else if (/\d+/.test(profileId)) {
profileId = Number(profileId);
if (session) {
if (session.key) {
session.lastProfile = profileId;
}
} else {

View file

@ -1,13 +0,0 @@
#about-page {
max-width: 1600px;
}
#about-text {
margin: 0;
padding: 16px;
font-size: 18px;
color: var(--foreground);
background-color: var(--distinction);
}

View file

@ -0,0 +1,459 @@
#achievement-page {
max-width: 1600px;
}
#importing {
flex-direction: column;
align-items: center;
display: none;
}
#importing-text {
margin: 0;
height: 96px;
font-size: 64px;
line-height: 96px;
color: var(--foreground);
}
#importing-loading {
height: 64px;
width: 64px;
}
.achievement-list {
width: 100%;
height: max-content;
border-radius: 8px;
overflow: hidden;
box-shadow: 0px 0px 8px 8px var(--shadow-color);
}
.achievement-entry {
overflow: hidden;
height: 64px;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
background-color: var(--distinction);
}
.achievement-entry-left {
height: 100%;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.achievement-entry-right {
height: 100%;
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
.achievement-entry-icon {
width: 64px;
flex-grow: 0;
}
.achievement-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 > .achievement-entry-text {
border: 0;
}
.achievement-entry-text.platform-name {
flex-grow: 1;
}
#achievement-info {
height: max-content;
}
#achievement-info-flex {
justify-content: flex-start;
align-items: flex-end;
}
#achievement-name-text {
flex-grow: 0;
width: max-content;
}
#achievement-icon-img {
flex-grow: 0;
margin-bottom: 0.25em;
margin-left: 32px;
font-size: 32px;
width: 64px;
height: 64px;
object-fit: contain;
background-color: var(--background-dark);
position: relative;
}
#achievement-description-text {
font-size: 24px;
color: var(--foreground);
background: var(--distinction);
margin: 0;
padding: 16px;
}
#achievement-section-1 {
box-sizing: border-box;
width: 100%;
height: max-content;
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-start;
}
#achievement-stats {
flex-basis: 0;
flex-grow: 1;
display: flex;
flex-direction: row;
justify-content: center;
}
#achievement-stats-numeric {
flex-grow: 1;
width: 100%;
max-width: 800px;
display: flex;
flex-direction: row;
justify-content: space-between;
}
#achievement-completion-stack {
flex-grow: 1;
height: max-content;
position: relative;
}
#achievement-completion-background {
width: 100%;
display: block;
}
#achievement-completion-canvas {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
#achievement-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;
}
#achievement-difficulty,
#achievement-quality {
flex-grow: 1;
}
#achievement-difficulty-text {
margin: 0;
height: 48px;
color: var(--foreground);
font-size: 48px;
line-height: 48px;
text-align: center;
}
#achievement-quality-text {
margin: 0;
height: 48px;
color: var(--foreground);
font-size: 48px;
line-height: 48px;
text-align: center;
}
#achievement-rating {
flex-basis: 0;
flex-grow: 1;
}
.achievement-save-stack {
display: none;
}
.achievement-save-stack.active {
display: block;
width: max-content;
height: max-content;
}
.achievement-save-stack:hover .achievement-save,
.achievement-save-hover {
display: none;
}
.achievement-save,
.achievement-save-stack:hover .achievement-save-hover {
display: block;
}
#achievement-rating-subsection {
background-color: var(--distinction);
}
#achievement-rating-numeric {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.achievement-rating-text-flex {
display: flex;
flex-direction: row;
align-items: center;
}
#achievement-difficulty-rating,
#achievement-quality-rating {
flex-grow: 1;
}
.achievement-rating-text {
flex-basis: max-content;
flex-grow: 1;
}
.achievement-rating-max-text {
flex-grow: 0;
margin: 0;
margin-left: 16px;
font-size: 24px;
color: var(--foreground);
}
#achievement-difficulty-rating-text,
#achievement-quality-rating-text,
#achievement-review-rating-text {
box-sizing: border-box;
padding: 8px;
font-size: 20px;
color: var(--background);
border: 0;
outline: none;
border-radius: 8px;
}
#achievement-review-rating-text {
width: 100%;
height: 96px;
resize: none;
}
#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;
}
#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;
}
.list-page-entry.rating:hover {
background-color: var(--background-light);
}
.list-page-entry-text.rating-username { flex-grow: 1.5; }
.list-page-entry-text.rating-difficulty { flex-grow: 1; }
.list-page-entry-text.rating-quality { flex-grow: 1; }
.list-page-entry-text.rating-review { flex-grow: 5; }
.list-page-header > .list-page-entry-text.rating-username:hover,
.list-page-header > .list-page-entry-text.rating-difficulty:hover,
.list-page-header > .list-page-entry-text.rating-quality:hover,
.list-page-header > .list-page-entry-text.rating-review:hover {
background-color: var(--accent-value2);
}

View file

@ -14,6 +14,14 @@ html, body {
font-family: sans-serif;
}
input {
font-family: sans-serif;
}
textarea {
font-family: sans-serif;
}
#navbar {
z-index: 1;
@ -235,3 +243,350 @@ html, body {
background-color: var(--accent-value3);
}
#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: max-content;
flex-grow: 1;
height: 100%;
display: flex;
flex-direction: column;
}
.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,
.list-page-filter-label {
margin: 0;
padding: 16px;
color: var(--foreground);
font-size: 24px;
user-select: none;
}
.list-page-filter-label {
width: 40%;
}
.list-page-filter-param {
padding: 4px;
width: 25%;
font-size: 24px;
color: var(--background);
background-color: var(--foreground);
border-radius: 8px;
border: 0;
outline: none;
}
#list-page-filters-background {
background-color: var(--distinction);
}
.list-page-entry-text {
flex-basis: 0;
}
.page.search {
max-width: 1720px;
}
.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-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;
transition-property: background-color;
transition-duration: 0.15s;
}
.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;
cursor: default;
}
.list-page-entry > .list-page-entry-text {
border-top: 1px solid var(--background);
border-left: 1px solid var(--background);
}
.list-page-header > .list-page-entry-text {
border-left: 1px solid var(--accent-value0);
transition-property: background-color;
transition-duration: 0.15s;
}
.list-page-header > .list-page-entry-text:hover {
background-color: var(--accent-value1);
}
.list-page-header > .list-page-entry-text:active {
background-color: var(--accent-value0);
}
#loading-results {
margin: 16px 0;
width: 100%;
height: 64px;
object-fit: contain;
display: none;
}

View file

@ -0,0 +1,78 @@
#import-page {
display: flex;
flex-direction: column;
}
#import-dropzone {
flex-grow: 1;
overflow-y: auto;
}
#import-dropzone-wrapper {
box-sizing: border-box;
width: 100%;
height: 100%;
}
#upload-wrapper {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
#upload-icon-stack {
width: max-content;
height: max-content;
position: relative;
}
#import-icon-base,
#import-icon-hover {
width: 256px;
height: 256px;
display: block;
}
#import-icon-hover {
position: absolute;
left: 0;
top: 0;
}
#import-dropzone.active #import-icon-base,
#import-icon-hover {
visibility: hidden;
}
#import-icon-base,
#import-dropzone.active #import-icon-hover {
visibility: visible;
}
#import-console {
display: none;
}
.console-entry {
box-sizing: border-box;
margin: 0;
padding-top: 0.25em;
height: max-content;
color: var(--foreground);
font-size: 24px;
font-family: 'Ubuntu Mono', monospace;
white-space: nowrap;
text-overflow: ellipsis;
overflow-x: hidden;
}
.console-entry.top {
padding-top: 0;
}

View file

@ -1,346 +0,0 @@
#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: max-content;
flex-grow: 1;
height: 100%;
display: flex;
flex-direction: column;
}
.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,
.list-page-filter-label {
margin: 0;
padding: 16px;
color: var(--foreground);
font-size: 24px;
user-select: none;
}
.list-page-filter-label {
width: 40%;
}
.list-page-filter-param {
padding: 4px;
width: 25%;
font-size: 24px;
color: var(--background);
background-color: var(--foreground);
border-radius: 8px;
border: 0;
outline: none;
}
#list-page-filters-background {
background-color: var(--distinction);
}
.list-page-entry-text {
flex-basis: 0;
}
.page.search {
max-width: 1720px;
}
.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-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;
transition-property: background-color;
transition-duration: 0.15s;
}
.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;
cursor: default;
}
.list-page-entry > .list-page-entry-text {
border-top: 1px solid var(--background);
border-left: 1px solid var(--background);
}
.list-page-header > .list-page-entry-text {
border-left: 1px solid var(--accent-value0);
transition-property: background-color;
transition-duration: 0.15s;
}
.list-page-header > .list-page-entry-text:hover {
background-color: var(--accent-value1);
}
.list-page-header > .list-page-entry-text:active {
background-color: var(--accent-value0);
}
#loading-results {
margin: 16px 0;
width: 100%;
height: 64px;
object-fit: contain;
display: none;
}

View file

@ -109,11 +109,11 @@
align-items: flex-start;
}
#profile-info {
width: 50%;
#profile-left {
widtH: 50%;
height: max-content;
max-width: 480px;
max-width: 512px;
}
#profile-info-username-text.active,
@ -309,11 +309,6 @@
height: 100%;
}
#profile-platforms {
flex-grow: 1;
max-width: 480px;
}
#profile-platforms .profile-entry {
display: none;
}
@ -417,5 +412,37 @@
}
#profile-ratings {
flex-grow: 1;
width: 100%;
}
.list-page-entry.achievement:hover {
background-color: var(--background-light);
}
.list-page-entry-text.achievement-name { flex-grow: 2; }
.list-page-entry-text.achievement-completion { flex-grow: 1; }
.list-page-entry-text.achievement-quality { flex-grow: 1; }
.list-page-entry-text.achievement-difficulty { flex-grow: 1; }
.list-page-header > .list-page-entry-text.-aichevevement-name:hover,
.list-page-header > .list-page-entry-text.-aichevevement-completion:hover,
.list-page-header > .list-page-entry-text.-aichevevement-quality:hover,
.list-page-header > .list-page-entry-text.-aichevevement-difficulty:hover {
background-color: var(--accent-value2);
}
.list-page-entry.rating:hover {
background-color: var(--background-light);
}
.list-page-entry-text.rating-achievement { flex-grow: 2; }
.list-page-entry-text.rating-difficulty { flex-grow: 1; }
.list-page-entry-text.rating-quality { flex-grow: 1; }
.list-page-entry-text.rating-review { flex-grow: 5; }
.list-page-header > .list-page-entry-text.rating-achievement:hover,
.list-page-header > .list-page-entry-text.rating-difficulty:hover,
.list-page-header > .list-page-entry-text.rating-quality:hover,
.list-page-header > .list-page-entry-text.rating-review:hover {
background-color: var(--accent-value2);
}

View file

@ -34,30 +34,63 @@
</div>
<template data-template="profile-page">
<div id="profile-section-1">
<div id="profile-info" class="page-subsection">
<div class="page-subsection-wrapper">
<div class="page-subheader">
<div id="profile-info-name" class="page-subheader-flex">
<p id="profile-info-username-text" class="page-subheader-text">${username}</p>
<input id="profile-info-username-field" class="page-subheader-text" value="${username}"/>
<span id="info-edit-stack" class="profile-edit-stack">
<img class="profile-edit page-subheader-icon" src="/static/res/edit.svg" alt="Edit Platforms" />
<img class="profile-edit-hover page-subheader-icon" src="/static/res/edit-hover.svg" alt="Edit Platforms" />
</span>
<span id="info-save-stack" class="profile-save-stack">
<img class="profile-save page-subheader-icon" src="/static/res/save.svg" alt="Save Platforms" />
<img class="profile-save-hover page-subheader-icon" src="/static/res/save-hover.svg" alt="Edit Platforms" />
</span>
<div id="profile-left">
<div id="profile-info" class="page-subsection">
<div class="page-subsection-wrapper">
<div class="page-subheader">
<div id="profile-info-name" class="page-subheader-flex">
<p id="profile-info-username-text" class="page-subheader-text">${username}</p>
<input id="profile-info-username-field" class="page-subheader-text" value="${username}"/>
<span id="info-edit-stack" class="profile-edit-stack">
<img class="profile-edit page-subheader-icon" src="/static/res/edit.svg" alt="Edit Platforms" />
<img class="profile-edit-hover page-subheader-icon" src="/static/res/edit-hover.svg" alt="Edit Platforms" />
</span>
<span id="info-save-stack" class="profile-save-stack">
<img class="profile-save page-subheader-icon" src="/static/res/save.svg" alt="Save Platforms" />
<img class="profile-save-hover page-subheader-icon" src="/static/res/save-hover.svg" alt="Edit Platforms" />
</span>
</div>
<div class="page-subheader-separator"></div>
</div>
<div id="profile-info-pfp-border" class="page-subsection-chunk">
<div id="profile-info-pfp">
<img id="profile-info-pfp-img" class="lazy-img" data-src="/api/user/${id}/image" alt="User's Profile Picture" />
<div id="profile-info-pfp-vignette"></div>
<img id="profile-info-pfp-upload" src="/static/res/upload.svg" alt="Upload Image" />
<img id="profile-info-pfp-upload-hover" src="/static/res/upload-hover.svg" alt="Upload Image" />
<img id="profile-info-pfp-upload-invalid" src="/static/res/upload-invalid.svg" alt="Invalid Image" />
</div>
</div>
<div class="page-subheader-separator"></div>
</div>
<div id="profile-info-pfp-border" class="page-subsection-chunk">
<div id="profile-info-pfp">
<img id="profile-info-pfp-img" class="lazy-img" data-src="/api/user/${id}/image" alt="User's Profile Picture" />
<div id="profile-info-pfp-vignette"></div>
<img id="profile-info-pfp-upload" src="/static/res/upload.svg" alt="Upload Image" />
<img id="profile-info-pfp-upload-hover" src="/static/res/upload-hover.svg" alt="Upload Image" />
<img id="profile-info-pfp-upload-invalid" src="/static/res/upload-invalid.svg" alt="Invalid Image" />
</div>
<div id="profile-platforms" class="page-subsection">
<div class="page-subsection-wrapper">
<div class="page-subheader">
<div class="page-subheader-flex">
<p class="page-subheader-text">Platforms</p>
<span id="platform-edit-stack" class="profile-edit-stack">
<img id="platform-edit" class="profile-edit page-subheader-icon" src="/static/res/edit.svg" alt="Edit Platforms" />
<img id="platform-edit-hover" class="profile-edit-hover page-subheader-icon" src="/static/res/edit-hover.svg" alt="Edit Platforms" />
</span>
<span id="platform-save-stack" class="profile-save-stack">
<img id="platform-save" class="profile-save page-subheader-icon" src="/static/res/save.svg" alt="Save Platforms" />
<img id="platform-save-hover" class="profile-save-hover page-subheader-icon" src="/static/res/save-hover.svg" alt="Edit Platforms" />
</span>
</div>
<div class="page-subheader-separator"></div>
</div>
<div class="profile-list page-subsection-chunk">
<template data-template="profile-platforms-list: List<Basic>">
<div class="profile-entry ${connected}">
${img}
<p class="profile-entry-text platform-name">${name}</p>
<div id="platform-${platform_id}" class="platform-remove-stack">
<img class="platform-remove" src="/static/res/cancel.svg" alt="Cancel" />
<img class="platform-remove-hover" src="/static/res/cancel-hover.svg" alt="Cancel" />
</div>
${add}
</div>
</template>
</div>
</div>
</div>
@ -88,47 +121,50 @@
<p class="page-subheader-text">Noteworthy Achievements</p>
<div class="page-subheader-separator"></div>
</div>
</div>
</div>
</div>
<div id="profile-section-2">
<div id="profile-platforms" class="page-subsection">
<div class="page-subsection-wrapper">
<div class="page-subheader">
<div class="page-subheader-flex">
<p class="page-subheader-text">Platforms</p>
<span id="platform-edit-stack" class="profile-edit-stack">
<img id="platform-edit" class="profile-edit page-subheader-icon" src="/static/res/edit.svg" alt="Edit Platforms" />
<img id="platform-edit-hover" class="profile-edit-hover page-subheader-icon" src="/static/res/edit-hover.svg" alt="Edit Platforms" />
</span>
<span id="platform-save-stack" class="profile-save-stack">
<img id="platform-save" class="profile-save page-subheader-icon" src="/static/res/save.svg" alt="Save Platforms" />
<img id="platform-save-hover" class="profile-save-hover page-subheader-icon" src="/static/res/save-hover.svg" alt="Edit Platforms" />
</span>
</div>
<div class="page-subheader-separator"></div>
</div>
<div class="profile-list page-subsection-chunk">
<template data-template="profile-platforms-list: List<Basic>">
<div class="profile-entry ${connected}">
${img}
<p class="profile-entry-text platform-name">${name}</p>
<div id="platform-${platform_id}" class="platform-remove-stack">
<img class="platform-remove" src="/static/res/cancel.svg" alt="Cancel" />
<img class="platform-remove-hover" src="/static/res/cancel-hover.svg" alt="Cancel" />
</div>
${add}
<div class="list-page-list">
<div class="list-page-header">
<p class="list-page-entry-icon"></p>
<p id="achievement-header-name" class="list-page-entry-text achievement-name">Name</p>
<p id="achievement-header-completion" class="list-page-entry-text achievement-completion">Completion %</p>
</div>
<template data-template="profile-noteworthy-list: List<Basic>">
<div class="list-page-entry achievement" data-id="${achievement_id}">
<img class="list-page-entry-icon lazy-img" data-src="/api/achievement/${achievement_id}/image" alt="Achievement Icon"></img>
<p class="list-page-entry-text achievement-name">${achievement_name}</p>
<p class="list-page-entry-text achievement-completion">${completion}</p>
</div>
</template>
</div>
</div>
</div>
</div>
<div id="profile-section-2">
<div id="profile-ratings" class="page-subsection">
<div class="page-subsection-wrapper">
<div class="page-subheader">
<p class="page-subheader-text">Ratings</p>
<div class="page-subheader-separator"></div>
</div>
<div class="page-subsection-chunk">
<div class="list-page-list">
<div class="list-page-header">
<p class="list-page-entry-icon"></p>
<p class="list-page-entry-text rating-achievement">Achievement</p>
<p class="list-page-entry-text rating-difficulty">Difficulty</p>
<p class="list-page-entry-text rating-quality">Quality</p>
<p class="list-page-entry-text rating-review">Review</p>
</div>
<template data-template="rating-list: List<Basic>">
<div class="list-page-entry rating" data-id="${achievement_id}">
<img class="list-page-entry-icon lazy-img" data-src="/api/achievement/${achievement_id}/image" alt="Achievement Image"></img>
<p class="list-page-entry-text rating-achievement">${rating_achievement}</p>
<p class="list-page-entry-text rating-difficulty">${rating_difficulty}</p>
<p class="list-page-entry-text rating-quality">${rating_quality}</p>
<p class="list-page-entry-text rating-review">${rating_review}</p>
</div>
</template>
</div>
</div>
</div>
</div>
</div>