cobalt/web/src/routes/updates/+page.svelte
2025-04-27 20:38:01 +06:00

226 lines
6.1 KiB
Svelte

<script lang="ts">
import { page } from "$app/stores";
import { browser } from "$app/environment";
import { t } from "$lib/i18n/translations";
import { getAllChangelogs } from "$lib/changelogs";
import type { Optional } from "$lib/types/generic";
import type { ChangelogImport } from "$lib/types/changelogs";
import ChangelogEntry from "$components/changelog/ChangelogEntry.svelte";
import IconArrowLeft from "@tabler/icons-svelte/IconArrowLeft.svelte";
import IconArrowRight from "@tabler/icons-svelte/IconArrowRight.svelte";
const changelogs = getAllChangelogs();
const versions = Object.keys(changelogs);
let changelog: Optional<{
version: string;
page: Promise<ChangelogImport>;
}>;
let currentIndex = 0;
let wrapper: HTMLDivElement;
{
const hash = $page.url.hash.replace("#", "");
const versionIndex = versions.indexOf(hash);
if (versionIndex !== -1 && currentIndex !== versionIndex) {
currentIndex = versionIndex;
}
}
const loadChangelog = async () => {
const version = versions[currentIndex];
changelog = {
version,
page: changelogs[version]() as Promise<ChangelogImport>,
};
if (browser) {
window.location.hash = version;
}
await changelog.page;
};
const loadNext = () => {
if (currentIndex < versions.length - 1) ++currentIndex;
};
const loadPrev = () => {
if (currentIndex > 0) --currentIndex;
};
const preloadNext = () => {
if (!next) return;
changelogs[next]().catch(() => {});
};
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === "ArrowLeft") loadPrev();
else if (e.key === "ArrowRight") loadNext();
};
const handleScroll = (e: WheelEvent) => {
if (!(e.target instanceof HTMLElement)) {
return;
}
if (!wrapper.contains(e.target)) {
wrapper.scrollTop += e.deltaY;
e.preventDefault();
}
};
$: prev = versions[currentIndex - 1];
$: next = versions[currentIndex + 1];
$: currentIndex, loadChangelog();
</script>
<svelte:head>
<title>
{$t("tabs.updates")} ~ {$t("general.cobalt")}
</title>
<meta
property="og:title"
content="{$t("tabs.updates")} ~ {$t("general.cobalt")}"
/>
</svelte:head>
<svelte:window on:keydown={handleKeydown} />
<div class="news" tabindex="-1" on:wheel={handleScroll}>
{#if changelog}
<div id="left-button" class="button-wrapper-desktop">
{#if prev}
<button
on:click={loadPrev}
aria-label={$t("updates.button.previous", { value: prev })}
>
<IconArrowLeft />
{prev || ""}
</button>
{/if}
</div>
<div class="changelog-wrapper" bind:this={wrapper}>
{#await changelog.page}
{#key changelog.version}
<ChangelogEntry
version={changelog.version}
skeleton
/>
{/key}
{:then page}
<svelte:component
this={page.default}
{...page.metadata}
version={changelog.version}
/>
{/await}
<div class="button-wrapper-mobile" class:only-right={!prev}>
{#if prev}
<button
on:click={loadPrev}
aria-label={$t("updates.button.previous", { value: prev })}
>
<IconArrowLeft />
{prev || ""}
</button>
{/if}
{#if next}
<button
on:click={loadNext}
on:focus={preloadNext}
on:mousemove={preloadNext}
aria-label={$t("updates.button.next", { value: next })}
>
{next || ""}
<IconArrowRight />
</button>
{/if}
</div>
</div>
<div id="right-button" class="button-wrapper-desktop">
{#if next}
<button
on:click={loadNext}
on:focus={preloadNext}
on:mousemove={preloadNext}
aria-label={$t("updates.button.next", { value: next })}
>
{next || ""}
<IconArrowRight />
</button>
{/if}
</div>
{/if}
</div>
<style>
.news {
display: flex;
width: 100%;
flex-direction: row;
justify-content: space-evenly;
}
.button-wrapper-desktop {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.button-wrapper-desktop button {
position: absolute;
background-color: transparent;
display: flex;
border: none;
}
button :global(svg) {
stroke-width: 1.6px;
}
.button-wrapper-desktop button {
box-shadow: none;
}
.changelog-wrapper {
flex: 1;
max-width: 850px;
overflow-x: hidden;
padding: var(--padding);
padding-top: calc(var(--padding) + 1em);
}
.button-wrapper-mobile {
display: none;
}
@media only screen and (max-width: 1150px) {
.button-wrapper-mobile {
display: flex;
padding: var(--border-radius);
padding-top: 0;
justify-content: space-between;
}
.button-wrapper-mobile.only-right {
justify-content: end;
}
.button-wrapper-desktop {
display: none;
}
}
@media screen and (max-width: 535px) {
.changelog-wrapper {
padding-top: var(--padding);
}
}
</style>