Convert multiple pages into SPA
This commit is contained in:
38
svelte/src/wrap/Bookmarks.svelte
Normal file
38
svelte/src/wrap/Bookmarks.svelte
Normal file
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { bookmark_del, bookmarks_store } from "lib/Bookmarks";
|
||||
import { highlight_current_page } from "lib/HighlightCurrentPage";
|
||||
</script>
|
||||
|
||||
{#each $bookmarks_store as bookmark}
|
||||
<div class="row">
|
||||
<a class="button" href="/d/{bookmark.id}" use:highlight_current_page>
|
||||
<i class="icon">{bookmark.icon}</i>
|
||||
<span class="hide_small">{bookmark.label}</span>
|
||||
</a>
|
||||
<button on:click={() => bookmark_del(bookmark.id)}>
|
||||
<i class="icon">delete</i>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<style>
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.row>a {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.row>button {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.button {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
@media(max-width: 1000px) {
|
||||
.hide_small {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
166
svelte/src/wrap/MainMenu.svelte
Normal file
166
svelte/src/wrap/MainMenu.svelte
Normal file
@@ -0,0 +1,166 @@
|
||||
<script lang="ts">
|
||||
import { highlight_current_page } from "lib/HighlightCurrentPage";
|
||||
import { user } from "lib/UserStore";
|
||||
import Euro from "util/Euro.svelte";
|
||||
import { formatDataVolume } from "util/Formatting";
|
||||
import Router from "wrap/Router.svelte";
|
||||
import Bookmarks from "./Bookmarks.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { fs_get_node } from "lib/FilesystemAPI";
|
||||
import { css_from_path } from "filesystem/edit_window/Branding";
|
||||
import { loading_run, loading_store } from "lib/Loading";
|
||||
import Spinner from "util/Spinner.svelte";
|
||||
|
||||
onMount(async () => {
|
||||
await loading_run(async () => {
|
||||
const root = await fs_get_node("/me")
|
||||
document.documentElement.style = css_from_path(root.path)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="nav_container">
|
||||
<nav class="nav">
|
||||
{#if $user.username !== undefined && $user.username !== ""}
|
||||
<div class="username hide_small">
|
||||
{$user.username}
|
||||
</div>
|
||||
<div class="stats_table hide_small">
|
||||
<div>Subscription</div>
|
||||
<div>{$user.subscription.name}</div>
|
||||
|
||||
{#if $user.subscription.type === "prepaid"}
|
||||
<div>Credit</div>
|
||||
<div><Euro amount={$user.balance_micro_eur}/></div>
|
||||
{/if}
|
||||
|
||||
<div>Storage used</div>
|
||||
<div>{formatDataVolume($user.filesystem_storage_used, 3)}</div>
|
||||
<div>Transfer used</div>
|
||||
<div>{formatDataVolume($user.monthly_transfer_used, 3)}</div>
|
||||
</div>
|
||||
<div class="separator hide_small"></div>
|
||||
{/if}
|
||||
|
||||
<a class="button" href="/" use:highlight_current_page>
|
||||
<i class="icon">home</i>
|
||||
<span class="hide_small">Home</span>
|
||||
</a>
|
||||
{#if !$user.username}
|
||||
<a class="button" href="/login" use:highlight_current_page>
|
||||
<i class="icon">login</i>
|
||||
<span class="hide_small">Login</span>
|
||||
</a>
|
||||
<a class="button" href="/register" use:highlight_current_page>
|
||||
<i class="icon">how_to_reg</i>
|
||||
<span class="hide_small">Register</span>
|
||||
</a>
|
||||
{:else}
|
||||
<a class="button" href="/user" use:highlight_current_page>
|
||||
<i class="icon">dashboard</i>
|
||||
<span class="hide_small">Dashboard</span>
|
||||
</a>
|
||||
<a class="button" href="/d/me" use:highlight_current_page>
|
||||
<i class="icon">folder</i>
|
||||
<span class="hide_small">Filesystem</span>
|
||||
</a>
|
||||
{#if $user.is_admin}
|
||||
<a class="button" href="/admin" use:highlight_current_page>
|
||||
<i class="icon">admin_panel_settings</i>
|
||||
<span class="hide_small">Admin Panel</span>
|
||||
</a>
|
||||
{/if}
|
||||
{/if}
|
||||
<a class="button" href="/speedtest" use:highlight_current_page>
|
||||
<i class="icon">speed</i>
|
||||
<span class="hide_small">Speedtest</span>
|
||||
</a>
|
||||
<a class="button" href="/appearance" use:highlight_current_page>
|
||||
<i class="icon">palette</i>
|
||||
<span class="hide_small">Themes</span>
|
||||
</a>
|
||||
|
||||
<div class="separator hide_small"></div>
|
||||
<Bookmarks/>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="page">
|
||||
<Router/>
|
||||
</div>
|
||||
|
||||
|
||||
{#if $loading_store !== 0}
|
||||
<div class="spinner">
|
||||
<Spinner/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
:global(body) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
color: var(--body_text_color);
|
||||
background-image: var(--background_image);
|
||||
background-color: var(--background_pattern_color);
|
||||
background-size: var(--background_image_size, initial);
|
||||
background-position: var(--background_image_position, initial);
|
||||
background-repeat: var(--background_image_repeat, repeat);
|
||||
background-attachment: fixed;
|
||||
}
|
||||
.nav_container {
|
||||
flex: 0 0 auto;
|
||||
border-right: 1px solid var(--separator);
|
||||
background: var(--shaded_background);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 15em;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
||||
}
|
||||
.nav > .button {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.page {
|
||||
flex: 1 1 auto;
|
||||
overflow-x: hidden;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.separator {
|
||||
height: 1px;
|
||||
margin: 2px 0;
|
||||
width: 100%;
|
||||
background-color: var(--separator);
|
||||
}
|
||||
.username {
|
||||
text-align: center;
|
||||
margin: 3px;
|
||||
}
|
||||
.stats_table {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
gap: 0.2em 1em;
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
@media(max-width: 1000px) {
|
||||
.hide_small {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.spinner {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
}
|
||||
</style>
|
||||
1
svelte/src/wrap/NotFound.svelte
Normal file
1
svelte/src/wrap/NotFound.svelte
Normal file
@@ -0,0 +1 @@
|
||||
Page not found
|
||||
154
svelte/src/wrap/Router.svelte
Normal file
154
svelte/src/wrap/Router.svelte
Normal file
@@ -0,0 +1,154 @@
|
||||
<script lang="ts">
|
||||
import { onMount, tick } from "svelte";
|
||||
import HomePage from "home_page/HomePage.svelte";
|
||||
import LoginRouter from "login/Router.svelte";
|
||||
import HomeRouter from "user_home/Router.svelte";
|
||||
import AdminRouter from "admin_panel/Router.svelte";
|
||||
import Filesystem from "filesystem/Filesystem.svelte";
|
||||
import NotFound from "./NotFound.svelte";
|
||||
import SpeedtestPage from "speedtest/SpeedtestPage.svelte";
|
||||
import Appearance from "pages/Appearance.svelte";
|
||||
import Footer from "layout/Footer.svelte";
|
||||
import { current_page_store, type Tab } from "./RouterStore";
|
||||
import { get_user, type User } from "lib/PixeldrainAPI";
|
||||
|
||||
export let pages: Tab[] = [
|
||||
{
|
||||
path: "/",
|
||||
title: "Home",
|
||||
component: HomePage,
|
||||
}, {
|
||||
path: "/login",
|
||||
title: "Login",
|
||||
component: LoginRouter,
|
||||
}, {
|
||||
path: "/register",
|
||||
title: "Register",
|
||||
component: LoginRouter,
|
||||
}, {
|
||||
path: "/user",
|
||||
prefix: "/user/",
|
||||
title: "Dashboard",
|
||||
component: HomeRouter,
|
||||
login: true,
|
||||
}, {
|
||||
path: "/d/me",
|
||||
prefix: "/d/",
|
||||
title: "Filesystem",
|
||||
component: Filesystem,
|
||||
footer: false,
|
||||
login: true,
|
||||
}, {
|
||||
path: "/admin",
|
||||
prefix: "/admin/",
|
||||
title: "Admin Panel",
|
||||
component: AdminRouter,
|
||||
login: true,
|
||||
}, {
|
||||
path: "/speedtest",
|
||||
title: "Speedtest",
|
||||
component: SpeedtestPage,
|
||||
}, {
|
||||
path: "/appearance",
|
||||
title: "Appearance",
|
||||
component: Appearance,
|
||||
},
|
||||
]
|
||||
|
||||
let user: User = null
|
||||
onMount(async () => {
|
||||
user = await get_user()
|
||||
load_page(window.location.pathname, false)
|
||||
})
|
||||
|
||||
let current_page: Tab = null
|
||||
|
||||
const load_page = (pathname: string, history: boolean): boolean => {
|
||||
console.debug("Navigating to page", pathname, "log history:", history)
|
||||
|
||||
const path_decoded = decodeURI(pathname)
|
||||
let page_by_path: Tab = null
|
||||
let page_by_prefix: Tab = null
|
||||
for (const page of pages) {
|
||||
if (path_decoded === page.path) {
|
||||
page_by_path = page
|
||||
}
|
||||
if (page.prefix !== undefined && path_decoded.startsWith(page.prefix)) {
|
||||
page_by_prefix = page
|
||||
}
|
||||
}
|
||||
|
||||
if (page_by_path !== null) {
|
||||
current_page = page_by_path
|
||||
} else if (page_by_prefix !== null) {
|
||||
current_page = page_by_prefix
|
||||
} else {
|
||||
current_page = {
|
||||
path: "",
|
||||
title: "Not Found",
|
||||
component: NotFound,
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// If this page requires login, and the user is not logged in, then we
|
||||
// redirect the user to the login page
|
||||
if (current_page.login === true && (user.username === "" || user.username === undefined)) {
|
||||
console.debug("User is not logged in, redirecting to login page", user)
|
||||
return load_page("/login", true)
|
||||
}
|
||||
|
||||
window.document.title = current_page.title+" / FNX"
|
||||
console.debug("Page", current_page)
|
||||
|
||||
if(history) {
|
||||
window.history.pushState({}, window.document.title, pathname)
|
||||
}
|
||||
|
||||
// The current_page_store updates all the listening pages for navigation
|
||||
// events. We first wait for a tick so that the current page gets unmounted
|
||||
// before sending the event. That way a stale page will not get events which
|
||||
// are not meant for them
|
||||
tick().then(() => {
|
||||
current_page_store.set(current_page)
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const click = (e: MouseEvent) => {
|
||||
const origin = (e.target as Element).closest("a");
|
||||
|
||||
if (origin === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const url = URL.parse(origin.href)
|
||||
if (window.location.host !== url.host) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log("Caught link click to", url.pathname);
|
||||
|
||||
// Try to load the page, if the page was found we cancel the browser
|
||||
// navigation event so we can handle it ourselves
|
||||
if (load_page(url.pathname, true)) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
const popstate = (e: PopStateEvent) => {
|
||||
load_page(window.location.pathname, false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:document on:click={click}/>
|
||||
<svelte:window on:popstate={popstate}/>
|
||||
|
||||
{#if current_page !== null}
|
||||
<svelte:component this={current_page.component} />
|
||||
|
||||
{#if current_page.footer === undefined || current_page.footer === true}
|
||||
<Footer/>
|
||||
{/if}
|
||||
{/if}
|
||||
12
svelte/src/wrap/RouterStore.ts
Normal file
12
svelte/src/wrap/RouterStore.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { ComponentType } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
export type Tab = {
|
||||
path: string,
|
||||
prefix?: string,
|
||||
title: string,
|
||||
component?: ComponentType,
|
||||
footer?: boolean,
|
||||
login?: boolean,
|
||||
};
|
||||
export let current_page_store = writable({} as Tab);
|
||||
Reference in New Issue
Block a user