feat(Store): added store page

This commit is contained in:
sethburkart123
2024-09-13 16:01:48 +10:00
parent 8d08354f6c
commit 1272c60a4d
14 changed files with 385 additions and 8 deletions
+39
View File
@@ -37,6 +37,7 @@ import injectedCSS from '@/css/injected.scss?inline'
import documentLoadCSS from '@/css/documentload.scss?inline' import documentLoadCSS from '@/css/documentload.scss?inline'
import renderSvelte from '@/svelte-interface/main' import renderSvelte from '@/svelte-interface/main'
import Settings from '@/svelte-interface/pages/settings.svelte' import Settings from '@/svelte-interface/pages/settings.svelte'
import { renderStore } from './seqta/ui/renderStore'
let SettingsClicked = false let SettingsClicked = false
export let MenuOptionsOpen = false export let MenuOptionsOpen = false
@@ -343,6 +344,44 @@ export function OpenWhatsNewPopup() {
}) })
} }
export function OpenStorePage() {
const remove = renderStore()
/* export function renderStore() {
const container = document.querySelector('#container');
if (!container) {
throw new Error('Container not found');
}
const child = document.createElement('div');
child.id = 'store';
container!.appendChild(child);
const shadow = child.attachShadow({ mode: 'open' });
const app = renderSvelte(Store, shadow);
return () => unmount(app)
}
*/
// add a close button to the page
const closeButton = document.createElement('button')
closeButton.id = 'storeclosebutton'
closeButton.classList.add('closeButton')
closeButton.innerHTML = 'Close'
document.body.appendChild(closeButton)
closeButton.addEventListener('click', () => {
closeButton.classList.add('hide')
document.getElementById('store')!.classList.add('hide')
setTimeout(() => {
remove()
document.body.removeChild(closeButton)
document.getElementById('store')!.remove()
}, 500)
})
}
export function OpenAboutPage() { export function OpenAboutPage() {
const background = document.createElement('div') const background = document.createElement('div')
background.id = 'whatsnewbk' background.id = 'whatsnewbk'
+53
View File
@@ -62,6 +62,58 @@ html {
} }
} }
#store {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 9998;
animation: fadeIn 500ms forwards;
animation-delay: 200ms;
opacity: 0;
&.hide {
animation: fadeOut 500ms forwards;
}
}
#storeclosebutton {
position: absolute;
top: 10px;
right: 10px;
z-index: 9999;
animation: fadeIn 500ms forwards;
animation-delay: 200ms;
opacity: 0;
&.hide {
animation: fadeOut 500ms forwards;
pointer-events: none;
}
}
.dark #storeclosebutton {
color: white;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.dark .resizeBar { .dark .resizeBar {
background-color: rgb(28 28 30); background-color: rgb(28 28 30);
} }
@@ -2532,6 +2584,7 @@ li.MessageList__unread___3imtO {
position: absolute; position: absolute;
display: none; display: none;
} }
/* Show the checkmark when checked */ /* Show the checkmark when checked */
.upcoming-checkbox-container input:checked ~ .upcoming-checkmark:after { .upcoming-checkbox-container input:checked ~ .upcoming-checkmark:after {
display: block; display: block;
+20
View File
@@ -0,0 +1,20 @@
import renderSvelte from '@/svelte-interface/main';
import Store from '@/svelte-interface/pages/store.svelte'
import { unmount } from 'svelte'
export function renderStore() {
const container = document.querySelector('#container');
if (!container) {
throw new Error('Container not found');
}
const child = document.createElement('div');
child.id = 'store';
container!.appendChild(child);
const shadow = child.attachShadow({ mode: 'open' });
const app = renderSvelte(Store, shadow);
return () => unmount(app)
}
@@ -0,0 +1,5 @@
<script lang="ts">
let { width, height} = $props<{width?: string, height?: string}>()
</script>
<div style="width: {width ? width : '100%'}; height: {height ? height : '100%'}; background: #e0e0e0;" class="animate-pulse"></div>
@@ -0,0 +1,48 @@
<script lang="ts">
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
import type { Theme } from '@/svelte-interface/types/Theme';
import { register, type SwiperContainer } from 'swiper/element/bundle';
let { coverThemes } = $props<{ coverThemes: Theme[] }>();
let swiperEl = $state<SwiperContainer | undefined>();
const slidePrev = () => swiperEl?.swiper.slidePrev();
const slideNext = () => swiperEl?.swiper.slideNext();
onMount(() => {
register();
});
</script>
{#if coverThemes.length > 0}
<div class="relative w-full transition-opacity rounded-xl overflow-clip" transition:fade>
<swiper-container bind:this={swiperEl} slides-per-view="1" space-between="20" loop="true" autoplay="true" disable-on-interaction="false" pause-on-mouse-enter="true" class="w-full aspect-[8/3]">
{#each coverThemes as theme, index (index)}
<swiper-slide class="relative cursor-pointer rounded-xl overflow-clip">
<img src={theme.marqueeImage} alt="Theme Preview" class="object-cover w-full h-full" />
<div class='absolute bottom-0 left-0 p-8 z-[1]'>
<h2 class='text-4xl font-bold text-white'>{theme.name}</h2>
<p class='text-lg text-white'>{theme.description}</p>
</div>
<div class='absolute bottom-0 left-0 w-full h-1/2 bg-gradient-to-t from-black/80 to-transparent'></div>
</swiper-slide>
{/each}
</swiper-container>
<!-- Pagination buttons -->
<div class='absolute z-10 flex gap-2 bottom-2 right-2'>
<button onclick={slidePrev} class='flex items-center justify-center w-8 h-8 text-white bg-black bg-opacity-50 rounded-full dark:bg-zinc-800 dark:bg-opacity-50'>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width={1.5} stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m15.75 19.5-7.5-7.5 7.5-7.5" />
</svg>
</button>
<button onclick={slideNext} class='flex items-center justify-center w-8 h-8 text-white bg-black bg-opacity-50 rounded-full dark:bg-zinc-800 dark:bg-opacity-50'>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width={1.5} stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
</svg>
</button>
</div>
</div>
{/if}
@@ -0,0 +1,19 @@
<script lang="ts">
import Theme from '@/svelte-interface/pages/settings/theme.svelte'
let { theme, onClick } = $props<{ theme: Theme; onClick: () => void }>();
import { fade } from 'svelte/transition';
</script>
<div class="w-full cursor-pointer" role="button" tabindex="-1" onkeydown={onClick} onclick={onClick}>
<div class="bg-gray-50 w-full transition-all hover:scale-105 duration-500 relative group flex flex-col hover:shadow-2xl dark:hover:shadow-white/[0.1] hover:shadow-white/[0.8] dark:bg-zinc-800 dark:border-white/[0.1] h-auto rounded-xl overflow-clip border" transition:fade>
<div class="absolute z-10 mb-1 text-xl font-bold text-white bottom-1 left-3">
{theme.name}
</div>
<div class='absolute bottom-0 z-0 w-full h-3/4 bg-gradient-to-t from-black/80 to-transparent'></div>
<div class='w-full'>
<img src={theme.coverImage} alt="Theme Preview" class="object-cover w-full h-48 rounded-md" />
</div>
</div>
</div>
@@ -0,0 +1,40 @@
<script lang="ts">
import type { Theme } from '@/svelte-interface/types/Theme'
import ThemeCard from './ThemeCard.svelte';
import ThemeModal from './ThemeModal.svelte';
let { themes, searchTerm } = $props<{ themes: Theme[]; searchTerm: string }>();
let displayTheme = $state<Theme | null>();
let filteredThemes = $derived(themes.filter((theme: Theme) =>
theme.name.toLowerCase().includes(searchTerm.toLowerCase()) || theme.description.toLowerCase().includes(searchTerm.toLowerCase())
));
</script>
<div class="grid grid-cols-1 gap-4 py-12 mx-auto sm:grid-cols-2 lg:grid-cols-3">
{#each filteredThemes as theme (theme.id)}
<ThemeCard theme={theme} onClick={() => displayTheme = theme} />
{/each}
<!-- "Got a Theme Idea?" card -->
<a href="https://betterseqta.gitbook.io/betterseqta-docs" class='w-full cursor-pointer'>
<div class="bg-zinc-50 h-48 w-full transition-all hover:scale-105 duration-500 relative justify-center items-center group group/card flex flex-col hover:shadow-2xl dark:hover:shadow-white/[0.1] hover:shadow-white/[0.8] dark:bg-zinc-800 dark:border-white/[0.1] rounded-xl overflow-clip border">
<div class="text-2xl font-IconFamily">{'\uecb3'}</div>
<div class="text-xl font-bold text-center transition-all duration-500 dark:text-white">
Got a Theme Idea?
<p class="text-lg font-light subtitle">Transform it into a stunning theme!</p>
</div>
</div>
</a>
{#if filteredThemes.length === 0}
<div class="flex flex-col items-center justify-center w-full text-center h-96">
<h1 class="mt-4 text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-5xl">That doesn't exist! 😭😭😭</h1>
<p class="mt-6 text-lg leading-7 text-zinc-600 dark:text-zinc-300">Sorry, we couldn't find the theme you're looking for. Maybe... you could create it?</p>
</div>
{/if}
</div>
{#if displayTheme}
<ThemeModal theme={displayTheme} onClose={() => displayTheme = null} onInstall={() => {}} onRemove={() => {}} />
{/if}
@@ -0,0 +1,45 @@
<script lang="ts">
export let theme;
export let currentThemes = [];
export let onClose;
export let onInstall;
export let onRemove;
let installing = false;
// Transitions
import { fade, slide } from 'svelte/transition';
</script>
<div class="fixed inset-0 z-50 flex items-end justify-center bg-black bg-opacity-70" on:click={onClose} transition:fade>
<div class="w-full max-w-xl h-[95%] p-4 bg-white rounded-t-2xl dark:bg-zinc-800 overflow-scroll" on:click|stopPropagation transition:slide={{ axis: 'y' }}>
<div class="relative h-auto">
<button class="absolute top-0 right-0 p-2 text-xl font-bold text-gray-600 font-IconFamily dark:text-gray-200" on:click={onClose}>
{'\ued8a'}
</button>
<h2 class="mb-4 text-2xl font-bold">
{theme.name}
</h2>
<img src={theme.marqueeImage} alt="Theme Cover" class="object-cover w-full mb-4 rounded-md" />
<p class="mb-4 text-gray-700 dark:text-gray-300">
{theme.description}
</p>
{#if currentThemes.includes(theme.id)}
<button on:click={() => {installing = true; onRemove(theme.id); installing = false;}} class="flex px-4 py-2 mt-4 ml-auto rounded-full dark:text-white bg-zinc-300 dark:bg-zinc-700 dark:hover:bg-zinc-600/50 hover:bg-zinc-200">
{#if installing}
Removing...
{:else}
Remove
{/if}
</button>
{:else}
<button on:click={() => {installing = true; onInstall(theme.id); installing = false;}} class="flex px-4 py-2 mt-4 ml-auto rounded-full dark:text-white bg-zinc-300 dark:bg-zinc-700 dark:hover:bg-zinc-600/50 hover:bg-zinc-200">
{#if installing}
Installing...
{:else}
Install
{/if}
</button>
{/if}
</div>
</div>
</div>
@@ -8,6 +8,7 @@
import { disableTheme } from '@/seqta/ui/themes/disableTheme' import { disableTheme } from '@/seqta/ui/themes/disableTheme'
import { setTheme } from '@/seqta/ui/themes/setTheme' import { setTheme } from '@/seqta/ui/themes/setTheme'
import { deleteTheme } from '@/seqta/ui/themes/deleteTheme' import { deleteTheme } from '@/seqta/ui/themes/deleteTheme'
import { OpenStorePage } from '@/SEQTA'
let themes = $state<ThemeList | null>(null); let themes = $state<ThemeList | null>(null);
let { isEditMode } = $props<{ isEditMode: boolean }>(); let { isEditMode } = $props<{ isEditMode: boolean }>();
@@ -183,17 +184,16 @@
<div id="divider" class="w-full h-[1px] my-2 bg-zinc-100 dark:bg-zinc-600"></div> <div id="divider" class="w-full h-[1px] my-2 bg-zinc-100 dark:bg-zinc-600"></div>
{/if} {/if}
<a <button
href={''} onclick={() => OpenStorePage()}
target="_blank"
class="flex items-center justify-center w-full transition aspect-theme rounded-xl bg-zinc-100 dark:bg-zinc-900 dark:text-white" class="flex items-center justify-center w-full transition aspect-theme rounded-xl bg-zinc-100 dark:bg-zinc-900 dark:text-white"
> >
<span class="text-xl font-IconFamily">&#xecc5;</span> <span class="text-xl font-IconFamily">&#xecc5;</span>
<span class="ml-2">Theme Store</span> <span class="ml-2">Theme Store</span>
</a> </button>
<button <button
onclick={() => OpenThemeCreator} onclick={() => OpenThemeCreator()}
class="flex items-center justify-center w-full transition aspect-theme rounded-xl bg-zinc-100 dark:bg-zinc-900 dark:text-white" class="flex items-center justify-center w-full transition aspect-theme rounded-xl bg-zinc-100 dark:bg-zinc-900 dark:text-white"
> >
<span class="text-xl font-IconFamily">&#xec60;</span> <span class="text-xl font-IconFamily">&#xec60;</span>
+13
View File
@@ -25,3 +25,16 @@ input {
box-shadow: unset !important; box-shadow: unset !important;
} }
} }
.animate-fade-in {
animation: fadeIn 0.5s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
+1 -2
View File
@@ -3,7 +3,7 @@ import { mount } from 'svelte';
import type { ComponentType } from 'svelte'; import type { ComponentType } from 'svelte';
export default function renderSvelte( export default function renderSvelte(
Component: ComponentType, Component: ComponentType | any,
mountPoint: ShadowRoot | HTMLElement, mountPoint: ShadowRoot | HTMLElement,
props: Record<string, any> = {} props: Record<string, any> = {}
) { ) {
@@ -19,6 +19,5 @@ export default function renderSvelte(
style.setAttribute("type", "text/css"); style.setAttribute("type", "text/css");
style.innerHTML = styles; style.innerHTML = styles;
mountPoint.appendChild(style); mountPoint.appendChild(style);
return app; return app;
} }
+1 -1
View File
@@ -36,7 +36,7 @@
<div class="grid border-b border-b-zinc-200/40 place-items-center"> <div class="grid border-b border-b-zinc-200/40 place-items-center">
<img src={browser.runtime.getURL('resources/icons/betterseqta-dark-full.png')} class="w-4/5 dark:hidden" alt="Light logo" /> <img src={browser.runtime.getURL('resources/icons/betterseqta-dark-full.png')} class="w-4/5 dark:hidden" alt="Light logo" />
<img src={browser.runtime.getURL('resources/icons/betterseqta-light-full.png')} class="hidden w-4/5 dark:block" alt="Dark logo" /> <img src={browser.runtime.getURL('resources/icons/betterseqta-light-full.png')} class="hidden w-4/5 dark:block" alt="Dark logo" />
<button onclick={openChangelog} class="absolute w-8 h-8 text-lg rounded-xl font-IconFamily top-1 right-0 bg-zinc-100 dark:bg-zinc-700"></button> <button onclick={openChangelog} class="absolute right-0 w-8 h-8 text-lg rounded-xl font-IconFamily top-1 bg-zinc-100 dark:bg-zinc-700"></button>
<button onclick={openAbout} class="absolute w-8 h-8 text-lg rounded-xl font-IconFamily top-1 right-10 bg-zinc-100 dark:bg-zinc-700"></button> <button onclick={openAbout} class="absolute w-8 h-8 text-lg rounded-xl font-IconFamily top-1 right-10 bg-zinc-100 dark:bg-zinc-700"></button>
</div> </div>
+89
View File
@@ -0,0 +1,89 @@
<script lang="ts">
import { onMount } from 'svelte';
// Import child components
import CoverSwiper from '../components/store/CoverSwiper.svelte';
import ThemeGrid from '../components/store/ThemeGrid.svelte';
import SkeletonLoader from '../components/SkeletonLoader.svelte';
import { settingsState } from '@/seqta/utils/listeners/SettingsState'
import type { Theme } from '../types/Theme'
import browser from 'webextension-polyfill'
// State variables
let searchTerm = $state<string>('');
let themes = $state<Theme[]>([]);
let coverThemes = $state<Theme[]>([]);
let loading = $state<boolean>(true);
let darkMode = $state<boolean>(false);
// Fetch themes and initialize app
const fetchThemes = async () => {
try {
const response = await fetch(`https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/store/themes.json?nocache=${(new Date()).getTime()}`, { cache: 'no-store' });
const data = await response.json();
themes = data.themes;
// Shuffle for cover themes
const shuffled = [...themes].sort(() => 0.5 - Math.random());
coverThemes = shuffled.slice(0, 3);
loading = false;
} catch (error) {
console.error('Failed to fetch themes', error);
setTimeout(fetchThemes, 5000); // Retry after 5 seconds if failure occurs
}
};
// On mount
onMount(async () => {
await fetchThemes();
darkMode = (await browser.storage.local.get('DarkMode')).DarkMode === 'true';
darkMode = $settingsState.DarkMode;
});
// Filter themes based on search term
let filteredThemes = $derived(themes.filter(theme =>
theme.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
theme.description.toLowerCase().includes(searchTerm.toLowerCase())
));
</script>
<div class="w-screen h-screen overflow-y-scroll bg-white {darkMode ? 'dark' : ''}">
<div class="bg-zinc-200/50 dark:bg-zinc-900 dark:text-white pt-[4.25rem] h-full px-12">
<!-- Search Input (optional) -->
<div class="px-8 py-4">
<input
type="text"
placeholder="Search Themes"
bind:value={searchTerm}
class="w-full p-2 bg-white border rounded-lg dark:bg-zinc-800 border-zinc-300 dark:border-zinc-600 text-zinc-800 dark:text-white"
/>
</div>
<!-- Loading State -->
{#if loading}
<div class="grid grid-cols-1 gap-4 py-12 mx-auto sm:grid-cols-2 lg:grid-cols-3">
<SkeletonLoader width="100%" height="200px" />
</div>
{:else}
{#if searchTerm === ''}
<CoverSwiper coverThemes={coverThemes} />
{/if}
<!-- ThemeGrid to display filtered themes -->
<ThemeGrid themes={filteredThemes} searchTerm={searchTerm} />
{#if filteredThemes.length === 0 && !loading}
<div class="flex flex-col items-center justify-center w-full text-center h-96">
<h1 class="mt-4 text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-5xl">No results! 😭</h1>
<p class="mt-6 text-lg leading-7 text-zinc-600 dark:text-zinc-300">Sorry, no themes match your search. Maybe create one?</p>
</div>
{/if}
{/if}
</div>
</div>
+7
View File
@@ -0,0 +1,7 @@
export type Theme = {
name: string;
description: string;
coverImage: string;
marqueeImage: string;
id: string;
};