refactor: Rename Modals to Dialogs, Move Dialogs and Banners to layout

This commit is contained in:
Ushie
2025-06-14 04:58:38 +03:00
parent 32c0bbb3d4
commit b4eed9ac6a
23 changed files with 458 additions and 420 deletions

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { read_announcements } from '$lib/stores'; import { read_announcements } from '$lib/stores';
import Banner from '$lib/components/Banner.svelte'; import Banner from '$layout/Banners/Banner.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import type { ResponseAnnouncement } from '$lib/types'; import type { ResponseAnnouncement } from '$lib/types';
import { browser } from '$app/environment'; import { browser } from '$app/environment';

View File

@@ -2,7 +2,7 @@
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import Close from 'svelte-material-icons/Close.svelte'; import Close from 'svelte-material-icons/Close.svelte';
import ArrowRight from 'svelte-material-icons/ArrowRight.svelte'; import ArrowRight from 'svelte-material-icons/ArrowRight.svelte';
import Button from './Button.svelte'; import Button from '$lib/components/Button.svelte';
export let title: string; export let title: string;
export let description: string | undefined = undefined; export let description: string | undefined = undefined;

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import Banner from '$lib/components/Banner.svelte'; import Banner from '$layout/Banners/Banner.svelte';
export let statusUrl: string | null = null; export let statusUrl: string | null = null;

View File

@@ -0,0 +1,55 @@
<script lang="ts">
import Button from '$lib/components/Button.svelte';
import Dialog from '$layout/Dialogs/Dialog.svelte';
import { onMount } from 'svelte';
import { allowAnalytics } from '$lib/stores';
import { RV_GOOGLE_TAG_MANAGER_ID } from '$env/static/public';
let showConsentDialog = false;
function enableAnalytics() {
//@ts-ignore
window.dataLayer = window.dataLayer || [];
function gtag(...args: any[]) {
//@ts-ignore
window.dataLayer.push(args);
}
gtag('js', new Date());
const script = document.createElement('script');
script.src = `https://www.googletagmanager.com/gtm.js?id=${RV_GOOGLE_TAG_MANAGER_ID}`;
document.head.append(script);
}
function handleConsent(allowed: boolean) {
localStorage.setItem('analytics', allowed.toString());
allowAnalytics.set(allowed);
showConsentDialog = false;
if (allowed) enableAnalytics();
}
onMount(() => {
const savedConsent = localStorage.getItem('analytics');
if (savedConsent !== null) {
const allowed = savedConsent === 'true';
allowAnalytics.set(allowed);
if (allowed) enableAnalytics();
} else {
showConsentDialog = true;
}
});
</script>
<Dialog bind:dialogOpen={showConsentDialog} notDismissible>
<svelte:fragment slot="title">It's your choice</svelte:fragment>
<svelte:fragment slot="description">
We use analytics to improve your experience on this site. By clicking "Allow", you allow us to
collect anonymous data about your visit.
</svelte:fragment>
<svelte:fragment slot="buttons">
<Button type="text" on:click={() => handleConsent(false)}>Deny</Button>
<Button type="filled" on:click={() => handleConsent(true)}>Allow</Button>
</svelte:fragment>
</Dialog>

View File

@@ -0,0 +1,145 @@
<script lang="ts">
import Button from '$lib/components/Button.svelte';
import Dialog from '$layout/Dialogs/Dialog.svelte';
import CircleMultipleOutline from 'svelte-material-icons/CircleMultipleOutline.svelte';
import ChevronUp from 'svelte-material-icons/ChevronUp.svelte';
import QRCode from '$lib/components/QRCode.svelte';
import WalletOutline from 'svelte-material-icons/WalletOutline.svelte';
import Snackbar from '$lib/components/Snackbar.svelte';
export let dialogOpen: boolean;
export let wallets;
let qrCodeValue = '';
let qrCodeDialogueName = '';
let qrCodeDialogue = false;
let addressSnackbar = false;
async function copyToClipboard(walletAddress: string) {
addressSnackbar = true;
qrCodeDialogue = false;
try {
await navigator.clipboard.writeText(walletAddress);
} catch (error) {
console.error('Failed to copy crypto wallet:', error);
}
}
</script>
<Dialog bind:dialogOpen>
<svelte:fragment slot="icon">
<CircleMultipleOutline size="32px" color="var(--surface-six)" />
</svelte:fragment>
<svelte:fragment slot="title">Cryptocurrencies</svelte:fragment>
<svelte:fragment slot="description">
<hr style="margin: 1rem 0;" />
<div class="wallets">
{#each wallets as wallet}
<button
on:click={() => {
qrCodeValue = wallet.address;
qrCodeDialogueName = wallet.currency_code;
qrCodeDialogue = !qrCodeDialogue;
// when the user clicks a wallet the crypto wallet goes away
// because multi page dialogues aren't implemented yet oops
dialogOpen = false;
}}
>
<div class="wallet-name">
<img
src="/donate/crypto/{wallet.currency_code}.svg"
onerror="this.onerror=null; this.src='/donate/fallback.svg'"
alt={`${wallet.network} icon.'`}
/>
{`${wallet.network} (${wallet.currency_code})`}
</div>
<div id="arrow">
<ChevronUp size="20px" color="var(--surface-six)" />
</div>
</button>
{/each}
</div>
</svelte:fragment>
<svelte:fragment slot="buttons">
<Button type="filled" on:click={() => (dialogOpen = false)}>Close</Button>
</svelte:fragment>
</Dialog>
<Dialog bind:dialogOpen={qrCodeDialogue}>
<svelte:fragment slot="icon">
<WalletOutline size="32px" color="var(--surface-six)" />
</svelte:fragment>
<svelte:fragment slot="title">{qrCodeDialogueName} Wallet</svelte:fragment>
<svelte:fragment slot="description">
<div class="qr-code-body">
{qrCodeValue}
<QRCode codeValue={qrCodeValue} />
</div>
</svelte:fragment>
<svelte:fragment slot="buttons">
<Button
type="text"
on:click={() => {
qrCodeDialogue = false;
dialogOpen = true;
}}>Back</Button
>
<Button type="filled" on:click={() => copyToClipboard(qrCodeValue)}>Copy Address</Button>
</svelte:fragment>
</Dialog>
<Snackbar bind:open={addressSnackbar}>
<svelte:fragment slot="text">Address copied to clipboard</svelte:fragment>
</Snackbar>
<style lang="scss">
.wallets {
// i just guessed this
width: clamp(200px, 75vw, 375px);
#arrow {
height: 20px;
transform: rotate(90deg);
}
button {
width: 100%;
font-size: 0.9rem;
background-color: transparent;
border: none;
color: var(--text-four);
cursor: pointer;
text-align: left;
display: flex;
justify-content: space-between;
background-color: var(--surface-seven);
padding: 0.75rem 1.25rem;
transition: filter 0.4s var(--bezier-one);
&:hover {
filter: brightness(85%);
}
}
.wallet-name {
display: flex;
align-items: center;
gap: 0.5rem;
// crypto icon
img {
height: 24px;
width: 24px;
}
}
}
.qr-code-body {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
word-break: break-word;
text-align: center;
}
</style>

View File

@@ -4,7 +4,7 @@
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte'; import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
export let modalOpen = false; export let dialogOpen = false;
export let fullscreen = false; export let fullscreen = false;
export let notDismissible = false; export let notDismissible = false;
@@ -16,20 +16,20 @@
} }
</script> </script>
{#if modalOpen} {#if dialogOpen}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div <div
class="overlay" class="overlay"
on:click={() => { on:click={() => {
if (!notDismissible) modalOpen = !modalOpen; if (!notDismissible) dialogOpen = !dialogOpen;
}} }}
on:keypress={() => { on:keypress={() => {
if (!notDismissible) modalOpen = !modalOpen; if (!notDismissible) dialogOpen = !dialogOpen;
}} }}
transition:fade={{ easing: quadInOut, duration: 150 }} transition:fade={{ easing: quadInOut, duration: 150 }}
/> />
<dialog <dialog
class="modal"
class:fullscreen class:fullscreen
class:scrolled={y > 10} class:scrolled={y > 10}
aria-modal="true" aria-modal="true"
@@ -37,41 +37,39 @@
on:scroll={parseScroll} on:scroll={parseScroll}
transition:fade={{ easing: quadInOut, duration: 150 }} transition:fade={{ easing: quadInOut, duration: 150 }}
> >
<div class="top"> <div class="title" class:hasIcon={$$slots.icon}>
<div class="title" class:hasIcon={$$slots.icon}> {#if fullscreen}
{#if fullscreen} <button id="back-button" on:click={() => (dialogOpen = !dialogOpen)}>
<button id="back-button" on:click={() => (modalOpen = !modalOpen)}> <ArrowLeft size="24px" color="var(--surface-six)" />
<ArrowLeft size="24px" color="var(--surface-six)" /> </button>
</button>
{/if}
{#if $$slots.icon}
<slot name="icon" />
{/if}
{#if $$slots.title}
<h3>
<slot name="title" />
</h3>
{/if}
</div>
{#if $$slots.description}
<p>
<slot name="description" />
</p>
{/if} {/if}
{#if $$slots.icon}
<div class="slot"><slot /></div> <slot name="icon" />
{/if}
{#if $$slots.buttons} {#if $$slots.title}
<div class="buttons"> <h3>
<slot name="buttons" /> <slot name="title" />
</div> </h3>
{/if} {/if}
</div> </div>
{#if $$slots.description}
<p>
<slot name="description" />
</p>
{/if}
<div class="slot"><slot /></div>
{#if $$slots.buttons}
<div class="buttons">
<slot name="buttons" />
</div>
{/if}
</dialog> </dialog>
{/if} {/if}
<style> <style lang="scss">
.overlay { .overlay {
position: fixed; position: fixed;
top: 0; top: 0;
@@ -82,37 +80,7 @@
z-index: 6; z-index: 6;
} }
.top { dialog {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.title {
display: flex;
align-items: center;
gap: 1rem;
width: 100%;
background-color: var(--surface-seven);
}
.buttons {
display: flex;
gap: 2rem;
justify-content: flex-end;
width: 100%;
}
#back-button {
cursor: pointer;
}
.hasIcon {
flex-direction: column;
}
.modal {
position: fixed; position: fixed;
width: min(85%, 425px); width: min(85%, 425px);
max-height: 75%; max-height: 75%;
@@ -128,7 +96,8 @@
white-space: normal; white-space: normal;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2px; align-items: center;
gap: 1rem;
z-index: 7; z-index: 7;
padding: 32px; padding: 32px;
box-shadow: box-shadow:
@@ -137,6 +106,57 @@
0px 2px 4px -1px rgba(0, 0, 0, 0.2); 0px 2px 4px -1px rgba(0, 0, 0, 0.2);
scrollbar-width: none; scrollbar-width: none;
-ms-overflow-style: none; -ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
#back-button {
cursor: pointer;
}
.hasIcon {
flex-direction: column;
}
.title {
display: flex;
align-items: center;
gap: 1rem;
width: 100%;
background-color: var(--surface-seven);
}
.buttons {
display: flex;
gap: 2rem;
justify-content: flex-end;
width: 100%;
}
&.fullscreen {
padding: 0;
max-height: 100%;
width: 100%;
border-radius: 0;
&.scrolled .title {
border-bottom: 1px solid var(--border);
}
.slot {
padding: 0 32px 32px;
}
.title {
justify-content: flex-start;
position: sticky;
padding: 32px;
padding-bottom: 0.75rem;
top: 0;
left: 0;
}
}
} }
button { button {
@@ -148,38 +168,10 @@
align-items: center; align-items: center;
} }
.fullscreen {
padding: 0;
max-height: 100%;
width: 100%;
border-radius: 0;
}
.fullscreen .slot {
padding: 0 32px 32px;
}
.fullscreen .title {
justify-content: flex-start;
position: sticky;
padding: 32px;
padding-bottom: 0.75rem;
top: 0;
left: 0;
}
.fullscreen.scrolled .title {
border-bottom: 1px solid var(--border);
}
.slot { .slot {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-content: center; align-content: center;
width: 100%; width: 100%;
} }
.modal::-webkit-scrollbar {
display: none;
}
</style> </style>

View File

@@ -0,0 +1,25 @@
<script lang="ts">
import Button from '$lib/components/Button.svelte';
import { Query } from '@tanstack/query-core';
import Dialog from '$layout/Dialogs/Dialog.svelte';
import { queries } from '$data/api';
import { createQuery } from '@tanstack/svelte-query';
export let dialogOpen: boolean;
export let warning: string;
const query = createQuery(queries.manager());
</script>
<Dialog bind:dialogOpen>
<svelte:fragment slot="title">Warning</svelte:fragment>
<svelte:fragment slot="description">{warning} Do you still want to download?</svelte:fragment>
<svelte:fragment slot="buttons">
<Query {query} let:data>
<Button type="text" href={data.release.download_url} on:click={() => (dialogOpen = false)}>
Okay
</Button>
</Query>
<Button type="text" on:click={() => (dialogOpen = false)}>Cancel</Button>
</svelte:fragment>
</Dialog>

View File

@@ -7,12 +7,12 @@
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
function closeModal() { function closeDialog() {
dispatch('close'); dispatch('close');
} }
function handleKeydown(event: KeyboardEvent) { function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') closeModal(); if (event.key === 'Escape') closeDialog();
} }
</script> </script>
@@ -20,16 +20,16 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="modal-overlay" on:click={closeModal} transition:fade={{ duration: 175 }}> <div class="dialog-overlay" on:click={closeDialog} transition:fade={{ duration: 175 }}>
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="modal-content" on:click|stopPropagation transition:fade={{ duration: 175 }}> <div class="dialog-content" on:click|stopPropagation transition:fade={{ duration: 175 }}>
<button class="close-button" on:click={closeModal}>×</button> <button class="close-button" on:click={closeDialog}>×</button>
<img {src} {alt} /> <img {src} {alt} />
</div> </div>
</div> </div>
<style> <style>
.modal-overlay { .dialog-overlay {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
@@ -44,7 +44,7 @@
box-sizing: border-box; box-sizing: border-box;
} }
.modal-content { .dialog-content {
position: relative; position: relative;
max-width: 90vw; max-width: 90vw;
max-height: 90vh; max-height: 90vh;

View File

@@ -3,10 +3,10 @@
import Input from '$lib/components/Input.svelte'; import Input from '$lib/components/Input.svelte';
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import Modal from '$lib/components/Dialogue.svelte'; import Dialog from '$layout/Dialogs/Dialog.svelte';
import { passed_login_with_creds } from '$lib/stores'; import { passed_login_with_creds } from '$lib/stores';
export let modalOpen: boolean; export let dialogOpen: boolean;
let loginForm: HTMLFormElement; let loginForm: HTMLFormElement;
let wrong_credentials = false; let wrong_credentials = false;
@@ -19,15 +19,14 @@
const success = await login(username, password); const success = await login(username, password);
modalOpen = !success; dialogOpen = !success;
console.log(success);
passed_login_with_creds.set(success); passed_login_with_creds.set(success);
wrong_credentials = !success; wrong_credentials = !success;
} }
</script> </script>
<Modal bind:modalOpen> <Dialog bind:dialogOpen>
<div class="admin-modal-content"> <div class="container">
<h2>Login</h2> <h2>Login</h2>
<p>This login is reserved for site administrators. Go back!</p> <p>This login is reserved for site administrators. Go back!</p>
{#if wrong_credentials} {#if wrong_credentials}
@@ -46,14 +45,14 @@
</form> </form>
</div> </div>
<svelte:fragment slot="buttons"> <svelte:fragment slot="buttons">
<Button type="text" on:click={() => (modalOpen = !modalOpen)}>Cancel</Button> <Button type="text" on:click={() => (dialogOpen = !dialogOpen)}>Cancel</Button>
<!-- first paragraph of https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/submit --> <!-- first paragraph of https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/submit -->
<Button type="filled" on:click={() => loginForm.requestSubmit()}>Login</Button> <Button type="filled" on:click={() => loginForm.requestSubmit()}>Login</Button>
</svelte:fragment> </svelte:fragment>
</Modal> </Dialog>
<style lang="scss"> <style lang="scss">
.admin-modal-content { .container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;

View File

@@ -3,26 +3,26 @@
import { admin_login, passed_login_with_creds } from '$lib/stores'; import { admin_login, passed_login_with_creds } from '$lib/stores';
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import Modal from '$lib/components/Dialogue.svelte'; import Dialog from '$layout/Dialogs/Dialog.svelte';
</script> </script>
<Modal bind:modalOpen={$passed_login_with_creds}> <Dialog bind:dialogOpen={$passed_login_with_creds}>
<svelte:fragment slot="title">Successfully logged in!</svelte:fragment> <svelte:fragment slot="title">Successfully logged in!</svelte:fragment>
<div class="login-success"> <div>
This session will expire in This session will expire in
<span class="exp-date">{$admin_login.logged_in ? fromNow($admin_login.expires) : '...'}</span> <span>{$admin_login.logged_in ? fromNow($admin_login.expires) : '...'}</span>
</div> </div>
<svelte:fragment slot="buttons"> <svelte:fragment slot="buttons">
<Button type="filled" on:click={() => passed_login_with_creds.set(false)}>OK</Button> <Button type="filled" on:click={() => passed_login_with_creds.set(false)}>OK</Button>
</svelte:fragment> </svelte:fragment>
</Modal> </Dialog>
<style> <style lang="scss">
.login-success { div {
color: var(--text-one); color: var(--text-one);
}
.exp-date { span {
color: var(--primary); color: var(--primary);
}
} }
</style> </style>

View File

@@ -0,0 +1,40 @@
<script lang="ts">
import Package from '../../routes/patches/Package.svelte';
import Dialog from '$layout/Dialogs/Dialog.svelte';
export let dialogOpen: boolean;
export let searchTerm: string;
export let data;
export let selectedPkg;
</script>
<Dialog bind:dialogOpen fullscreen>
<svelte:fragment slot="title">Packages</svelte:fragment>
<div class="mobile-packages">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span
on:click={() => (dialogOpen = !dialogOpen)}
on:keypress={() => (dialogOpen = !dialogOpen)}
>
<Package {selectedPkg} name="All packages" bind:searchTerm />
</span>
{#each data.packages as pkg}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span
on:click={() => (dialogOpen = !dialogOpen)}
on:keypress={() => (dialogOpen = !dialogOpen)}
>
<Package {selectedPkg} name={pkg} bind:searchTerm />
</span>
{/each}
</div>
</Dialog>
<style lang="scss">
.mobile-packages {
margin-bottom: -1px;
overflow: hidden;
border-radius: 12px;
border: 1px solid var(--border);
}
</style>

View File

@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import { set_access_token } from '$lib/auth'; import { set_access_token } from '$lib/auth';
import { admin_login } from '$lib/stores'; import { admin_login } from '$lib/stores';
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import Modal from '$lib/components/Dialogue.svelte'; import Dialog from '$layout/Dialogs/Dialog.svelte';
export let loginOpen: boolean; export let loginOpen: boolean;
@@ -15,9 +15,9 @@
} }
</script> </script>
<Modal modalOpen={session_expired}> <Dialog dialogOpen={session_expired}>
<svelte:fragment slot="title">Expired session</svelte:fragment> <svelte:fragment slot="title">Expired session</svelte:fragment>
<div class="session-expired"> <div>
This session has expired, log in again to renew or lose all access to administrative power. This session has expired, log in again to renew or lose all access to administrative power.
</div> </div>
<svelte:fragment slot="buttons"> <svelte:fragment slot="buttons">
@@ -26,10 +26,10 @@
Login Login
</Button> </Button>
</svelte:fragment> </svelte:fragment>
</Modal> </Dialog>
<style> <style>
.session-expired { div {
color: var(--text-four); color: var(--text-four);
} }
</style> </style>

View File

@@ -5,12 +5,12 @@
import { useQueryClient } from '@tanstack/svelte-query'; import { useQueryClient } from '@tanstack/svelte-query';
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import Modal from '$lib/components/Dialogue.svelte'; import Dialog from '$layout/Dialogs/Dialog.svelte';
import Replay from 'svelte-material-icons/Replay.svelte'; import Replay from 'svelte-material-icons/Replay.svelte';
import Cog from 'svelte-material-icons/Cog.svelte'; import Cog from 'svelte-material-icons/Cog.svelte';
export let loginOpen: boolean; export let loginOpen: boolean;
export let modalOpen: boolean; export let dialogOpen: boolean;
const client = useQueryClient(); const client = useQueryClient();
@@ -38,7 +38,7 @@
} }
</script> </script>
<Modal bind:modalOpen> <Dialog bind:dialogOpen>
<svelte:fragment slot="icon"> <svelte:fragment slot="icon">
<Cog size="24px" color="var(--surface-six)" /> <Cog size="24px" color="var(--surface-six)" />
</svelte:fragment> </svelte:fragment>
@@ -58,7 +58,7 @@
<Button <Button
type="text" type="text"
disabled={$admin_login.logged_in} disabled={$admin_login.logged_in}
on:click={() => ((loginOpen = !loginOpen), (modalOpen = !modalOpen))} on:click={() => ((loginOpen = !loginOpen), (dialogOpen = !dialogOpen))}
> >
{$admin_login.logged_in ? `Logged in for ${fromNow($admin_login.expires)}` : 'Login'} {$admin_login.logged_in ? `Logged in for ${fromNow($admin_login.expires)}` : 'Login'}
</Button> </Button>
@@ -68,7 +68,7 @@
</div> </div>
</div> </div>
</svelte:fragment> </svelte:fragment>
</Modal> </Dialog>
<style lang="scss"> <style lang="scss">
input { input {
@@ -106,7 +106,8 @@
justify-content: space-between; justify-content: space-between;
flex-wrap: wrap; flex-wrap: wrap;
gap: 1rem; gap: 1rem;
div {
.buttons {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
flex-wrap: wrap; flex-wrap: wrap;

View File

@@ -5,9 +5,9 @@
import { expoOut } from 'svelte/easing'; import { expoOut } from 'svelte/easing';
import { createQuery } from '@tanstack/svelte-query'; import { createQuery } from '@tanstack/svelte-query';
import Navigation from './NavButton.svelte'; import Navigation from '$layout/Navbar/NavButton.svelte';
import Query from '$lib/components/Query.svelte'; import Query from '$lib/components/Query.svelte';
import AnnouncementBanner from '../../routes/announcements/AnnouncementBanner.svelte'; import AnnouncementBanner from '$layout/Banners/AnnouncementBanner.svelte';
import Cog from 'svelte-material-icons/Cog.svelte'; import Cog from 'svelte-material-icons/Cog.svelte';
@@ -15,17 +15,17 @@
import RouterEvents from '$data/RouterEvents'; import RouterEvents from '$data/RouterEvents';
import { queries } from '$data/api'; import { queries } from '$data/api';
import StatusBanner from './StatusBanner.svelte'; import StatusBanner from '$layout/Banners/StatusBanner.svelte';
import SettingsModal from './Modals/SettingsModal.svelte'; import SettingsDialog from '$layout/Dialogs/SettingsDialog.svelte';
import LoginModal from './Modals/LoginModal.svelte'; import LoginDialog from '$layout/Dialogs/LoginDialog.svelte';
import LoginSuccessfulModal from './Modals/LoginSuccessfulModal.svelte'; import LoginSuccessfulDialog from '$layout/Dialogs/LoginSuccessfulDialog.svelte';
import SessionExpiredModal from './Modals/SessionExpiredModal.svelte'; import SessionExpiredDialog from '$layout/Dialogs/SessionExpiredDialog.svelte';
const ping = createQuery(queries.ping()); const ping = createQuery(queries.ping());
const statusUrl = status_url(); const statusUrl = status_url();
let menuOpen = false; let menuOpen = false;
const modals: Record<string, boolean> = { const dialogs: Record<string, boolean> = {
settings: false, settings: false,
login: false login: false
}; };
@@ -92,11 +92,11 @@
</div> </div>
<div id="secondary-navigation"> <div id="secondary-navigation">
<button <button
on:click={() => (modals.settings = !modals.settings)} on:click={() => (dialogs.settings = !dialogs.settings)}
class:selected={modals.settings} class:selected={dialogs.settings}
aria-label="Settings" aria-label="Settings"
> >
<Cog size="20px" color={modals.settings ? 'var(--primary)' : 'var(--surface-six)'} /> <Cog size="20px" color={dialogs.settings ? 'var(--primary)' : 'var(--surface-six)'} />
</button> </button>
</div> </div>
</div> </div>
@@ -114,13 +114,13 @@
{/if} {/if}
</nav> </nav>
<SettingsModal bind:modalOpen={modals.settings} bind:loginOpen={modals.login} /> <SettingsDialog bind:dialogOpen={dialogs.settings} bind:loginOpen={dialogs.login} />
<LoginModal bind:modalOpen={modals.login} /> <LoginDialog bind:dialogOpen={dialogs.login} />
<LoginSuccessfulModal /> <LoginSuccessfulDialog />
<SessionExpiredModal bind:loginOpen={modals.login} /> <SessionExpiredDialog bind:loginOpen={dialogs.login} />
<style lang="scss"> <style lang="scss">
#secondary-navigation { #secondary-navigation {

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import ImageModal from '$lib/components/ImageModal.svelte'; import ImageDialog from '$layout/Dialogs/ImageDialog.svelte';
export let images: string[]; export let images: string[];
export let columns: number = 3; export let columns: number = 3;
@@ -7,14 +7,14 @@
let selectedImage: { src: string; alt: string } | null = null; let selectedImage: { src: string; alt: string } | null = null;
function openModal(image: string, index: number) { function openDialog(image: string, index: number) {
selectedImage = { selectedImage = {
src: image, src: image,
alt: `Gallery image ${index + 1}` alt: `Gallery image ${index + 1}`
}; };
} }
function closeModal() { function closeDialog() {
selectedImage = null; selectedImage = null;
} }
</script> </script>
@@ -28,8 +28,8 @@
src={image} src={image}
alt={`Gallery image ${i + 1}`} alt={`Gallery image ${i + 1}`}
loading="lazy" loading="lazy"
on:click={() => openModal(image, i)} on:click={() => openDialog(image, i)}
on:keydown={(e) => e.key === 'Enter' && openModal(image, i)} on:keydown={(e) => e.key === 'Enter' && openDialog(image, i)}
tabindex="0" tabindex="0"
/> />
</div> </div>
@@ -37,7 +37,7 @@
</div> </div>
{#if selectedImage} {#if selectedImage}
<ImageModal src={selectedImage.src} alt={selectedImage.alt} on:close={closeModal} /> <ImageDialog src={selectedImage.src} alt={selectedImage.alt} on:close={closeDialog} />
{/if} {/if}
<style> <style>

View File

@@ -7,7 +7,7 @@
viewBox="0 0 1440 500" viewBox="0 0 1440 500"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="none" preserveAspectRatio="none"
style="opacity: {visibility ? '100%' : '0'}" style="opacity: {visibility ? '100%' : '0'}; height: {visibility ? '40vh' : '0px'}"
> >
<path class="wave" /> <path class="wave" />
</svg> </svg>
@@ -19,10 +19,9 @@
bottom: -1px; bottom: -1px;
z-index: -1; z-index: -1;
width: 100%; width: 100%;
height: 40vh;
} }
@media screen and (max-height: 780px) { @media (max-height: 780px) {
svg { svg {
opacity: 0 !important; opacity: 0 !important;
} }

View File

@@ -64,3 +64,5 @@ export const read_announcements = writable<Set<number>>(new Set(), (set) => {
}); });
export const passed_login_with_creds = writable(false); // will only change when the user INPUTS the credentials, not if the session is just valid export const passed_login_with_creds = writable(false); // will only change when the user INPUTS the credentials, not if the session is just valid
export const allowAnalytics = writable(false);

View File

@@ -18,13 +18,11 @@
import NavHost from '$layout/Navbar/NavHost.svelte'; import NavHost from '$layout/Navbar/NavHost.svelte';
import Spinner from '$lib/components/Spinner.svelte'; import Spinner from '$lib/components/Spinner.svelte';
import Dialogue from '$lib/components/Dialogue.svelte'; import ConsentDialog from '$layout/Dialogs/ConsentDialog.svelte';
import Button from '$lib/components/Button.svelte';
import { staleTime } from '$data/api'; import { staleTime } from '$data/api';
import RouterEvents from '$data/RouterEvents'; import RouterEvents from '$data/RouterEvents';
import { events as themeEvents } from '$util/themeEvents'; import { events as themeEvents } from '$util/themeEvents';
import { RV_GOOGLE_TAG_MANAGER_ID } from '$env/static/public';
import FooterHost from '$layout/Footer/FooterHost.svelte'; import FooterHost from '$layout/Footer/FooterHost.svelte';
const queryClient = new QueryClient({ const queryClient = new QueryClient({
@@ -36,50 +34,6 @@
} }
}); });
let showConsentModal = false;
let allowAnalytics = false;
function enableAnalytics() {
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', RV_GOOGLE_TAG_MANAGER_ID);
var s = document.createElement('script');
s.src = `https://www.googletagmanager.com/gtm.js?id=${RV_GOOGLE_TAG_MANAGER_ID}`;
document.head.append(s);
}
function rememberChoice(allow: boolean) {
localStorage.setItem('analytics', allow.toString());
showConsentModal = false;
allowAnalytics = allow;
if (allowAnalytics) enableAnalytics();
}
onMount(() => {
// Check if the user has already decided
const hasDecided = localStorage.getItem('analytics') !== null;
if (hasDecided) {
allowAnalytics = localStorage.getItem('analytics') === 'true';
if (allowAnalytics) enableAnalytics();
} else {
showConsentModal = true;
}
new DateTriggerEventHandler(themeEvents);
isRestoring.set(true);
const [unsubscribe, promise] = persistQueryClient({
queryClient,
persister: createSyncStoragePersister({ storage: localStorage })
});
promise.then(() => isRestoring.set(false));
return unsubscribe;
});
// Just like the set/clearInterval example found here: https://svelte.dev/docs#run-time-svelte-store-derived // Just like the set/clearInterval example found here: https://svelte.dev/docs#run-time-svelte-store-derived
const show_loading_animation = derived( const show_loading_animation = derived(
RouterEvents, RouterEvents,
@@ -94,32 +48,22 @@
}, },
false false
); );
onMount(() => {
new DateTriggerEventHandler(themeEvents);
isRestoring.set(true);
const [unsubscribe, promise] = persistQueryClient({
queryClient,
persister: createSyncStoragePersister({ storage: localStorage })
});
promise.then(() => isRestoring.set(false));
return unsubscribe;
});
</script> </script>
{#if allowAnalytics} <ConsentDialog />
<!-- Google Tag Manager (noscript) -->
<noscript>
<!-- svelte-ignore a11y-missing-attribute -->
<iframe
src="https://www.googletagmanager.com/ns.html?id={RV_GOOGLE_TAG_MANAGER_ID}"
height="0"
width="0"
style="display: none; visibility: hidden"
></iframe>
</noscript>
{/if}
<Dialogue bind:modalOpen={showConsentModal} notDismissible>
<svelte:fragment slot="title">It's your choice</svelte:fragment>
<svelte:fragment slot="description">
We use analytics to improve your experience on this site. By clicking "Allow", you allow us to
collect anonymous data about your visit.
</svelte:fragment>
<svelte:fragment slot="buttons">
<Button type="text" on:click={() => rememberChoice(false)}>Deny</Button>
<Button type="filled" on:click={() => rememberChoice(true)}>Allow</Button>
</svelte:fragment>
</Dialogue>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<NavHost /> <NavHost />
<div id="skiptab"> <div id="skiptab">

View File

@@ -3,7 +3,7 @@
import { admin, queries } from '$data/api'; import { admin, queries } from '$data/api';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import Dialogue from '$lib/components/Dialogue.svelte'; import Dialog from '$layout/Dialogs/Dialog.svelte';
import type { Announcement, ResponseAnnouncement } from '$lib/types'; import type { Announcement, ResponseAnnouncement } from '$lib/types';
import moment from 'moment'; import moment from 'moment';
import { isValidUrl } from '$util/isValidUrl'; import { isValidUrl } from '$util/isValidUrl';
@@ -97,14 +97,14 @@
<svelte:window on:beforeunload={handleUnload} /> <svelte:window on:beforeunload={handleUnload} />
<Dialogue bind:modalOpen={showDeleteConfirm}> <Dialog bind:dialogOpen={showDeleteConfirm}>
<svelte:fragment slot="title">Confirm?</svelte:fragment> <svelte:fragment slot="title">Confirm?</svelte:fragment>
<svelte:fragment slot="description">Do you want to delete this announcement?</svelte:fragment> <svelte:fragment slot="description">Do you want to delete this announcement?</svelte:fragment>
<svelte:fragment slot="buttons"> <svelte:fragment slot="buttons">
<Button type="text" on:click={() => (showDeleteConfirm = !showDeleteConfirm)}>Cancel</Button> <Button type="text" on:click={() => (showDeleteConfirm = !showDeleteConfirm)}>Cancel</Button>
<Button type="filled" on:click={deleteAnnouncement}>OK</Button> <Button type="filled" on:click={deleteAnnouncement}>OK</Button>
</svelte:fragment> </svelte:fragment>
</Dialogue> </Dialog>
<div> <div>
{#if isEditing || isCreating} {#if isEditing || isCreating}

View File

@@ -6,40 +6,18 @@
import { createQuery } from '@tanstack/svelte-query'; import { createQuery } from '@tanstack/svelte-query';
import Head from '$lib/components/Head.svelte'; import Head from '$lib/components/Head.svelte';
import Button from '$lib/components/Button.svelte';
import Snackbar from '$lib/components/Snackbar.svelte';
import Query from '$lib/components/Query.svelte'; import Query from '$lib/components/Query.svelte';
import Dialogue from '$lib/components/Dialogue.svelte'; import CryptoDialog from '$layout/Dialogs/CryptoDialog.svelte';
import QRCode from './QRCode.svelte';
import DonateHeartAnimation from './DonateHeartAnimation.svelte'; import DonateHeartAnimation from './DonateHeartAnimation.svelte';
import TeamMember from './TeamMember.svelte'; import TeamMember from './TeamMember.svelte';
import CircleMultipleOutline from 'svelte-material-icons/CircleMultipleOutline.svelte';
import WalletOutline from 'svelte-material-icons/WalletOutline.svelte';
import ChevronUp from 'svelte-material-icons/ChevronUp.svelte';
import { supportsWebP } from '$util/supportsWebP'; import { supportsWebP } from '$util/supportsWebP';
const teamQuery = createQuery(queries.team()); const teamQuery = createQuery(queries.team());
const aboutQuery = createQuery(queries.about()); const aboutQuery = createQuery(queries.about());
let qrCodeDialogue = false;
let cryptoDialogue = false; let cryptoDialogue = false;
let addressSnackbar = false;
let qrCodeValue = '';
let qrCodeDialogueName = '';
async function copyToClipboard(walletAddress: string) {
addressSnackbar = true;
qrCodeDialogue = false;
try {
await navigator.clipboard.writeText(walletAddress);
} catch (error) {
console.error('Failed to copy crypto wallet:', error);
}
}
const shuffle = <T,>(array: T[]) => const shuffle = <T,>(array: T[]) =>
array array
@@ -136,80 +114,16 @@
</Query> </Query>
</main> </main>
<Dialogue bind:modalOpen={cryptoDialogue}> <Query query={aboutQuery} let:data>
<svelte:fragment slot="icon"> <CryptoDialog bind:dialogOpen={cryptoDialogue} wallets={data.about.donations.wallets} />
<CircleMultipleOutline size="32px" color="var(--surface-six)" /> </Query>
</svelte:fragment>
<svelte:fragment slot="title">Cryptocurrencies</svelte:fragment>
<svelte:fragment slot="description">
<hr style="margin: 1rem 0;" />
<div class="wallets">
<Query query={aboutQuery} let:data>
{#each data.about.donations.wallets as wallet}
<button
on:click={() => {
qrCodeValue = wallet.address;
qrCodeDialogueName = wallet.currency_code;
qrCodeDialogue = !qrCodeDialogue;
// when the user clicks a wallet the crypto wallet goes away
// because multi page dialogues aren't implemented yet oops
cryptoDialogue = false;
}}
>
<div class="wallet-name">
<img
src="/donate/crypto/{wallet.currency_code}.svg"
onerror="this.onerror=null; this.src='/donate/fallback.svg'"
alt={`${wallet.network} icon.'`}
/>
{`${wallet.network} (${wallet.currency_code})`}
</div>
<div id="arrow">
<ChevronUp size="20px" color="var(--surface-six)" />
</div>
</button>
{/each}
</Query>
</div>
</svelte:fragment>
<svelte:fragment slot="buttons">
<Button type="filled" on:click={() => (cryptoDialogue = false)}>Close</Button>
</svelte:fragment>
</Dialogue>
<Dialogue bind:modalOpen={qrCodeDialogue}>
<svelte:fragment slot="icon">
<WalletOutline size="32px" color="var(--surface-six)" />
</svelte:fragment>
<svelte:fragment slot="title">{qrCodeDialogueName} Wallet</svelte:fragment>
<svelte:fragment slot="description">
<div class="qr-code-body">
{qrCodeValue}
<QRCode codeValue={qrCodeValue} />
</div>
</svelte:fragment>
<svelte:fragment slot="buttons">
<Button
type="text"
on:click={() => {
qrCodeDialogue = false;
cryptoDialogue = true;
}}>Back</Button
>
<Button type="filled" on:click={() => copyToClipboard(qrCodeValue)}>Copy Address</Button>
</svelte:fragment>
</Dialogue>
<Snackbar bind:open={addressSnackbar}>
<svelte:fragment slot="text">Address copied to clipboard</svelte:fragment>
</Snackbar>
<style lang="scss"> <style lang="scss">
main { main {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-bottom: 5rem;
// support revanced and heart thingy
section { section {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -296,55 +210,6 @@
} }
} }
.wallets {
// i just guessed this
width: clamp(200px, 75vw, 375px);
#arrow {
height: 20px;
transform: rotate(90deg);
}
button {
width: 100%;
font-size: 0.9rem;
background-color: transparent;
border: none;
color: var(--text-four);
cursor: pointer;
text-align: left;
display: flex;
justify-content: space-between;
background-color: var(--surface-seven);
padding: 0.75rem 1.25rem;
transition: filter 0.4s var(--bezier-one);
&:hover {
filter: brightness(85%);
}
}
}
.wallet-name {
display: flex;
align-items: center;
gap: 0.5rem;
// crypto icon
img {
height: 24px;
width: 24px;
}
}
.qr-code-body {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
word-break: break-word;
text-align: center;
}
.team { .team {
width: 100%; width: 100%;
display: grid; display: grid;
@@ -352,6 +217,5 @@
justify-content: space-between; justify-content: space-between;
align-items: stretch; align-items: stretch;
gap: 1rem; gap: 1rem;
margin-bottom: 4rem;
} }
</style> </style>

View File

@@ -13,7 +13,7 @@
import Query from '$lib/components/Query.svelte'; import Query from '$lib/components/Query.svelte';
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import Picture from '$lib/components/Picture.svelte'; import Picture from '$lib/components/Picture.svelte';
import Dialogue from '$lib/components/Dialogue.svelte'; import DownloadCompatibilityWarningDialog from '$layout/Dialogs/DownloadCompatibilityWarningDialog.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
const query = createQuery(queries.manager()); const query = createQuery(queries.manager());
@@ -69,21 +69,6 @@
]} ]}
/> />
<Dialogue bind:modalOpen={warningDialogue}>
<svelte:fragment slot="title">Warning</svelte:fragment>
<svelte:fragment slot="description">{warning} Do you still want to download?</svelte:fragment>
<svelte:fragment slot="buttons">
<Query {query} let:data>
<Button
type="text"
href={data.release.download_url}
on:click={() => (warningDialogue = false)}>Okay</Button
>
</Query>
<Button type="text" on:click={() => (warningDialogue = false)}>Cancel</Button>
</svelte:fragment>
</Dialogue>
<main class="wrapper center" in:fly={{ y: 10, easing: quintOut, duration: 750 }}> <main class="wrapper center" in:fly={{ y: 10, easing: quintOut, duration: 750 }}>
<h2>ReVanced <span>Manager</span></h2> <h2>ReVanced <span>Manager</span></h2>
<p>Patch your favourite apps, right on your device.</p> <p>Patch your favourite apps, right on your device.</p>
@@ -113,6 +98,8 @@
</div> </div>
</main> </main>
<DownloadCompatibilityWarningDialog bind:dialogOpen={warningDialogue} {warning} />
<style> <style>
.center { .center {
display: flex; display: flex;

View File

@@ -16,7 +16,7 @@
import PatchItem from './PatchItem.svelte'; import PatchItem from './PatchItem.svelte';
import Search from '$lib/components/Search.svelte'; import Search from '$lib/components/Search.svelte';
import FilterChip from '$lib/components/FilterChip.svelte'; import FilterChip from '$lib/components/FilterChip.svelte';
import Dialogue from '$lib/components/Dialogue.svelte'; import MobilePatchesPackagesDialog from '$layout/Dialogs/MobilePatchesPackagesDialog.svelte';
import Query from '$lib/components/Query.svelte'; import Query from '$lib/components/Query.svelte';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
@@ -77,6 +77,13 @@
}; };
onMount(update); onMount(update);
console.log(
query.subscribe((data) => {
// console.log(filterPatches(data.data?.patches, selectedPkg || '', displayedTerm));
console.log(data.data?.patches);
console.log(displayedTerm);
})
);
</script> </script>
<Head <Head
@@ -124,31 +131,16 @@
> >
{selectedPkg || 'Packages'} {selectedPkg || 'Packages'}
</FilterChip> </FilterChip>
<!-- <FilterChip check>Universal</FilterChip>
<FilterChip>Patch options</FilterChip> -->
</div> </div>
<Query {query} let:data> <Query {query} let:data>
<div class="mobile-packages-Dialogue"> <div class="mobile-packages-dialogue">
<Dialogue bind:modalOpen={mobilePackages} fullscreen> <MobilePatchesPackagesDialog
<svelte:fragment slot="title">Packages</svelte:fragment> bind:dialogOpen={mobilePackages}
<div class="mobile-packages"> bind:searchTerm
<span {data}
on:click={() => (mobilePackages = !mobilePackages)} {selectedPkg}
on:keypress={() => (mobilePackages = !mobilePackages)} />
>
<Package {selectedPkg} name="All packages" bind:searchTerm />
</span>
{#each data.packages as pkg}
<span
on:click={() => (mobilePackages = !mobilePackages)}
on:keypress={() => (mobilePackages = !mobilePackages)}
>
<Package {selectedPkg} name={pkg} bind:searchTerm />
</span>
{/each}
</div>
</Dialogue>
</div> </div>
<aside in:fly={{ y: 10, easing: quintOut, duration: 750 }}> <aside in:fly={{ y: 10, easing: quintOut, duration: 750 }}>
@@ -213,15 +205,8 @@
display: none; display: none;
} }
.mobile-packages {
margin-bottom: -1px;
overflow: hidden;
border-radius: 12px;
border: 1px solid var(--border);
}
@media (min-width: 768px) { @media (min-width: 768px) {
.mobile-packages-Dialogue { .mobile-packages-dialogue {
display: none; display: none;
} }
} }