Merge pull request #104 from teaxyz/popup-search-results

#36 general search
This commit is contained in:
Neil 2022-12-23 10:42:08 +08:00 committed by GitHub
commit 1ec48fa8c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 305 additions and 68 deletions

View file

@ -8,7 +8,7 @@ async function main() {
type: 'section',
text: {
type: 'mrkdwn',
text: `NEW BUILD FOR ${process.env.PLATFORM} <${process.env.DOWNLOAD_URL}|download ${process.env.VERSION ? ('v'+process.env.VERSION) : ''}>`
text: `NEW BUILD FOR ${process.env.PLATFORM} <${process.env.DOWNLOAD_URL}|download ${process.env.VERSION || ''}>`
}
}
]

View file

@ -87,4 +87,4 @@ jobs:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
PLATFORM: ${{ matrix.platform }}
VERSION: ${{steps.tag.outputs.tag}}
DOWNLOAD_URL: http://preview.gui.tea.xyz.s3-website-us-east-1.amazonaws.com/release/tea_${{ steps.date.outputs.unix_seconds }}.${{ matrix.platform == 'ubuntu-latest' && 'deb' || 'dmg'}}
DOWNLOAD_URL: http://preview.gui.tea.xyz.s3-website-us-east-1.amazonaws.com/release/tea_gui_${{steps.tag.outputs.tag}}.${{matrix.platform}}.${{ matrix.platform == 'ubuntu-latest' && 'deb' || 'dmg'}}

View file

@ -3,7 +3,7 @@
import type { GUIPackage } from '$libs/types';
import { PackageStates } from '$libs/types';
import PanelHeader from '@tea/ui/PanelHeader/PanelHeader.svelte';
import { packages as packagesStore } from '$libs/stores';
import { packagesStore } from '$libs/stores';
import MiniPackageCard from '@tea/ui/MiniPackageCard/MiniPackageCard.svelte';
import Preloader from '@tea/ui/Preloader/Preloader.svelte';
let packages: GUIPackage[] = [];

View file

@ -1,16 +1,12 @@
<script lang="ts">
import { page } from '$app/stores';
import { open } from '@tauri-apps/api/shell';
import { appWindow } from '@tauri-apps/api/window';
import { searchStore } from '$libs/stores';
import SearchInput from '@tea/ui/SearchInput/SearchInput.svelte';
import Button from '@tea/ui/Button/Button.svelte';
import { beforeUpdate } from 'svelte';
const openGithub = () => {
open('https://github.com/teaxyz');
};
let maximized = false;
const toggleMaximize = () => {
maximized = !maximized;
@ -63,7 +59,7 @@
});
const onSearch = (term: string) => {
console.log('navbar search:', term);
searchStore.search(term);
};
</script>

View file

@ -4,7 +4,7 @@
<section class="border-2 border-gray bg-black p-2">
<div class="profile_banner container flex border border-gray bg-black">
<img class="w-1/5" src="/images/bored-ape.png" />
<img class="w-1/5" src="/images/bored-ape.png" alt="profile" />
<div class="flex w-4/5 items-center p-5">
<div class="w-1/2 pl-5">
<p class="uppercase text-gray">Authenticated with GitHub</p>
@ -15,7 +15,7 @@
<div class="w-1/2 pl-10">
<p class="uppercase leading-loose text-gray">
Country: <span>Germany</span><br />Wallet:
<a class="text-green underline" href="">Connect Now</a>
<a class="text-green underline" href="/">Connect Now</a>
</p>
</div>
</div>

View file

@ -1,21 +1,16 @@
<script lang="ts">
import '$appcss';
import Fuse from 'fuse.js';
import { packages as packagesStore, initializePackages } from '$libs/stores';
import { packagesStore } from '$libs/stores';
import SortingButtons from './SortingButtons.svelte';
import type { GUIPackage } from '$libs/types';
import { PackageStates } from '$libs/types';
import PackageCard from '@tea/ui/PackageCard/PackageCard.svelte';
import SearchInput from '@tea/ui/SearchInput/SearchInput.svelte';
import Preloader from '@tea/ui/Preloader/Preloader.svelte';
import { onMount } from 'svelte';
import { installPackage } from '@api';
let allPackages: GUIPackage[] = [];
let packagesIndex: Fuse<GUIPackage>;
let packages: GUIPackage[] = [];
let initialized = false;
let sortBy = 'popularity';
let sortDirection: 'asc' | 'desc' = 'desc';
@ -39,32 +34,12 @@
});
};
packagesStore.subscribe((v) => {
allPackages = v;
setPackages(allPackages);
if (!packagesIndex && allPackages.length) {
// dont remove or this can get crazy
packagesIndex = new Fuse(allPackages, {
keys: ['name', 'full_name', 'desc']
});
}
});
onMount(async () => {
if (!packages.length && !initialized) {
initialized = true;
initializePackages();
}
});
const onSearch = (term: string) => {
const onSearch = async (term: string) => {
if (term !== '' && term.length > 1) {
const res = packagesIndex.search(term, { limit: searchLimit });
const matchingPackages: GUIPackage[] = res.map((v) => v.item);
const matchingPackages: GUIPackage[] = await packagesStore.search(term, searchLimit);
setPackages(matchingPackages, true);
} else {
setPackages(allPackages);
setPackages(packagesStore.packages, false);
}
};
@ -82,6 +57,8 @@
[PackageStates.UNINSTALLED]: 'RE-INSTALL'
}[state];
};
packagesStore.subscribe(setPackages);
</script>
<div class="border border-gray bg-black">

View file

@ -0,0 +1,160 @@
<script lang="ts">
import { searchStore } from '$libs/stores';
import type { GUIPackage } from '$libs/types';
import Preloader from '@tea/ui/Preloader/Preloader.svelte';
import PackageCard from '@tea/ui/PackageCard/PackageCard.svelte';
import { PackageStates } from '$libs/types';
import Posts from '@tea/ui/Posts/Posts.svelte';
import { installPackage } from '@api';
import type { AirtablePost } from '@tea/ui/types';
let term: string;
let packages: GUIPackage[] = [];
let articles: AirtablePost[] = []; // news, blogs, etc
let workshops: AirtablePost[] = []; // workshops, course
let loading = true;
searchStore.subscribe((v) => {
term = v;
});
searchStore.packagesSearch.subscribe((pkgs) => {
packages = pkgs;
});
searchStore.postsSearch.subscribe((posts) => {
let partialArticles: AirtablePost[] = [];
let partialWorkshops: AirtablePost[] = [];
for (let post of posts) {
if (post.tags.includes('news')) {
partialArticles.push(post);
}
if (post.tags.includes('course') || post.tags.includes('featured_course')) {
partialWorkshops.push(post);
}
}
articles = partialArticles;
workshops = partialWorkshops;
});
searchStore.searching.subscribe((v) => (loading = v));
const getCTALabel = (state: PackageStates): string => {
return {
[PackageStates.AVAILABLE]: 'INSTALL',
[PackageStates.INSTALLED]: 'INSTALLED',
[PackageStates.INSTALLING]: 'INSTALLING',
[PackageStates.UNINSTALLED]: 'RE-INSTALL'
}[state];
};
const onClose = () => {
term = '';
};
</script>
<section class={term ? 'show' : ''}>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<figure on:click={onClose} />
<div class="z-20 border border-gray bg-black">
<header class="flex justify-between p-4">
<div class="text-2xl text-primary">Showing search results for `{term}`</div>
<button on:click={onClose}>&#x2715</button>
</header>
<menu class="flex h-8 w-full gap-4 bg-accent px-4 text-xs">
<button>packages ({packages.length})</button>
<button>articles ({articles.length})</button>
<button>workshops ({workshops.length})</button>
</menu>
<header class="p-4 text-lg text-primary">
Top Package Results ({packages.length})
</header>
<ul class="grid grid-cols-3">
{#if packages.length > 0}
{#each packages as pkg}
<div class={pkg.state === PackageStates.INSTALLING ? 'animate-pulse' : ''}>
<PackageCard
{pkg}
link={`/packages/${pkg.slug}`}
ctaLabel={getCTALabel(pkg.state)}
onClickCTA={async () => {
try {
pkg.state = PackageStates.INSTALLING;
await installPackage(pkg.full_name);
pkg.state = PackageStates.INSTALLED;
} catch (error) {
console.error(error);
}
}}
/>
</div>
{/each}
{:else if loading}
{#each Array(12) as _}
<section class="h-50 border border-gray p-4">
<Preloader />
</section>
{/each}
{/if}
</ul>
<header class="p-4 text-lg text-primary">
Top Article Results ({articles.length})
</header>
{#if articles.length}
<Posts posts={articles} linkTarget="_blank" />
{:else if loading}
<section class="h-64 border border-gray bg-black p-4">
<Preloader />
</section>
{/if}
<header class="p-4 text-lg text-primary">
Top Workshop Results ({workshops.length})
</header>
{#if workshops.length}
<Posts posts={workshops} linkTarget="_blank" />
{:else if loading}
<section class="h-64 border border-gray bg-black p-4">
<Preloader />
</section>
{/if}
</div>
</section>
<style>
section {
position: fixed;
top: 0;
left: 240px;
right: 0;
background: rgba(0, 0, 0, 0.7);
transition: opacity 0.3s ease-in-out;
padding: 36px;
opacity: 0%;
overflow: hidden;
height: 0px;
z-index: 10;
}
section > figure {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
section.show {
opacity: 100%;
height: 100%;
}
section > div {
position: relative;
height: 0%;
transition: height 0.6s ease-in-out;
overflow-y: scroll;
}
section.show > div {
height: 90%;
}
</style>

View file

@ -26,7 +26,8 @@ async function get<T>(path: string, query?: { [key: string]: string }) {
const uri = join(base, path);
const { data } = await client.get<T>(uri.toString(), {
headers: {
Authorization: 'public' // TODO: figure out why req w/o Authorization does not work
Authorization: 'public', // TODO: figure out why req w/o Authorization does not work
'cache-control': 'no-cache'
},
query: query || {}
});
@ -151,9 +152,9 @@ export async function getTopPackages(): Promise<GUIPackage[]> {
return packages;
}
export async function getAllPosts(tag: string): Promise<AirtablePost[]> {
export async function getAllPosts(tag?: string): Promise<AirtablePost[]> {
// add filter here someday: tag = news | course
const posts = await get<AirtablePost[]>('posts', { tag });
const posts = await get<AirtablePost[]>('posts', tag ? { tag } : {});
return posts;
}

View file

@ -1,21 +1,49 @@
import { writable } from 'svelte/store';
import type { Package, Review } from '@tea/ui/types';
import Fuse from 'fuse.js';
import type { Package, Review, AirtablePost } from '@tea/ui/types';
import type { GUIPackage } from '$libs/types';
// TODO: figure out a better structure for managing states maybe turn them into models?
import { getPackages, getFeaturedPackages, getPackageReviews } from '@api';
import { getPackages, getFeaturedPackages, getPackageReviews, getAllPosts } from '@api';
export const backLink = writable<string>('/');
export const packages = writable<GUIPackage[]>([]);
export const featuredPackages = writable<Package[]>([]);
export const initializePackages = async () => {
console.log('initialize packages');
const newPackages = await getPackages();
packages.set(newPackages);
};
function initPackagesStore() {
let initialized = false;
const { subscribe, set } = writable<GUIPackage[]>([]);
const packages: GUIPackage[] = [];
let packagesIndex: Fuse<GUIPackage>;
if (!initialized) {
initialized = true;
getPackages().then((pkgs) => {
set(pkgs);
packagesIndex = new Fuse(pkgs, {
keys: ['name', 'full_name', 'desc']
});
});
}
subscribe((v) => packages.push(...v));
return {
packages,
subscribe,
search: async (term: string, limit = 5): Promise<GUIPackage[]> => {
if (!term || !packagesIndex) return [];
// TODO: if online, use algolia else use Fuse
const res = packagesIndex.search(term, { limit });
const matchingPackages: GUIPackage[] = res.map((v) => v.item);
return matchingPackages;
}
};
}
export const packagesStore = initPackagesStore();
export const initializeFeaturedPackages = async () => {
console.log('initialzie featured packages');
@ -60,3 +88,81 @@ function initPackagesReviewStore() {
}
export const packagesReviewStore = initPackagesReviewStore();
function initPosts() {
let initialized = false;
const { subscribe, set } = writable<AirtablePost[]>([]);
const posts: AirtablePost[] = [];
let postsIndex: Fuse<AirtablePost>;
if (!initialized) {
initialized = true;
getAllPosts().then(set);
}
subscribe((v) => {
posts.push(...v);
postsIndex = new Fuse(posts, {
keys: ['title', 'sub_title', 'short_description', 'tags']
});
});
return {
subscribe,
search: async (term: string, limit = 10) => {
const res = postsIndex.search(term, { limit });
const matchingPosts: AirtablePost[] = res.map((v) => v.item);
return matchingPosts;
}
};
}
export const postsStore = initPosts();
function initSearchStore() {
const searching = writable<boolean>(false);
const { subscribe, set } = writable<string>('');
const packagesSearch = writable<GUIPackage[]>([]);
const postsSearch = writable<AirtablePost[]>([]);
// TODO:
// add fuse.js index here: packages, articles/posts, etc
// define fuse.js shape { tags:[], desc:string, title: string, thumb_image_url: string }
// should use algolia if user is somehow online
const packagesFound: GUIPackage[] = [];
let term = '';
subscribe((v) => (term = v));
packagesSearch.subscribe((v) => packagesFound.push(...v));
return {
term,
searching,
packagesSearch,
postsSearch,
packagesFound,
subscribe,
search: async (term: string) => {
searching.set(true);
try {
if (term) {
const [resultPkgs, resultPosts] = await Promise.all([
packagesStore.search(term, 5),
postsStore.search(term, 10)
]);
packagesSearch.set(resultPkgs);
postsSearch.set(resultPosts);
} else {
packagesSearch.set([]);
postsSearch.set([]);
}
set(term);
} finally {
searching.set(false);
}
}
};
}
export const searchStore = initSearchStore();

View file

@ -6,6 +6,7 @@
import FooterLinks from '$components/FooterLinks/FooterLinks.svelte';
import { backLink as backLinkStore } from '$libs/stores';
import SearchPopupResults from '$components/SearchPopupResults/SearchPopupResults.svelte';
let view: HTMLElement;
@ -36,6 +37,7 @@
<footer class="mt-8 w-full border border-r-0 border-gray bg-black">
<FooterLinks />
</footer>
<SearchPopupResults />
</section>
</div>
@ -56,15 +58,17 @@
overflow-y: scroll;
}
@media screen and (min-width: 1440px) {
figure {
position: fixed;
z-index: 0;
position: fixed;
top: 220px;
left: 240px;
right: 0px;
bottom: 0px;
background-image: url('/images/gui-background-grid.svg');
}
@media screen and (min-width: 1440px) {
figure {
background-size: cover;
background-repeat: repeat-y;
}
@ -74,13 +78,6 @@
}
@media screen and (max-width: 1440px) {
figure {
position: fixed;
z-index: 0;
top: 220px;
left: 240px;
right: 0px;
bottom: 0px;
background-image: url('/images/gui-background-grid.svg');
background-size: contain;
background-repeat: repeat;
}

View file

@ -9,7 +9,7 @@
/** @type {import('./$types').PageData} */
export let data;
import { packages, featuredPackages } from '$libs/stores';
import { packagesStore, featuredPackages } from '$libs/stores';
import type { Package } from '@tea/ui/types';
@ -30,7 +30,7 @@
// }
};
packages.subscribe(setPkg);
packagesStore.subscribe(setPkg);
featuredPackages.subscribe(setPkg);
</script>