mirror of
https://github.com/revanced/revanced-website.git
synced 2025-04-29 22:24:31 +02:00
feat: API outage banner (#254)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de> Co-authored-by: oSumAtrIX <github@osumatrix.me> Co-authored-by: Ushie <ushiekane@gmail.com>
This commit is contained in:
parent
87ce20ff56
commit
adf569c6be
1230
pnpm-lock.yaml
generated
1230
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -34,7 +34,7 @@ body {
|
||||
.wrapper {
|
||||
margin-inline: auto;
|
||||
width: min(90%, 80rem);
|
||||
margin-top: 7rem;
|
||||
margin-top: 2.6rem;
|
||||
}
|
||||
|
||||
:root {
|
||||
@ -73,6 +73,9 @@ body {
|
||||
--surface-nine: hsl(calc(var(--hue, 206) + 24), 9.5%, 17.5%);
|
||||
|
||||
--red-one: hsl(333, 84%, 62%);
|
||||
--red-two: hsl(357, 74%, 60%);
|
||||
|
||||
--yellow-one: hsl(59, 100%, 72%);
|
||||
|
||||
--bezier-one: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
--drop-shadow-one: 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12),
|
||||
|
@ -74,6 +74,15 @@ async function about(): Promise<AboutData> {
|
||||
return { about: json };
|
||||
}
|
||||
|
||||
async function ping(): Promise<boolean> {
|
||||
try {
|
||||
const res = await fetch(`${settings.api_base_url()}/v4/ping`, { method: 'HEAD' });
|
||||
return res.ok;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const staleTime = 5 * 60 * 1000;
|
||||
export const queries = {
|
||||
manager: {
|
||||
@ -100,5 +109,10 @@ export const queries = {
|
||||
queryKey: ['info'],
|
||||
queryFn: about,
|
||||
staleTime
|
||||
},
|
||||
ping: {
|
||||
queryKey: ['ping'],
|
||||
queryFn: ping,
|
||||
staleTime
|
||||
}
|
||||
};
|
||||
|
@ -4,21 +4,48 @@ import { RV_API_URL } from '$env/static/public';
|
||||
export const default_api_url = RV_API_URL;
|
||||
|
||||
const URL_KEY = 'revanced_api_url';
|
||||
const STATUS_KEY = 'revanced_status_url';
|
||||
|
||||
function set_status_url(apiUrl: string) {
|
||||
fetch(`${apiUrl}/v4/about`)
|
||||
.then((response) => (response.ok ? response.json() : null))
|
||||
.then((data) => {
|
||||
if (data?.status) {
|
||||
localStorage.setItem(STATUS_KEY, data.status);
|
||||
console.log('status is now ' + localStorage.getItem(STATUS_KEY));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get base URL
|
||||
export function api_base_url(): string {
|
||||
if (browser) {
|
||||
return localStorage.getItem(URL_KEY) || default_api_url;
|
||||
const apiUrl = localStorage.getItem(URL_KEY) || default_api_url;
|
||||
|
||||
if (!localStorage.getItem(STATUS_KEY)) {
|
||||
set_status_url(apiUrl);
|
||||
}
|
||||
|
||||
return apiUrl;
|
||||
}
|
||||
|
||||
return default_api_url;
|
||||
}
|
||||
|
||||
export function status_url(): string | null {
|
||||
if (browser) {
|
||||
return localStorage.getItem(STATUS_KEY) || null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// (re)set base URL.
|
||||
export function set_api_base_url(url?: string) {
|
||||
if (!url) {
|
||||
localStorage.removeItem(URL_KEY);
|
||||
} else {
|
||||
localStorage.setItem(URL_KEY, url);
|
||||
set_status_url(url);
|
||||
}
|
||||
}
|
||||
|
@ -3,16 +3,20 @@
|
||||
import { horizontalSlide } from '$util/horizontalSlide';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { expoOut } from 'svelte/easing';
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
|
||||
import Navigation from './NavButton.svelte';
|
||||
import Modal from '$lib/components/Dialogue.svelte';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import Banner from '$lib/components/Banner.svelte';
|
||||
import Query from '$lib/components/Query.svelte';
|
||||
|
||||
import Cog from 'svelte-material-icons/Cog.svelte';
|
||||
import Replay from 'svelte-material-icons/Replay.svelte';
|
||||
|
||||
import { api_base_url, set_api_base_url, default_api_url } from '$data/api/settings';
|
||||
import { status_url, api_base_url, set_api_base_url, default_api_url } from '$data/api/settings';
|
||||
import RouterEvents from '$data/RouterEvents';
|
||||
import { queries } from '$data/api';
|
||||
|
||||
import { useQueryClient } from '@tanstack/svelte-query';
|
||||
|
||||
@ -31,6 +35,7 @@
|
||||
}
|
||||
|
||||
let url = api_base_url();
|
||||
const statusUrl = status_url();
|
||||
|
||||
function save() {
|
||||
set_api_base_url(url);
|
||||
@ -44,6 +49,7 @@
|
||||
let menuOpen = false;
|
||||
let modalOpen = false;
|
||||
let y: number;
|
||||
const pingQuery = () => createQuery(['ping'], queries.ping);
|
||||
|
||||
onMount(() => {
|
||||
return RouterEvents.subscribe((event) => {
|
||||
@ -56,53 +62,88 @@
|
||||
|
||||
<svelte:window bind:scrollY={y} />
|
||||
|
||||
<nav class:scrolled={y > 10}>
|
||||
<a class="menu-btn skiptab-btn" href="#skiptab">Skip navigation</a>
|
||||
<div id="nav-container">
|
||||
<Query query={pingQuery()} let:data>
|
||||
{#if !data}
|
||||
<span class="banner">
|
||||
<Banner level="caution" permanent>
|
||||
The API is currently unresponsive and some services may not work correctly. {#if statusUrl}
|
||||
Check the <a href={statusUrl} target="_blank" rel="noopener noreferrer">status page</a> for
|
||||
updates.
|
||||
{/if}
|
||||
</Banner>
|
||||
</span>
|
||||
{/if}
|
||||
</Query>
|
||||
|
||||
<button
|
||||
class="menu-btn mobile-only"
|
||||
on:click={() => (menuOpen = !menuOpen)}
|
||||
class:open={menuOpen}
|
||||
aria-label="Menu"
|
||||
>
|
||||
<span class="menu-btn__burger" />
|
||||
</button>
|
||||
<a href="/" id="logo"><img src="/logo.svg" alt="ReVanced Logo" /></a>
|
||||
<nav class:scrolled={y > 10}>
|
||||
<a class="menu-btn skiptab-btn" href="#skiptab">Skip navigation</a>
|
||||
|
||||
{#key menuOpen}
|
||||
<div
|
||||
class="nav-wrapper"
|
||||
class:desktop-only={!menuOpen}
|
||||
transition:horizontalSlide={{ direction: 'inline', easing: expoOut, duration: 400 }}
|
||||
>
|
||||
<div id="main-navigation">
|
||||
<ul class="nav-buttons">
|
||||
<Navigation href="/" label="Home">Home</Navigation>
|
||||
<Navigation queryKey="manager" href="/download" label="Download">Download</Navigation>
|
||||
<Navigation queryKey="patches" href="/patches" label="Patches">Patches</Navigation>
|
||||
<Navigation queryKey="contributors" href="/contributors" label="Contributors">
|
||||
Contributors
|
||||
</Navigation>
|
||||
<Navigation queryKey={['about', 'team']} href="/donate" label="Donate">Donate</Navigation>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="secondary-navigation">
|
||||
<button on:click={() => (modalOpen = !modalOpen)} aria-label="Settings">
|
||||
<Cog size="20px" color="var(--surface-six)" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
|
||||
{#if menuOpen}
|
||||
<div
|
||||
class="overlay mobile-only"
|
||||
transition:fade={{ duration: 350 }}
|
||||
<button
|
||||
class="menu-btn mobile-only"
|
||||
on:click={() => (menuOpen = !menuOpen)}
|
||||
on:keypress={() => (menuOpen = !menuOpen)}
|
||||
/>
|
||||
{/if}
|
||||
</nav>
|
||||
class:open={menuOpen}
|
||||
aria-label="Menu"
|
||||
>
|
||||
<span class="menu-btn__burger" />
|
||||
</button>
|
||||
<a href="/" id="logo"><img src="/logo.svg" alt="ReVanced Logo" /></a>
|
||||
|
||||
{#key menuOpen}
|
||||
<div
|
||||
id="nav-wrapper-container"
|
||||
class:desktop-only={!menuOpen}
|
||||
transition:horizontalSlide={{ direction: 'inline', easing: expoOut, duration: 400 }}
|
||||
>
|
||||
<div id="banner-pad">
|
||||
<Query query={pingQuery()} let:data>
|
||||
{#if !data}
|
||||
<span class="banner">
|
||||
<Banner level="caution" permanent>
|
||||
The API is currently unresponsive and some services may not work correctly. {#if statusUrl}
|
||||
Check the
|
||||
<a href={statusUrl} target="_blank" rel="noopener noreferrer">status page</a> for
|
||||
updates.
|
||||
{/if}
|
||||
</Banner>
|
||||
</span>
|
||||
{/if}
|
||||
</Query>
|
||||
</div>
|
||||
|
||||
<div class="nav-wrapper">
|
||||
<div id="main-navigation">
|
||||
<ul class="nav-buttons">
|
||||
<Navigation href="/" label="Home">Home</Navigation>
|
||||
<Navigation queryKey="manager" href="/download" label="Download">Download</Navigation>
|
||||
<Navigation queryKey="patches" href="/patches" label="Patches">Patches</Navigation>
|
||||
<Navigation queryKey="contributors" href="/contributors" label="Contributors">
|
||||
Contributors
|
||||
</Navigation>
|
||||
<Navigation queryKey={['about', 'team']} href="/donate" label="Donate"
|
||||
>Donate</Navigation
|
||||
>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="secondary-navigation">
|
||||
<button on:click={() => (modalOpen = !modalOpen)} aria-label="Settings">
|
||||
<Cog size="20px" color="var(--surface-six)" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
|
||||
{#if menuOpen}
|
||||
<div
|
||||
class="overlay mobile-only"
|
||||
transition:fade={{ duration: 350 }}
|
||||
on:click={() => (menuOpen = !menuOpen)}
|
||||
on:keypress={() => (menuOpen = !menuOpen)}
|
||||
/>
|
||||
{/if}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- settings -->
|
||||
<Modal bind:modalOpen>
|
||||
@ -126,7 +167,7 @@
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
<style lang="scss">
|
||||
#logo {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
@ -160,15 +201,26 @@
|
||||
top: 30px;
|
||||
}
|
||||
|
||||
#nav-container {
|
||||
position: sticky;
|
||||
z-index: 666;
|
||||
width: 100%;
|
||||
|
||||
&:has(.nav-buttons > li:first-child.selected) {
|
||||
margin-bottom: 2.65rem;
|
||||
|
||||
&:has(.banner) {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 2rem;
|
||||
z-index: 666;
|
||||
height: 70px;
|
||||
background-color: var(--surface-eight);
|
||||
width: 100%;
|
||||
@ -181,10 +233,6 @@
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 22px;
|
||||
}
|
||||
@ -220,21 +268,45 @@
|
||||
}
|
||||
}
|
||||
|
||||
#banner-pad {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#nav-wrapper-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
#banner-pad {
|
||||
display: block;
|
||||
width: 100vw;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#nav-container:has(.nav-buttons > li:first-child.selected):has(.banner) {
|
||||
margin-bottom: 0rem;
|
||||
}
|
||||
|
||||
#nav-wrapper-container {
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
width: 20rem;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
background-color: var(--surface-eight);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.nav-wrapper {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
position: fixed;
|
||||
width: 20rem;
|
||||
top: 0px;
|
||||
border-radius: 0px 24px 24px 0px;
|
||||
left: 0px;
|
||||
background-color: var(--surface-eight);
|
||||
padding: 1rem;
|
||||
padding-top: 6rem;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.desktop-only {
|
||||
|
160
src/lib/components/Banner.svelte
Normal file
160
src/lib/components/Banner.svelte
Normal file
@ -0,0 +1,160 @@
|
||||
<script lang="ts">
|
||||
import Info from 'svelte-material-icons/InformationOutline.svelte';
|
||||
import Warning from 'svelte-material-icons/AlertOutline.svelte';
|
||||
import Caution from 'svelte-material-icons/AlertCircleOutline.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Button from './Button.svelte';
|
||||
|
||||
export let level: 'info' | 'warning' | 'caution' = 'info';
|
||||
export let permanent: boolean = false;
|
||||
|
||||
const icons = { info: Info, warning: Warning, caution: Caution };
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let closed: boolean = false;
|
||||
|
||||
const dismissBanner = () => {
|
||||
closed = true;
|
||||
dispatch('dismissed');
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="banner-container" class:closed class:permanent>
|
||||
<div class="banner {level}">
|
||||
<div class="banner-text">
|
||||
<svelte:component this={icons[level]} size={permanent ? 22.4 : 32} />
|
||||
<span><slot /></span>
|
||||
</div>
|
||||
{#if !permanent}
|
||||
<Button type="text" icon="close" on:click={dismissBanner}>Dismiss</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.banner-container,
|
||||
.banner-container *,
|
||||
.banner-container :global(*) {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.banner-text :global(a) {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.banner-text :global(a:hover) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.banner-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.banner-container:not(.permanent) {
|
||||
animation: dropDown var(--bezier-one) 0.7s forwards;
|
||||
}
|
||||
|
||||
.banner-container.closed {
|
||||
animation: swipeUp var(--bezier-one) 1.5s forwards;
|
||||
}
|
||||
|
||||
.banner-container.permanent {
|
||||
font-size: 0.87rem;
|
||||
}
|
||||
|
||||
.banner {
|
||||
margin: 0;
|
||||
padding: 1.5rem 1.7rem;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1.3rem;
|
||||
margin: 0.7rem 1rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.banner-container.permanent > .banner {
|
||||
padding: 0.5rem 0.7rem;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.banner-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
gap: 0.55rem;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.banner.info {
|
||||
background-color: var(--surface-four);
|
||||
color: var(--text-one);
|
||||
}
|
||||
|
||||
.banner.warning {
|
||||
background-color: var(--yellow-one);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.banner.warning > :global(button) {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.banner.warning > :global(button img) {
|
||||
filter: grayscale(1) brightness(0); /* Make the icon black */
|
||||
}
|
||||
|
||||
.banner.caution {
|
||||
background-color: var(--red-two);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.banner.caution > :global(button) {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.banner.caution > :global(button img) {
|
||||
filter: grayscale(1) brightness(0); /* Make the icon white */
|
||||
}
|
||||
|
||||
.banner > :global(button):hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.banner {
|
||||
flex-direction: column;
|
||||
padding: 1.1rem 1.3rem;
|
||||
}
|
||||
|
||||
.banner > :global(button) {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dropDown {
|
||||
0% {
|
||||
top: -100%;
|
||||
}
|
||||
100% {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes swipeUp {
|
||||
0% {
|
||||
top: 0;
|
||||
}
|
||||
100% {
|
||||
top: -100%;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -110,6 +110,7 @@
|
||||
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NavHost />
|
||||
|
||||
<Dialogue bind:modalOpen={showConsentModal} notDismissible>
|
||||
<svelte:fragment slot="title">It's your choice</svelte:fragment>
|
||||
<svelte:fragment slot="description">
|
||||
|
@ -168,7 +168,6 @@
|
||||
.wrap {
|
||||
margin-inline: auto;
|
||||
width: min(87%, 100rem);
|
||||
margin-top: 7rem;
|
||||
}
|
||||
.wrappezoid {
|
||||
height: calc(100vh - 225px);
|
||||
|
@ -211,7 +211,6 @@
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 7rem;
|
||||
|
||||
// support revanced and heart thingy
|
||||
section {
|
||||
|
@ -207,7 +207,7 @@
|
||||
}
|
||||
|
||||
.search {
|
||||
padding-top: 5rem;
|
||||
padding-top: 0.6rem;
|
||||
padding-bottom: 1.25rem;
|
||||
background-color: var(--surface-eight);
|
||||
}
|
||||
@ -258,10 +258,6 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search {
|
||||
padding-top: 4.5rem;
|
||||
}
|
||||
|
||||
.patches-container {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
|
Loading…
x
Reference in New Issue
Block a user