Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 41 additions & 36 deletions src/lib/components/KenerNav.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import trackEvent from "$lib/beacon";
import MenuIcon from "@lucide/svelte/icons/menu";

let { controls }: { controls?: import("svelte").Snippet } = $props();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
let { data } = page;
const navItems: { name: string; url: string; iconURL: string }[] = data.navItems || [];
const { siteName, logo, globalPageVisibilitySettings } = data;
Expand Down Expand Up @@ -80,42 +81,46 @@
</NavigationMenu.List>
</NavigationMenu.Root>

<!-- Mobile Nav -->
{#if navItems.length > 0}
<Dropdown.Root>
<Dropdown.Trigger>
{#snippet child({ props })}
<button
{...props}
type="button"
class="{navigationMenuTriggerStyle()} hover:border-border border border-transparent bg-transparent text-xs hover:bg-transparent sm:hidden"
style="border-radius: var(--radius-3xl)"
aria-label="Open navigation menu"
>
<MenuIcon class="h-4 w-4" />
</button>
{/snippet}
</Dropdown.Trigger>
<Dropdown.Content align="end" class="w-56 rounded-3xl p-2">
{#each navItems as item (item.url)}
<Button
variant="ghost"
size="sm"
href={clientResolver(resolve, item.url)}
class="w-full justify-start rounded-full text-xs"
target={item.url.startsWith("http") ? "_blank" : undefined}
rel={item.url.startsWith("http") ? "noopener noreferrer" : undefined}
onclick={() => trackNavClick(item)}
>
{#if item.iconURL}
<img src={clientResolver(resolve, item.iconURL)} alt={item.name} class="mr-2 h-4 w-4 object-cover" />
{/if}
{item.name}
</Button>
{/each}
</Dropdown.Content>
</Dropdown.Root>
{/if}
<div class="flex items-center gap-1">
{@render controls?.()}

<!-- Mobile Nav -->
{#if navItems.length > 0}
<Dropdown.Root>
<Dropdown.Trigger>
{#snippet child({ props })}
<button
{...props}
type="button"
class="{navigationMenuTriggerStyle()} hover:border-border border border-transparent bg-transparent text-xs hover:bg-transparent sm:hidden"
style="border-radius: var(--radius-3xl)"
aria-label="Open navigation menu"
>
<MenuIcon class="h-4 w-4" />
</button>
{/snippet}
</Dropdown.Trigger>
<Dropdown.Content align="end" class="w-56 rounded-3xl p-2">
{#each navItems as item (item.url)}
<Button
variant="ghost"
size="sm"
href={clientResolver(resolve, item.url)}
class="w-full justify-start rounded-full text-xs"
target={item.url.startsWith("http") ? "_blank" : undefined}
rel={item.url.startsWith("http") ? "noopener noreferrer" : undefined}
onclick={() => trackNavClick(item)}
>
{#if item.iconURL}
<img src={clientResolver(resolve, item.iconURL)} alt={item.name} class="mr-2 h-4 w-4 object-cover" />
{/if}
{item.name}
</Button>
{/each}
</Dropdown.Content>
</Dropdown.Root>
{/if}
</div>
</div>
</div>
</div>
6 changes: 6 additions & 0 deletions src/lib/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
"%latency %metric latency": "%latency %metric latency",
"Affected Monitors (%count)": "Affected Monitors (%count)",
"All Systems Operational": "All Systems Operational",
"Auto-Refresh": "Auto-Refresh",
"auto-refresh-description": "Enable periodic page data refresh.",
"Interval": "Interval",
"Seconds": "Seconds",
"Enable": "Enable",
"Disable": "Disable",
"Average Latency": "Average Latency",
"Avg Latency": "Avg Latency",
"Back": "Back",
Expand Down
6 changes: 6 additions & 0 deletions src/lib/locales/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
"%latency %metric latency": "%latency %metric latência",
"Affected Monitors (%count)": "Monitores afetados (%count)",
"All Systems Operational": "Todos os sistemas operando",
"Auto-Refresh": "Atualização Automática",
"auto-refresh-description": "Ative para atualizar os dados da página periodicamente.",
"Interval": "Intervalo",
"Seconds": "Segundos",
"Enable": "Ativar",
"Disable": "Desativar",
"Average Latency": "Latência Média",
"Avg Latency": "Latência Média",
"Back": "Voltar",
Expand Down
45 changes: 45 additions & 0 deletions src/lib/stores/refreshStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { writable } from "svelte/store";

export type RefreshState = {
enabled: boolean;
interval: number;
lastRefresh: number | null;
};
Comment thread
lokiee0 marked this conversation as resolved.
Outdated

const defaultState: RefreshState = {
enabled: false,
interval: 30,
lastRefresh: null
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};

function createRefreshStore() {
const { subscribe, set, update } = writable<RefreshState>(defaultState);

return {
subscribe,

toggle: () =>
update((state) => ({
...state,
enabled: !state.enabled
})),

setInterval: (interval: number) =>
update((state) => ({
...state,
interval
})),

updateLastRefresh: () =>
update((state) => ({
...state,
lastRefresh: Date.now()
})),

setState: (newState: RefreshState) => set(newState),

reset: () => set(defaultState)
};
}

export const refreshStore = createRefreshStore();
120 changes: 119 additions & 1 deletion src/routes/(kener)/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
<script lang="ts">
import "../layout.css";
import "../kener.css";
import { onMount } from "svelte";
import { ModeWatcher } from "mode-watcher";
import KenerNav from "$lib/components/KenerNav.svelte";
import KenerFooter from "$lib/components/KenerFooter.svelte";
import { Button } from "$lib/components/ui/button/index.js";
import { Input } from "$lib/components/ui/input/index.js";
import * as Popover from "$lib/components/ui/popover/index.js";
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
import { Toaster } from "$lib/components/ui/sonner/index.js";
import { invalidateAll } from "$app/navigation";
import { resolve } from "$app/paths";
import { page } from "$app/state";
import clientResolver from "$lib/client/resolver.js";
import RefreshCw from "@lucide/svelte/icons/refresh-cw";
import { t } from "$lib/stores/i18n";
import { refreshStore } from "$lib/stores/refreshStore";

let { children, data } = $props();

Expand All @@ -18,6 +26,71 @@
if (params.page_path) return clientResolver(resolve, `/${params.page_path}/rss.xml`);
return clientResolver(resolve, "/rss.xml");
});

let refreshInterval = $state(60);
let refreshIntervalId: number | undefined;
let refreshInProgress = $state(false);

async function refreshPageData(): Promise<void> {
if (refreshInProgress) return;

refreshInProgress = true;

try {
await invalidateAll();
refreshStore.updateLastRefresh();
} catch (error) {
console.error("Failed to refresh page data:", error);
} finally {
refreshInProgress = false;
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function startGlobalRefresh() {
stopGlobalRefresh();
refreshIntervalId = window.setInterval(() => {
void refreshPageData();
}, $refreshStore.interval * 1000);
}

function stopGlobalRefresh() {
if (refreshIntervalId !== undefined) {
clearInterval(refreshIntervalId);
refreshIntervalId = undefined;
}
}

function saveRefreshInterval() {
refreshInterval = Math.max(5, Number(refreshInterval) || 60);
localStorage.setItem("kener-global-refresh-interval", String(refreshInterval));
refreshStore.setInterval(refreshInterval);
if ($refreshStore.enabled) startGlobalRefresh();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

function toggleGlobalRefresh() {
refreshStore.toggle();
localStorage.setItem("kener-global-refresh-enabled", String($refreshStore.enabled));

if ($refreshStore.enabled) {
startGlobalRefresh();
void refreshPageData();
} else {
stopGlobalRefresh();
}
}

onMount(() => {
const savedInterval = Number(localStorage.getItem("kener-global-refresh-interval"));
if (Number.isFinite(savedInterval) && savedInterval >= 5) refreshInterval = savedInterval;
refreshStore.setInterval(refreshInterval);

if (localStorage.getItem("kener-global-refresh-enabled") === "true") {
refreshStore.setState({ ...$refreshStore, enabled: true });
startGlobalRefresh();
}

return stopGlobalRefresh;
});
</script>

<ModeWatcher defaultMode={data.defaultSiteTheme as 'light' | 'dark' | 'system'} />
Expand Down Expand Up @@ -56,9 +129,54 @@
</style>`}
<script src={clientResolver(resolve, "/capture.js")}></script>
</svelte:head>
{#snippet refreshControls()}
<Popover.Root>
<Popover.Trigger>
{#snippet child({ props })}
<Button
{...props}
variant="ghost"
size="icon"
aria-label={$t("Auto-Refresh")}
>
<RefreshCw
class={`h-[1.2rem] w-[1.2rem] ${
refreshInProgress ? "animate-spin" : ""
}`}
/>
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-60">
<div class="grid gap-4">
<div class="space-y-2">
<h4 class="font-medium leading-none">{$t("Auto-Refresh")}</h4>
<p class="text-sm text-muted-foreground">{$t("auto-refresh-description")}</p>
</div>
<div class="grid gap-2">
<div class="grid grid-cols-3 items-center gap-4">
<label for="global-interval" class="text-xs">{$t("Interval")}</label>
<Input
id="global-interval"
type="number"
bind:value={refreshInterval}
min="5"
class="col-span-2 h-8"
placeholder={$t("Seconds")}
onchange={saveRefreshInterval}
/>
</div>
<Button onclick={toggleGlobalRefresh} size="sm">
{$refreshStore.enabled ? $t("Disable") : $t("Enable")}
</Button>
</div>
</div>
</Popover.Content>
</Popover.Root>
{/snippet}
<main class="kener-public status-page-app">
<!-- Nav -->
<KenerNav />
<KenerNav controls={refreshControls} />
<!-- Body -->
<div class="mx-auto max-w-5xl px-4 pt-18">
<Tooltip.Provider>
Expand Down