Skip to content

Commit

Permalink
feat: toast (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
peterpeterparker authored Feb 2, 2025
1 parent 0e7759d commit b3957b0
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 0 deletions.
141 changes: 141 additions & 0 deletions src/lib/components/ui/Toast.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<script lang="ts">
import { isNullish } from '@dfinity/utils';
import { onDestroy, untrack } from 'svelte';
import { fade, fly } from 'svelte/transition';
import IconClose from '$lib/components/icons/IconClose.svelte';
import { toasts } from '$lib/stores/toasts.store';
import type { ToastLevel, ToastMsg } from '$lib/types/toast';
interface Props {
msg: ToastMsg;
}
let { msg }: Props = $props();
const close = () => toasts.hide();
let text: string = $derived(msg.text);
let level: ToastLevel = $derived(msg.level);
let detail: string | undefined = $derived(msg.detail);
let timer = $state<number | undefined>(undefined);
$effect(() => {
const { duration } = msg;
untrack(() => {
if (isNullish(duration) || duration <= 0) {
return;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore NodeJS.timeout vs number
timer = setTimeout(close, duration);
});
});
onDestroy(() => clearTimeout(timer));
let reorgDetail: string | undefined = $state(undefined);
$effect(() => {
if (isNullish(detail)) {
reorgDetail = undefined;
return;
}
// Present the message we throw in the backend first
const trapKeywords = 'trapped explicitly:' as const;
if (!detail.includes(trapKeywords)) {
reorgDetail = detail;
return;
}
const splits = detail.split(trapKeywords);
const last = splits.splice(-1);
reorgDetail = `${last[0]?.trim() ?? ''}${
splits.length > 0 ? ` | Stacktrace: ${splits.join('').trim()}` : ''
}`;
});
</script>

<div
role="dialog"
class="toast"
class:error={level === 'error'}
class:warn={level === 'warn'}
in:fly={{ y: 100, duration: 200 }}
out:fade={{ delay: 100 }}
>
<div class="toast-scroll">
<p title={text}>
{text}{reorgDetail ? ` | ${reorgDetail}` : ''}
</p>
</div>

<button class="text" onclick={close} aria-label="Close"><IconClose /></button>
</div>

<style lang="scss">
@use '../../themes/mixins/text';
.toast {
display: flex;
justify-content: space-between;
align-items: center;
position: fixed;
bottom: calc(6 * var(--padding));
left: 50%;
transform: translate(-50%, 0);
width: calc(100% - (8 * var(--padding)));
padding: var(--padding) calc(var(--padding) * 2);
box-sizing: border-box;
z-index: calc(var(--z-index) + 999);
background: var(--color-secondary);
color: var(--color-secondary-contrast);
border-radius: var(--padding);
border: 4px solid black;
@media (min-width: 768px) {
max-width: 576px;
}
&.error {
background: var(--color-error);
color: var(--color-error-contrast);
}
&.warn {
background: var(--color-warning);
color: var(--color-warning-contrast);
}
}
.toast-scroll {
overflow-y: auto;
max-height: calc(16px * 3 * 1.3);
// Workaround to get rid of the redundant scrollbar (even when there is enough space).
line-height: normal;
direction: rtl;
&::-webkit-scrollbar-thumb {
background: var(--color-secondary-contrast);
}
p {
direction: ltr;
margin: 0;
padding: 0 var(--padding);
}
}
</style>
8 changes: 8 additions & 0 deletions src/lib/components/ui/Toasts.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script lang="ts">
import Toast from './Toast.svelte';
import { toasts } from '$lib/stores/toasts.store';
</script>

{#if $toasts.length > 0}
<Toast msg={$toasts[0]} />
{/if}
45 changes: 45 additions & 0 deletions src/lib/stores/toasts.store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { ToastMsg } from '$lib/types/toast';
import { errorDetailToString } from '$lib/utils/error.utils';
import { writable } from 'svelte/store';

const initToastsStore = () => {
const { subscribe, update } = writable<ToastMsg[]>([]);

return {
subscribe,

error({ text, detail }: { text: string; detail?: unknown }) {
console.error(text, detail);
update((messages: ToastMsg[]) => [
...messages,
{ text, level: 'error', detail: errorDetailToString(detail) }
]);
},

show(msg: ToastMsg) {
update((messages: ToastMsg[]) => [...messages, msg]);
},

warn(text: string) {
this.show({
text,
level: 'warn',
duration: 2000
});
},

success(text: string) {
this.show({
text,
level: 'info',
duration: 2000
});
},

hide() {
update((messages: ToastMsg[]) => messages.slice(1));
}
};
};

export const toasts = initToastsStore();
8 changes: 8 additions & 0 deletions src/lib/types/toast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type ToastLevel = 'info' | 'warn' | 'error';

export interface ToastMsg {
text: string;
level: ToastLevel;
detail?: string;
duration?: number;
}
2 changes: 2 additions & 0 deletions src/lib/utils/error.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const errorDetailToString = (err: unknown): string | undefined =>
typeof err === 'string' ? err : err instanceof Error ? err.message : undefined;
2 changes: 2 additions & 0 deletions src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import Busy from '$lib/components/ui/Busy.svelte';
import { initSatellite } from '@junobuild/core';
import { CONTAINER, SATELLITE_ID } from '$lib/constants/constants';
import Toasts from "$lib/components/ui/Toasts.svelte";
onMount(async () => {
await initSatellite({
Expand All @@ -29,3 +30,4 @@

<Add />
<Busy />
<Toasts />

0 comments on commit b3957b0

Please sign in to comment.