Add achievement tracker

- close #32
pull/1/head
Made Baruna 2021-04-19 22:13:20 +08:00
parent 8eb07051f9
commit 4a985028f4
9 changed files with 323 additions and 9 deletions

View File

@ -2,8 +2,25 @@
export let checked = false;
export let disabled = false;
export let className = '';
export let inverted = false;
</script>
<label
class="checkbox-wrapper flex flex-1 pl-4 items-center rounded-2xl h-14 cursor-pointer {inverted
? 'bg-item'
: 'bg-background'} {className}"
style="--bg: {inverted ? '#202442' : '#2D325A'};"
>
<span class="flex-1 text-white"><slot /></span>
<input {disabled} class="w-0 h-0 opacity-0" on:change bind:checked type="checkbox" />
<svg
class="checkbox border-4 {inverted ? 'border-background' : 'border-item'} w-10 h-10 rounded-xl mr-2"
viewBox="0 0 100 100"
>
<polyline points="20,50 42,70 80,30" />
</svg>
</label>
<style>
.checkbox {
fill: none;
@ -23,15 +40,7 @@
}
input:checked + .checkbox {
@apply bg-item !important;
background-color: var(--bg) !important;
stroke-dashoffset: 0;
}
</style>
<label class={`checkbox-wrapper flex flex-1 pl-4 items-center bg-background rounded-2xl h-14 cursor-pointer ${className}`}>
<span class="flex-1 text-white"><slot /></span>
<input {disabled} class="w-0 h-0 opacity-0" on:change bind:checked type="checkbox" />
<svg class="checkbox border-4 border-item w-10 h-10 rounded-xl mr-2" viewBox="0 0 100 100">
<polyline points="20,50 42,70 80,30" />
</svg>
</label>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -59,6 +59,10 @@
"twitter": {
"title": "Follow my Twitter, will occasionally post sneak peak what I currently develop and latest update to paimon.moe!",
"detail": "Follow Twitter"
},
"achievement": {
"title": "🏆 View and track your achievement list here",
"detail": "Achievement"
}
},
"characters": {
@ -541,5 +545,9 @@
"current": "Current reminder",
"hoyolab": "Hoyolab Daily Check-In Reminder",
"comingsoon": "Coming Soon!"
},
"achievement": {
"title": "Achievement",
"of": "of"
}
}

View File

@ -519,5 +519,9 @@
"current": "Reminder saat ini",
"hoyolab": "Reminder Hoyolab Daily Check-In",
"comingsoon": "Coming Soon!"
},
"achievement": {
"title": "Achievement",
"of": "dari"
}
}

View File

@ -0,0 +1,18 @@
<script>
import { mdiChevronRight } from '@mdi/js';
import { t } from 'svelte-i18n';
import Icon from '../../components/Icon.svelte';
</script>
<div class="bg-item rounded-xl p-4 flex flex-col">
<p class="text-white">{$t('home.achievement.title')}</p>
<a
href="/achievement"
class="flex justify-end items-center self-end lg:self-start text-white mt-4
bg-background-secondary rounded-xl py-2 px-4 hover:bg-background transition-colors duration-100"
>
{$t('home.achievement.detail')}
<Icon path={mdiChevronRight} />
</a>
</div>

View File

@ -0,0 +1,270 @@
<script context="module">
import data from '../../data/achievement/en.json';
export async function preload() {
return { data };
}
</script>
<script>
import { locale, t } from 'svelte-i18n';
import { onMount, tick } from 'svelte';
import debounce from 'lodash/debounce';
import Check from '../../components/Check.svelte';
import { getAccountPrefix } from '../../stores/account';
import { readSave, updateSave } from '../../stores/saveManager';
export let data;
let achievementContainer;
let achievement = data;
let checkList = {};
let list = [];
let active = '0';
let activeIndex = 0;
let totalAchievement = 0;
let finishedAchievement = 0;
let totalPrimogem = 0;
let obtainedPrimogem = 0;
let categories = [];
function parseCategories() {
categories = Object.entries(achievement).map(([id, data]) => ({
id,
name: data.name,
finished: checkList[id] !== undefined ? Object.values(checkList[id]).filter((e) => e === true).length : 0,
...data.achievements.reduce(
(prev, cur) => {
if (Array.isArray(cur)) {
prev.total += cur.length;
totalAchievement += cur.length;
for (const f of cur) {
totalPrimogem += f.reward;
const finished = checkList[id] !== undefined && checkList[id][f.id];
prev.primogem += finished ? f.reward : 0;
obtainedPrimogem += finished ? f.reward : 0;
finishedAchievement += finished ? 1 : 0;
}
} else {
prev.total += 1;
totalAchievement += 1;
totalPrimogem += cur.reward;
const finished = checkList[id] !== undefined && checkList[id][cur.id];
prev.primogem += finished ? cur.reward : 0;
obtainedPrimogem += finished ? cur.reward : 0;
finishedAchievement += finished ? 1 : 0;
}
return prev;
},
{ total: 0, primogem: 0 },
),
}));
}
const saveData = debounce(() => {
const data = JSON.stringify(checkList);
const prefix = getAccountPrefix();
updateSave(`${prefix}achievement`, data);
}, 2000);
async function changeCategory(id, index, firstLoad) {
active = id;
activeIndex = index;
if (checkList[active] === undefined) {
checkList[active] = {};
}
list = achievement[active].achievements.map((e) => {
if (Array.isArray(e)) {
for (let i = 0; i < e.length; i++) {
e[i].checked = checkList[active][e[i].id] === true;
}
return e;
} else {
e.checked = checkList[active][e.id] === true;
return e;
}
});
if (firstLoad) return;
await tick();
achievementContainer.scrollIntoView({
behavior: 'smooth',
});
}
function toggle({ index, subindex, primogem }) {
let val;
if (subindex !== undefined) {
val = !list[index][subindex].checked;
list[index][subindex].checked = val;
checkList[active][list[index][subindex].id] = val;
} else {
val = !list[index].checked;
list[index].checked = val;
checkList[active][list[index].id] = val;
}
categories[activeIndex].finished += val ? 1 : -1;
categories[activeIndex].primogem += val ? primogem : -primogem;
obtainedPrimogem += val ? primogem : -primogem;
finishedAchievement += val ? 1 : -1;
saveData();
}
async function changeLocale(locale) {
const data = await import(`../../data/achievement/${locale}.json`);
achievement = data.default;
Object.entries(achievement).forEach(([id, data], i) => {
categories[i].name = data.name;
});
changeCategory(active, activeIndex, true);
}
function readLocalData() {
const prefix = getAccountPrefix();
const achievementData = readSave(`${prefix}achievement`);
if (achievementData !== null) {
checkList = JSON.parse(achievementData);
}
}
onMount(() => {
readLocalData();
parseCategories();
changeCategory('0', 0, true);
locale.subscribe((val) => {
changeLocale(val);
});
});
</script>
<svelte:head>
<title>Achievements - Paimon.moe</title>
<meta name="description" content="Track your Genshin Impact achievement easily" />
<meta property="og:description" content="Track your Genshin Impact achievement easily" />
</svelte:head>
<div class="lg:ml-64 pt-20 px-4 lg:px-8 lg:pt-8 max-w-screen-xl">
<div class="flex flex-col md:flex-row items-center gap-2 mb-2 md:mb-0">
<h1 class="font-display font-black text-3xl md:text-4xl text-white">{$t('achievement.title')}</h1>
<div class="flex gap-2">
<p class="text-gray-400 text-xl rounded-xl bg-black bg-opacity-50 px-2 py-1">
{finishedAchievement}
{$t('achievement.of')}
{totalAchievement}
</p>
<div class="text-gray-400 text-xl rounded-xl bg-black bg-opacity-50 px-2 py-1 flex items-center">
<p>{obtainedPrimogem} {$t('achievement.of')} {totalPrimogem}</p>
<img src="/images/primogem.png" class="w-4 h-4 ml-1" alt="primogem" />
</div>
</div>
</div>
<div class="flex flex-col lg:flex-row gap-3">
<div class="flex flex-col gap-2 lg:h-screen lg:overflow-auto lg:sticky lg:pr-1 pb-4 category">
{#each categories as category, index}
<div
class="rounded-xl p-2 cursor-pointer flex flex-col {category.id === active ? 'bg-primary' : 'bg-item'}"
on:click={() => changeCategory(category.id, index)}
>
<p class="font-semibold {category.id === active ? 'text-black' : 'text-white'}">{category.name}</p>
<div class="flex">
<p class="flex-1 {category.id === active ? 'text-gray-900' : 'text-gray-400'}">
{category.finished}
{$t('achievement.of')}
{category.total}
</p>
<p class={category.id === active ? 'text-gray-900' : 'text-gray-400'}>
{category.primogem}
</p>
<img src="/images/primogem.png" class="w-6 h-6 ml-1" alt="primogem" />
</div>
</div>
{/each}
</div>
<div class="flex flex-col gap-2 flex-1 pt-20 lg:pt-2" bind:this={achievementContainer}>
{#each list as el, index}
{#if Array.isArray(el)}
<div class="bg-item rounded-xl px-2 py-1 text-white flex flex-col">
{#each el as it, i}
<div
class="flex items-center {i !== 0 ? 'border-t border-gray-700 pt-1' : ''}
{i > 0 &&
el[i - 1].checked !== true
? 'opacity-25'
: ''}"
>
<div class="flex-1 pr-1">
<p class="font-semibold">{it.name}</p>
<p class="text-gray-400">{it.desc}</p>
</div>
<div class="flex items-center">
<p class="mr-1">{it.reward}</p>
<img src="/images/primogem.png" class="w-8 h-8" alt="primogem" />
</div>
<div>
<Check
checked={list[index][i].checked}
on:change={() => toggle({ index, subindex: i, primogem: it.reward })}
inverted
/>
</div>
</div>
{/each}
</div>
{:else}
<div class="bg-item rounded-xl px-2 py-1 text-white flex items-center">
<div class="flex-1 pr-1">
<p class="font-semibold">{el.name}</p>
<p class="text-gray-400">{el.desc}</p>
</div>
<div class="flex items-center">
<p class="mr-1">{el.reward}</p>
<img src="/images/primogem.png" class="w-8 h-8" alt="primogem" />
</div>
<div>
<Check checked={list[index].checked} on:change={() => toggle({ index, primogem: el.reward })} inverted />
</div>
</div>
{/if}
{/each}
</div>
</div>
</div>
<style>
.category {
width: 100%;
}
@screen lg {
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
@apply bg-transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.35);
@apply rounded-xl;
}
.category {
min-width: 20rem;
width: 20rem;
top: 0px;
padding-top: 8px;
}
}
</style>

View File

@ -14,6 +14,7 @@
import Calculator from './_index/calculator.svelte';
import Discord from './_index/discord.svelte';
import Twitter from './_index/twitter.svelte';
import Achievement from './_index/achievement.svelte';
let refreshLayout;
@ -56,5 +57,6 @@
<Discord on:done={onDone} />
<Twitter on:done={onDone} />
<Calculator on:done={onDone} />
<Achievement on:done={onDone} />
</Masonry>
</div>