Lots of layout changes
This commit is contained in:
@@ -37,3 +37,9 @@
|
|||||||
--jellyfin-blue: #00a4dc;
|
--jellyfin-blue: #00a4dc;
|
||||||
--youtube-red: #ff0000;
|
--youtube-red: #ff0000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 640px) {
|
||||||
|
:root {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import Services from '$lib/services.json'
|
import Services from '$lib/services.json'
|
||||||
import IconButton from '$lib/components/utility/iconButton.svelte'
|
import IconButton from '$lib/components/utility/iconButton.svelte'
|
||||||
|
import { backgroundImage } from '$lib/utils/stores.js'
|
||||||
|
|
||||||
const iconClasses = {
|
const iconClasses = {
|
||||||
song: 'fa-solid fa-music',
|
song: 'fa-solid fa-music',
|
||||||
@@ -29,7 +30,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a id="card-wrapper" on:mousedown|preventDefault on:mousemove={(event) => rotateCard(event)} on:mouseleave={() => (card.style.transform = null)} href="/details?id={mediaData.id}&service={mediaData.connectionId}">
|
<a id="card-wrapper" on:mousedown|preventDefault on:mousemove={(event) => rotateCard(event)} on:mouseleave={() => (card.style.transform = null)} href="/details?id={mediaData.id}&service={mediaData.connectionId}">
|
||||||
<div bind:this={card} id="card" class="relative h-60 transition-all duration-200 ease-out">
|
<div bind:this={card} id="card" class="relative h-56 transition-all duration-200 ease-out">
|
||||||
{#if mediaData.image}
|
{#if mediaData.image}
|
||||||
<img id="card-image" class="h-full max-w-none rounded-lg transition-all" src={mediaData.image} alt="{mediaData.name} thumbnail" />
|
<img id="card-image" class="h-full max-w-none rounded-lg transition-all" src={mediaData.image} alt="{mediaData.name} thumbnail" />
|
||||||
{:else}
|
{:else}
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<div bind:this={cardGlare} id="card-glare" class="absolute top-0 grid h-full w-full place-items-center rounded-lg opacity-0 transition-opacity duration-200 ease-out">
|
<div bind:this={cardGlare} id="card-glare" class="absolute top-0 grid h-full w-full place-items-center rounded-lg opacity-0 transition-opacity duration-200 ease-out">
|
||||||
<span class="relative h-12">
|
<span class="relative h-12">
|
||||||
<IconButton on:click={() => console.log(`Play ${mediaData.name}`)}>
|
<IconButton on:click={() => ($backgroundImage = mediaData.image)}>
|
||||||
<i slot="icon" class="fa-solid fa-play text-xl" />
|
<i slot="icon" class="fa-solid fa-play text-xl" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -3,13 +3,31 @@
|
|||||||
export let cardDataList
|
export let cardDataList
|
||||||
|
|
||||||
import Card from '$lib/components/media/mediaCard.svelte'
|
import Card from '$lib/components/media/mediaCard.svelte'
|
||||||
|
import IconButton from '$lib/components/utility/iconButton.svelte'
|
||||||
|
|
||||||
|
let scrollable,
|
||||||
|
scrollpos = 0
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="overflow-visible">
|
<section>
|
||||||
{#if header}
|
<div class="flex h-10 items-center justify-between">
|
||||||
<h1 class="text-4xl"><strong>{header}</strong></h1>
|
{#if header}
|
||||||
{/if}
|
<h1 class="text-4xl"><strong>{header}</strong></h1>
|
||||||
<div class="no-scrollbar flex gap-6 overflow-y-hidden overflow-x-scroll py-4">
|
{/if}
|
||||||
|
<div class="flex h-full gap-2">
|
||||||
|
<IconButton disabled={scrollpos < 0.01} on:click={() => (scrollable.scrollLeft -= scrollable.clientWidth)}>
|
||||||
|
<i slot="icon" class="fa-solid fa-angle-left" />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton disabled={scrollpos > 0.99} on:click={() => (scrollable.scrollLeft += scrollable.clientWidth)}>
|
||||||
|
<i slot="icon" class="fa-solid fa-angle-right" />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
bind:this={scrollable}
|
||||||
|
on:scroll={() => (scrollpos = scrollable.scrollLeft / (scrollable.scrollWidth - scrollable.clientWidth))}
|
||||||
|
class="no-scrollbar flex gap-6 overflow-y-hidden overflow-x-scroll scroll-smooth py-4"
|
||||||
|
>
|
||||||
{#each cardDataList as cardData}
|
{#each cardDataList as cardData}
|
||||||
<Card mediaData={cardData} />
|
<Card mediaData={cardData} />
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
{#if show}
|
{#if show}
|
||||||
<div in:fly={{ duration: 300, x: 500 }} out:slide={{ duration: 300, axis: 'y' }} class="py-1">
|
<div in:fly={{ duration: 300, x: 500 }} out:slide={{ duration: 300, axis: 'y' }} class="py-1">
|
||||||
<div class="flex gap-1 overflow-hidden rounded-md">
|
<div class="flex gap-1 overflow-hidden rounded-md">
|
||||||
<div class="flex min-h-[3.5rem] w-full items-center px-4 py-2 {bgColors[alertType]}">
|
<div class="flex w-full items-center p-4 {bgColors[alertType]}">
|
||||||
{alertMessage}
|
{alertMessage}
|
||||||
</div>
|
</div>
|
||||||
<button class="w-16 {bgColors[alertType]}" on:click={() => triggerClose()}>
|
<button class="w-16 {bgColors[alertType]}" on:click={() => triggerClose()}>
|
||||||
|
|||||||
@@ -27,4 +27,4 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={alertBox} class="fixed right-4 top-4 z-50 max-h-screen w-full max-w-sm overflow-hidden"></div>
|
<div bind:this={alertBox} class="fixed right-0 top-0 z-50 max-h-screen w-full max-w-sm overflow-hidden p-4"></div>
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
<script>
|
<script>
|
||||||
|
export let disabled = false
|
||||||
|
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button class="relative grid aspect-square h-full place-items-center transition-transform duration-75 active:scale-90" on:click|preventDefault={() => dispatch('click')}>
|
<button
|
||||||
|
class:disabled
|
||||||
|
class="relative grid aspect-square h-full place-items-center transition-transform duration-75 active:scale-90 {disabled ? 'text-neutral-600' : ''}"
|
||||||
|
on:click|preventDefault={() => dispatch('click')}
|
||||||
|
{disabled}
|
||||||
|
>
|
||||||
<slot name="icon" />
|
<slot name="icon" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
button::before {
|
button:not(.disabled)::before {
|
||||||
content: '';
|
content: '';
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
@@ -19,14 +26,14 @@
|
|||||||
transition-duration: 200ms;
|
transition-duration: 200ms;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
button:hover::before {
|
button:not(.disabled):hover::before {
|
||||||
width: 130%;
|
width: 130%;
|
||||||
height: 130%;
|
height: 130%;
|
||||||
}
|
}
|
||||||
button :global(> :first-child) {
|
button :global(> :first-child) {
|
||||||
transition: color 200ms;
|
transition: color 200ms;
|
||||||
}
|
}
|
||||||
button:hover :global(> :first-child) {
|
button:not(.disabled):hover :global(> :first-child) {
|
||||||
color: var(--lazuli-primary);
|
color: var(--lazuli-primary);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,38 +1,48 @@
|
|||||||
<script>
|
<script>
|
||||||
import IconButton from './iconButton.svelte'
|
import IconButton from './iconButton.svelte'
|
||||||
import SearchBar from './searchBar.svelte'
|
import { goto } from '$app/navigation'
|
||||||
import { goto, afterNavigate } from '$app/navigation'
|
|
||||||
import { page } from '$app/stores'
|
import { page } from '$app/stores'
|
||||||
import { fade } from 'svelte/transition'
|
|
||||||
|
|
||||||
let previousPage = null
|
|
||||||
afterNavigate(({ from }) => {
|
|
||||||
if (from) previousPage = from.url
|
|
||||||
})
|
|
||||||
|
|
||||||
let windowY = 0
|
let windowY = 0
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window bind:scrollY={windowY} />
|
<svelte:window bind:scrollY={windowY} />
|
||||||
<nav id="navbar" class="sticky left-0 top-0 isolate z-10 h-16">
|
<nav class="sticky top-0 z-10 flex items-center justify-between px-8 duration-300" class:background-active={windowY > 0}>
|
||||||
<div class="flex h-full items-center justify-between gap-6 px-6">
|
<section class="flex h-full">
|
||||||
<div class="w-full max-w-2xl">
|
<IconButton>
|
||||||
<SearchBar />
|
<i slot="icon" class="fa-solid fa-gear" />
|
||||||
</div>
|
</IconButton>
|
||||||
<div class="flex h-full gap-4 py-4">
|
{#if $page.url.pathname !== '/'}
|
||||||
{#if previousPage && $page.url.pathname !== '/'}
|
<IconButton on:click={() => goto('/')}>
|
||||||
<IconButton on:click={() => history.back()}>
|
<i slot="icon" class="fa-solid fa-house" />
|
||||||
<i slot="icon" class="fa-solid fa-arrow-left text-xl" />
|
</IconButton>
|
||||||
</IconButton>
|
<IconButton on:click={() => history.back()}>
|
||||||
{/if}
|
<i slot="icon" class="fa-solid fa-arrow-left" />
|
||||||
{#if $page.url.pathname !== '/'}
|
</IconButton>
|
||||||
<IconButton on:click={() => goto('/')}>
|
{/if}
|
||||||
<i slot="icon" class="fa-solid fa-house text-xl" />
|
</section>
|
||||||
</IconButton>
|
<section class="flex h-full">
|
||||||
{/if}
|
<IconButton>
|
||||||
</div>
|
<i slot="icon" class="fa-solid fa-magnifying-glass" />
|
||||||
</div>
|
</IconButton>
|
||||||
{#if windowY > 0}
|
<IconButton>
|
||||||
<div transition:fade={{ duration: 150 }} id="navbar-background" class="absolute left-0 top-0 -z-10 h-full w-full bg-neutral-925" />
|
<i slot="icon" class="fa-solid fa-user" />
|
||||||
{/if}
|
</IconButton>
|
||||||
|
</section>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
nav {
|
||||||
|
height: 64px;
|
||||||
|
padding-top: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
nav.background-active {
|
||||||
|
background-color: rgb(10, 10, 10);
|
||||||
|
}
|
||||||
|
section {
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
27
src/lib/components/utility/sidebar.svelte
Normal file
27
src/lib/components/utility/sidebar.svelte
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<script>
|
||||||
|
export let open = false
|
||||||
|
|
||||||
|
import IconButton from './iconButton.svelte'
|
||||||
|
import { slide } from 'svelte/transition'
|
||||||
|
|
||||||
|
export const toggleOpen = () => (open = !open)
|
||||||
|
|
||||||
|
let sidebar
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:document
|
||||||
|
on:mouseup={(event) => {
|
||||||
|
if (sidebar && open) {
|
||||||
|
if (!sidebar.contains(event.target)) open = false
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{#if open}
|
||||||
|
<section bind:this={sidebar} transition:slide={{ axis: 'x' }} class="fixed left-0 top-0 z-20 h-full w-full max-w-sm bg-slate-600 p-2" style="width: calc(100% - 4rem);">
|
||||||
|
<div class="float-right h-8">
|
||||||
|
<IconButton on:click={toggleOpen}>
|
||||||
|
<i slot="icon" class="fa-solid fa-x" />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
import { writable } from 'svelte/store'
|
import { writable } from 'svelte/store'
|
||||||
|
|
||||||
export const newestAlert = writable([null, null])
|
export const newestAlert = writable([null, null])
|
||||||
|
|
||||||
|
export const backgroundImage = writable(null)
|
||||||
@@ -1 +1,8 @@
|
|||||||
export const trailingSlash = 'never'
|
export const trailingSlash = 'never'
|
||||||
|
|
||||||
|
/** @type {import('./$types').PageServerLoad} */
|
||||||
|
export const load = ({ url }) => {
|
||||||
|
return {
|
||||||
|
url: url.pathname,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
<script>
|
<script>
|
||||||
import '../app.css'
|
import '../app.css'
|
||||||
import '@fortawesome/fontawesome-free/css/all.min.css'
|
import '@fortawesome/fontawesome-free/css/all.min.css'
|
||||||
import Navbar from '$lib/components/utility/navbar.svelte'
|
|
||||||
import AlertBox from '$lib/components/utility/alertBox.svelte'
|
import AlertBox from '$lib/components/utility/alertBox.svelte'
|
||||||
import { page } from '$app/stores'
|
import SubLayouts from './subLayouts.svelte'
|
||||||
import { newestAlert } from '$lib/stores/alertStore.js'
|
import { newestAlert, backgroundImage } from '$lib/utils/stores.js'
|
||||||
import { fade } from 'svelte/transition'
|
import { fade } from 'svelte/transition'
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
|
|
||||||
|
export let data
|
||||||
|
|
||||||
let alertBox
|
let alertBox
|
||||||
$: addAlert($newestAlert)
|
$: addAlert($newestAlert)
|
||||||
|
|
||||||
@@ -16,35 +17,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Might want to change this functionallity to a fetch/preload/await for the image
|
// Might want to change this functionallity to a fetch/preload/await for the image
|
||||||
const backgroundImage = 'https://www.gstatic.com/youtube/media/ytm/images/sbg/wsbg@4000x2250.png' // <-- Default youtube music background
|
const ytBg = 'https://www.gstatic.com/youtube/media/ytm/images/sbg/wsbg@4000x2250.png' // <-- Default youtube music background
|
||||||
let loaded = false
|
let loaded = false
|
||||||
onMount(() => (loaded = true))
|
onMount(() => (loaded = true))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="h-screen font-notoSans text-white">
|
<div class="no-scrollbar h-screen font-notoSans text-white">
|
||||||
<div class="fixed isolate -z-10 h-full w-full bg-black">
|
<div class="fixed isolate -z-10 h-full w-screen bg-black">
|
||||||
<!-- This whole bg is a complete copy of ytmusic, design own at some point (Place for customization w/ album art etc?) (EDIT: Ok, it looks SICK with album art!) -->
|
<!-- This whole bg is a complete copy of ytmusic, design own at some point (Place for customization w/ album art etc?) (EDIT: Ok, it looks SICK with album art!) -->
|
||||||
<div id="background-gradient" class="absolute z-10 h-1/2 w-full bg-cover" />
|
<div id="background-gradient" class="absolute z-10 h-1/2 w-full bg-cover" />
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<!-- May want to add a small blur filter in the event that the album/song image is below a certain resolution -->
|
{#key $backgroundImage}
|
||||||
<img id="background-image" src={backgroundImage} alt="" class="h-1/2 w-full object-cover blur-xl" in:fade={{ duration: 1000 }} />
|
<!-- May want to add a small blur filter in the event that the album/song image is below a certain resolution -->
|
||||||
|
<img id="background-image" src={$backgroundImage ? $backgroundImage : ytBg} alt="" class="absolute h-1/2 w-full object-cover blur-lg" transition:fade={{ duration: 1000 }} />
|
||||||
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if $page.url.pathname === '/login'}
|
<SubLayouts currentPage={data.url}>
|
||||||
<slot />
|
<slot slot="innerContent" />
|
||||||
{:else}
|
</SubLayouts>
|
||||||
<div class="grid h-full grid-cols-[5rem_auto]">
|
<AlertBox bind:this={alertBox} />
|
||||||
<div class="h-full bg-slate-600" />
|
</div>
|
||||||
<div class="grid h-full grid-rows-[4rem_auto] gap-8">
|
|
||||||
<Navbar />
|
|
||||||
<div class="no-scrollbar overflow-y-scroll">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<AlertBox bind:this={alertBox} />
|
|
||||||
{/if}
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#background-gradient {
|
#background-gradient {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { SECRET_INTERNAL_API_KEY } from '$env/static/private'
|
|||||||
export const prerender = false
|
export const prerender = false
|
||||||
|
|
||||||
/** @type {import('./$types').PageServerLoad} */
|
/** @type {import('./$types').PageServerLoad} */
|
||||||
export const load = async ({ locals, fetch }) => {
|
export const load = async ({ locals, fetch, url }) => {
|
||||||
const recommendationResponse = await fetch(`/api/user/recommendations?userId=${locals.userId}&limit=10`, {
|
const recommendationResponse = await fetch(`/api/user/recommendations?userId=${locals.userId}&limit=10`, {
|
||||||
headers: {
|
headers: {
|
||||||
apikey: SECRET_INTERNAL_API_KEY,
|
apikey: SECRET_INTERNAL_API_KEY,
|
||||||
@@ -13,6 +13,7 @@ export const load = async ({ locals, fetch }) => {
|
|||||||
const { recommendations, errors } = recommendationsData
|
const { recommendations, errors } = recommendationsData
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
url: url.pathname,
|
||||||
user: locals.user,
|
user: locals.user,
|
||||||
recommendations,
|
recommendations,
|
||||||
fetchingErrors: errors,
|
fetchingErrors: errors,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import { newestAlert } from '$lib/stores/alertStore.js'
|
import { newestAlert } from '$lib/utils/stores.js'
|
||||||
import ScrollableCardMenu from '$lib/components/media/scrollableCardMenu.svelte'
|
import ScrollableCardMenu from '$lib/components/media/scrollableCardMenu.svelte'
|
||||||
|
|
||||||
export let data
|
export let data
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<p class="text-neutral-400">Click the menu in the top left corner and go to Settings > Connections to link to your accounts</p>
|
<p class="text-neutral-400">Click the menu in the top left corner and go to Settings > Connections to link to your accounts</p>
|
||||||
</main>
|
</main>
|
||||||
{:else}
|
{:else}
|
||||||
<main id="recommendations-wrapper" class="h-screen px-8">
|
<main id="recommendations-wrapper" class="h-[200vh] w-full">
|
||||||
<ScrollableCardMenu header={'Listen Again'} cardDataList={data.recommendations} />
|
<ScrollableCardMenu header={'Listen Again'} cardDataList={data.recommendations} />
|
||||||
</main>
|
</main>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
1
src/routes/library/+page.svelte
Normal file
1
src/routes/library/+page.svelte
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<h1>This is where library items will go</h1>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import { enhance } from '$app/forms'
|
import { enhance } from '$app/forms'
|
||||||
import { goto } from '$app/navigation'
|
import { goto } from '$app/navigation'
|
||||||
import { fade } from 'svelte/transition'
|
import { fade } from 'svelte/transition'
|
||||||
import { newestAlert } from '$lib/stores/alertStore.js'
|
import { newestAlert } from '$lib/utils/stores.js'
|
||||||
|
|
||||||
export let data
|
export let data
|
||||||
|
|
||||||
@@ -77,11 +77,7 @@
|
|||||||
Sign In
|
Sign In
|
||||||
<i class="fa-solid fa-right-to-bracket ml-1" />
|
<i class="fa-solid fa-right-to-bracket ml-1" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button formaction="?/newUser" class="h-12 w-1/3 rounded-md transition-all active:scale-[97%]" style="background-color: {formMode === 'newUser' ? 'var(--lazuli-primary)' : '#262626'};">
|
||||||
formaction="?/newUser"
|
|
||||||
class="h-12 w-1/3 rounded-md transition-all active:scale-[97%]"
|
|
||||||
style="background-color: {formMode === 'newUser' ? 'var(--lazuli-primary)' : '#262626'};"
|
|
||||||
>
|
|
||||||
Create New User
|
Create New User
|
||||||
<i class="fa-solid fa-user-plus ml-1" />
|
<i class="fa-solid fa-user-plus ml-1" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
1
src/routes/playlist/+page.svelte
Normal file
1
src/routes/playlist/+page.svelte
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<main>Hello this is where playlist go</main>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
import { JellyfinUtils } from '$lib/utils/utils'
|
import { JellyfinUtils } from '$lib/utils/utils'
|
||||||
import Services from '$lib/services.json'
|
import Services from '$lib/services.json'
|
||||||
import JellyfinAuthBox from './jellyfinAuthBox.svelte'
|
import JellyfinAuthBox from './jellyfinAuthBox.svelte'
|
||||||
import { newestAlert } from '$lib/stores/alertStore.js'
|
import { newestAlert } from '$lib/utils/stores.js'
|
||||||
import IconButton from '$lib/components/utility/iconButton.svelte'
|
import IconButton from '$lib/components/utility/iconButton.svelte'
|
||||||
import Toggle from '$lib/components/utility/toggle.svelte'
|
import Toggle from '$lib/components/utility/toggle.svelte'
|
||||||
|
|
||||||
|
|||||||
72
src/routes/subLayouts.svelte
Normal file
72
src/routes/subLayouts.svelte
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<script>
|
||||||
|
export let currentPage
|
||||||
|
|
||||||
|
import Navbar from '$lib/components/utility/navbar.svelte'
|
||||||
|
import { fly } from 'svelte/transition'
|
||||||
|
|
||||||
|
const contentTabs = {
|
||||||
|
Home: '/',
|
||||||
|
Artists: '/artist',
|
||||||
|
Playlists: '/playlist',
|
||||||
|
Libray: '/library',
|
||||||
|
}
|
||||||
|
|
||||||
|
let previousPage = currentPage
|
||||||
|
let direction = 1
|
||||||
|
$: calculateDirection(currentPage)
|
||||||
|
|
||||||
|
const calculateDirection = (newPage) => {
|
||||||
|
const contentLinks = Object.values(contentTabs)
|
||||||
|
const newPageIndex = contentLinks.indexOf(newPage)
|
||||||
|
const previousPageIndex = contentLinks.indexOf(previousPage)
|
||||||
|
if (newPageIndex > previousPageIndex) {
|
||||||
|
direction = 1
|
||||||
|
} else {
|
||||||
|
direction = -1
|
||||||
|
}
|
||||||
|
previousPage = currentPage
|
||||||
|
}
|
||||||
|
|
||||||
|
let activeTab, indicatorBar, tabList
|
||||||
|
$: calculateBar(activeTab)
|
||||||
|
|
||||||
|
const calculateBar = (activeTab) => {
|
||||||
|
if (activeTab) {
|
||||||
|
const listRect = tabList.getBoundingClientRect()
|
||||||
|
const tabRec = activeTab.getBoundingClientRect()
|
||||||
|
indicatorBar.style.top = `${listRect.height}px`
|
||||||
|
if (direction === 1) {
|
||||||
|
indicatorBar.style.right = `${listRect.right - tabRec.right}px`
|
||||||
|
setTimeout(() => (indicatorBar.style.left = `${tabRec.left - listRect.left}px`), 350)
|
||||||
|
} else {
|
||||||
|
indicatorBar.style.left = `${tabRec.left - listRect.left}px`
|
||||||
|
setTimeout(() => (indicatorBar.style.right = `${listRect.right - tabRec.right}px`), 350)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if Object.values(contentTabs).includes(currentPage)}
|
||||||
|
<Navbar />
|
||||||
|
<div class="flex justify-center py-4">
|
||||||
|
<h1 bind:this={tabList} class="relative flex justify-center gap-12 text-lg">
|
||||||
|
{#each Object.entries(contentTabs) as [header, page]}
|
||||||
|
{#if currentPage === page}
|
||||||
|
<span bind:this={activeTab} class="pointer-events-none">{header}</span>
|
||||||
|
{:else}
|
||||||
|
<a class="text-neutral-400 hover:text-lazuli-primary" href={page}>{header}</a>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
<div bind:this={indicatorBar} class="absolute h-0.5 bg-lazuli-primary transition-all duration-300 ease-in-out" />
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-hidden px-8 sm:px-32">
|
||||||
|
{#key previousPage}
|
||||||
|
<div in:fly={{ x: 200 * direction, duration: 300, delay: 300 }} out:fly={{ x: -200 * direction, duration: 300 }}>
|
||||||
|
<slot name="innerContent" />
|
||||||
|
</div>
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<slot name="innerContent" />
|
||||||
|
{/if}
|
||||||
Reference in New Issue
Block a user