#301 init home page with side nav (#302)

* #301 init home page with side nav

* #301 filter by state and sort

* #301 update package cards layout

---------

Co-authored-by: neil <neil@neils-MacBook-Pro.local>
This commit is contained in:
Neil 2023-03-16 09:24:48 +08:00 committed by GitHub
parent b43a64566d
commit c5a3a72948
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 307 additions and 219 deletions

View file

@ -49,7 +49,8 @@
link={`/packages/${pkg.slug}`}
ctaLabel={getCTALabel(pkg.state)}
progessLoading={+fakeLoadingProgress.toFixed(2)}
ctaType={PackageStates.INSTALLED === pkg.state ? "ghost" : "plain"}
ctaType="plain"
ctaColor={PackageStates.INSTALLED === pkg.state ? "green" : "secondary"}
onClickCTA={async () => {
fakeLoadingProgress = 1;
startFakeLoader();

View file

@ -9,23 +9,18 @@
import { installPackage } from '@native';
import { trackInstall, trackInstallFailed } from '$libs/analytics';
import SortingButtons from '$components/search-packages/sorting-buttons.svelte';
export let title = 'Packages';
let pkgNeedsUpdateCount = 0;
const { packages: allPackages } = packagesStore;
export let tab: "ALL" | "INSTALLED" | "INSTALLED_WITH_UPDATES" = "ALL";
export let stateFilters: {[key: string]: boolean};
export let sortBy: "popularity" | "most recent" = 'popularity';
export let sortDirection: 'asc' | 'desc' = 'desc';
let sortBy = 'popularity';
let sortDirection: 'asc' | 'desc' = 'desc';
const loadMore = 12;
const loadMore = 9;
let limit = loadMore;
let packages: GUIPackage[] = [];
const setPackages = (pkgs: GUIPackage[]) => {
pkgNeedsUpdateCount = pkgs.filter((p) => p.state === PackageStates.NEEDS_UPDATE).length;
const sortedPackages = pkgs.sort((a, b) => {
$: filterExists = Object.keys(stateFilters).some((k) => stateFilters[k]);
$: packages = $allPackages
.sort((a, b) => {
if (sortBy === 'popularity') {
const aPop = +a.dl_count + a.installs;
const bPop = +b.dl_count + b.installs;
@ -36,100 +31,63 @@
const bDate = new Date(b.last_modified);
return sortDirection === 'asc' ? +aDate - +bDate : +bDate - +aDate;
}
})
.filter((pkg) => {
if (!filterExists || pkg.state === PackageStates.INSTALLING) return true;
return stateFilters[pkg.state];
});
const filteredStates = [
PackageStates.NEEDS_UPDATE
];
switch (tab) {
case "INSTALLED":
case "INSTALLED_WITH_UPDATES":
if (tab === "INSTALLED") filteredStates.push(PackageStates.INSTALLED);
packages = sortedPackages.filter((p) => filteredStates.includes(p.state!));
break;
case "ALL":
default:
packages = sortedPackages;
break;
}
};
packagesStore.subscribe(setPackages);
const onSort = (opt: string, dir: 'asc' | 'desc') => {
sortBy = opt;
sortDirection = dir;
setPackages(packages);
};
const switchTab = (nextTab: "ALL" | "INSTALLED" | "INSTALLED_WITH_UPDATES") => {
tab = nextTab;
setPackages($packagesStore);
}
</script>
<header class="flex items-center justify-between my-4">
<h1 class="text-primary text-4xl font-bold">{title}</h1>
<!-- <header class="flex items-center justify-between z-50 w-full absolute">
<h1 class="text-primary text-4xl font-bold">{$t("home.all-packages")}</h1>
<div class="flex">
<section class="border border-gray mr-2 rounded-sm h-10 text-gray font-thin flex">
<button on:click={() => switchTab("ALL")} class={`px-2 ${tab === "ALL" && "active"}`}>All packages</button>
<button on:click={() => switchTab("INSTALLED")} class={`px-2 ${tab === "INSTALLED" && "active"}`}>installed only</button>
{#if pkgNeedsUpdateCount}
<button on:click={() => switchTab("INSTALLED_WITH_UPDATES")} class={`px-2 ${tab === "INSTALLED_WITH_UPDATES" && "active"}`}>
<div class="flex justify-center align-middle">
<div>updates</div>
<div class="bg-red text-white bg-[red] rounded-sm text-xs h-6 leading-6 px-1 ml-2">{pkgNeedsUpdateCount}</div>
</div>
</button>
{/if}
</section>
<section class="border-gray h-10 w-48 border rounded-sm">
<SortingButtons {onSort} />
</section>
</div>
</header>
<ul class="grid grid-cols-3 bg-black">
{#if packages.length > 0}
{#each packages as pkg, index}
{#if index < limit}
<div class={pkg.state === PackageStates.INSTALLING ? 'animate-pulse' : ''}>
<Package
{pkg}
onClick={async () => {
try {
pkg.state = PackageStates.INSTALLING;
await installPackage(pkg);
trackInstall(pkg.full_name);
pkg.state = PackageStates.INSTALLED;
packagesStore.updatePackage(pkg.full_name, {
state: PackageStates.INSTALLED, // this would also mean its the latest version
});
} catch (error) {
let message = 'Unknown Error'
if (error instanceof Error) message = error.message
trackInstallFailed(pkg.full_name, message || "unknown");
}
}}
/>
</div>
{/if}
{/each}
{:else}
{#each Array(9) as _}
<section class="h-50 border-gray border p-4">
<Preloader />
</section>
{/each}
</header> -->
<div>
<ul class="grid grid-cols-3 gap-2 bg-black">
{#if packages.length > 0}
{#each packages as pkg, index}
{#if index < limit}
<div class={pkg.state === PackageStates.INSTALLING ? 'animate-pulse' : ''}>
<Package
{pkg}
onClick={async () => {
try {
pkg.state = PackageStates.INSTALLING;
await installPackage(pkg);
trackInstall(pkg.full_name);
pkg.state = PackageStates.INSTALLED;
packagesStore.updatePackage(pkg.full_name, {
state: PackageStates.INSTALLED, // this would also mean its the latest version
});
} catch (error) {
let message = 'Unknown Error'
if (error instanceof Error) message = error.message
trackInstallFailed(pkg.full_name, message || "unknown");
}
}}
/>
</div>
{/if}
{/each}
{:else}
{#each Array(9) as _}
<section class="h-50 border-gray border p-4">
<Preloader />
</section>
{/each}
{/if}
</ul>
{#if limit < packages.length }
<footer class="w-full flex border border-gray h-16">
<button class="flex-grow h-16" on:click={() => limit += loadMore }>show more</button>
</footer>
{/if}
</ul>
{#if limit < packages.length }
<footer class="w-full flex border border-gray h-16">
<button class="flex-grow h-16" on:click={() => limit += loadMore }>show more</button>
</footer>
{/if}
</div>
<style>
button {

View file

@ -2,25 +2,27 @@
import '$appcss';
import { t } from '$libs/translations';
export let onSort: (opt: string, dir: 'asc' | 'desc') => void;
let sortBy = 'popularity';
type SortOption = "popularity" | "most recent";
export let onSort: (opt: SortOption, dir: 'asc' | 'desc') => void;
let sortBy: SortOption = "popularity";
let sortDirection: 'asc' | 'desc' = 'desc';
const sortOptions = ['popularity', 'most recent'];
const sortOptions: SortOption[] = ["popularity", "most recent"];
const optionLabels = {
[sortOptions[0]]: $t("sorting.popularity"),
[sortOptions[1]]: $t("sorting.most-recent")
}
const setSortBy = (opt: string) => {
const setSortBy = (opt: SortOption) => {
sortBy = opt;
if (onSort) {
onSort(sortBy, sortDirection);
}
};
const setSortDir = (opt: string, dir: 'asc' | 'desc') => {
const setSortDir = (opt: SortOption, dir: 'asc' | 'desc') => {
sortDirection = dir;
setSortBy(opt);
};

View file

@ -60,7 +60,7 @@
<header class="text-primary p-4 text-lg">
Top Package Results ({packages.length})
</header>
<ul class="grid grid-cols-3">
<ul class="grid grid-cols-3 gap-2">
{#if packages.length > 0}
{#each packages as pkg}
<div class={pkg.state === PackageStates.INSTALLING ? 'animate-pulse' : ''}>

View file

@ -10,14 +10,14 @@ import semverCompare from "semver/functions/compare";
export default function initPackagesStore() {
let initialized = false;
const { subscribe, set, update } = writable<GUIPackage[]>([]);
const packages: GUIPackage[] = [];
const packages = writable<GUIPackage[]>([]);
// const packages: GUIPackage[] = [];
let packagesIndex: Fuse<GUIPackage>;
if (!initialized) {
initialized = true;
getPackages().then((pkgs) => {
set(pkgs);
packages.set(pkgs);
packagesIndex = new Fuse(pkgs, {
keys: ["name", "full_name", "desc", "categories"]
});
@ -30,10 +30,8 @@ export default function initPackagesStore() {
});
}
subscribe((v) => packages.push(...v));
const updatePackage = (full_name: string, props: Partial<GUIPackage>) => {
update((pkgs) => {
packages.update((pkgs) => {
const i = pkgs.findIndex((pkg) => pkg.full_name === full_name);
if (i >= 0) {
pkgs[i] = {
@ -77,26 +75,35 @@ To read more about this package go to [${guiPkg.homepage}](${guiPkg.homepage}).
const syncPackageBottlesAndState = async (pkgName: string) => {
const bottles = await getPackageBottles(pkgName);
const pkg = packages.find((p) => p.full_name === pkgName);
const availableVersions = bottles
.map(({ version }) => version)
.sort((a, b) => semverCompare(b, a));
packages.update((pkgs) => {
const i = pkgs.findIndex((pkg) => pkg.full_name === pkgName);
if (i >= 0) {
const pkg = pkgs[i];
const installedVersions = pkg?.installed_versions?.sort((a, b) => semverCompare(b, a)) || [];
const availableVersions = bottles
.map(({ version }) => version)
.sort((a, b) => semverCompare(b, a));
updatePackage(pkgName, {
available_versions: availableVersions,
state:
availableVersions[0] === installedVersions[0]
? PackageStates.INSTALLED
: PackageStates.NEEDS_UPDATE
const installedVersions =
pkg?.installed_versions?.sort((a, b) => semverCompare(b, a)) || [];
pkgs[i] = {
...pkg,
available_versions: availableVersions,
state:
availableVersions[0] === installedVersions[0]
? PackageStates.INSTALLED
: PackageStates.NEEDS_UPDATE
};
}
return pkgs;
});
};
return {
packages,
subscribe,
subscribe: packages.subscribe,
search: async (term: string, limit = 5): Promise<GUIPackage[]> => {
if (!term || !packagesIndex) return [];
// TODO: if online, use algolia else use Fuse
@ -105,7 +112,7 @@ To read more about this package go to [${guiPkg.homepage}](${guiPkg.homepage}).
return matchingPackages;
},
subscribeToPackage: (slug: string, cb: (pkg: GUIPackage) => void) => {
subscribe((pkgs) => {
packages.subscribe((pkgs) => {
const foundPackage = pkgs.find((p) => p.slug === slug) as GUIPackage;
if (foundPackage) {
cb(foundPackage);

View file

@ -9,6 +9,7 @@
"store-search-placeholder": "type to search",
"search": "search",
"home": {
"all-packages": "All packages",
"discover-title": "DISCOVER",
"asset-title": "ASSET TYPE",
"tutorials-title": "TUTORIALS",
@ -63,7 +64,7 @@
},
"view-all": "view all",
"sorting": {
"label": "Filters",
"label": "Sort by",
"popularity": "Most popular",
"most-recent": "Most recent"
},

View file

@ -6,11 +6,11 @@
import type { Package, Developer } from "@tea/ui/types";
export enum PackageStates {
AVAILABLE,
INSTALLED,
INSTALLING,
UNINSTALLED,
NEEDS_UPDATE
AVAILABLE = "AVAILABLE",
INSTALLED = "INSTALLED",
INSTALLING = "INSTALLING",
UNINSTALLED = "UNINSTALLED",
NEEDS_UPDATE = "NEEDS_UPDATE"
}
export type GUIPackage = Package & {

View file

@ -63,8 +63,8 @@
<div class="content">
<TeaUpdate />
</div>
<div class="content">
<slot />
<div class="content p-2">
<slot/>
</div>
<SearchPopupResults />
</section>
@ -85,21 +85,6 @@
box-sizing: border-box;
}
@media screen and (min-width: 1440px) {
figure {
background-size: cover;
background-repeat: repeat-y;
}
.content {
padding: 0vw 3.6vw !important;
}
}
@media screen and (max-width: 1440px) {
.content {
padding: 0vw 3.33vw;
}
}
slot {
z-index: 1;
}

View file

@ -1,18 +1,92 @@
<!-- home / discover / welcome page -->
<script lang="ts">
import '$appcss';
import { t } from '$libs/translations';
import { t } from '$libs/translations';
import { navStore, packagesStore, notificationStore } from '$libs/stores';
import Packages from '$components/packages/packages.svelte';
import Resources from '$components/resources/resources.svelte';
import Checkbox from "@tea/ui/checkbox/checkbox.svelte";
import { PackageStates } from '$libs/types';
import SortingButtons from "$components/search-packages/sorting-buttons.svelte";
const { sideNavOpen } = navStore;
let stateFilters = {
[PackageStates.AVAILABLE]: false,
[PackageStates.NEEDS_UPDATE]: false,
[PackageStates.INSTALLED]: false,
}
let sortBy: "popularity" | "most recent" = "popularity";
let sortDirection: "asc" | "desc" = "desc";
const { packages } = packagesStore;
$: needsUpdateCount = $packages.filter((p) => p.state === PackageStates.NEEDS_UPDATE).length;
</script>
<div>
<section class="mt-8 mb-8">
<Packages title={$t("home.discover-title")}/>
</section>
<section class="mt-8 mb-8">
<Resources/>
</section>
<div id="package-container">
<Packages stateFilters={{
...stateFilters,
[PackageStates.NEEDS_UPDATE]: needsUpdateCount ? stateFilters[PackageStates.NEEDS_UPDATE] : false,
}} {sortBy} {sortDirection}/>
</div>
<aside class={`border border-gray p-2 ${$notificationStore.length ? "lower": ""}`}>
<h2 class="text-xl text-primary">Search OSS</h2>
<h3 class="text-lg text-primary">Status</h3>
<ul>
<li>
<Checkbox label={"Not installed"} bind:checked={stateFilters[PackageStates.AVAILABLE]} />
</li>
<li>
<Checkbox label={"Installed"} bind:checked={stateFilters[PackageStates.INSTALLED]} />
</li>
{#if needsUpdateCount}
<li>
<Checkbox label={`Update Available [${needsUpdateCount}]`} bind:checked={stateFilters[PackageStates.NEEDS_UPDATE]} />
</li>
{/if}
</ul>
</aside>
<header class={`transition-all px-2 flex justify-between items-center align-middle ${$sideNavOpen ? "min": ""} ${$notificationStore.length ? "lower": ""}`}>
<h1 class="text-primary mt-4 text-2xl font-bold">{$t("home.all-packages")}</h1>
<section class="border-gray mt-4 mr-4 h-10 w-48 border rounded-sm">
<SortingButtons onSort={(prop, dir) => {
sortBy = prop;
sortDirection = dir;
}} />
</section>
</header>
<style>
#package-container {
padding-top: 36px;
width: calc(100% - 200px);
margin-left: 200px;
}
aside {
position: fixed;
top: 100px;
left: 10px;
height: calc(100% - 110px);
width: 190px;
}
aside.lower {
top: 140px;
}
header {
position: fixed;
top: 40px;
left: 0px;
height: 50px;
width: 100%;
background-image: linear-gradient(black, rgba(0,0,0,0.6), rgba(0,0,0,0));
}
header.lower {
top: 80px;
}
header.min {
width: 75%;
}
</style>

View file

@ -8,7 +8,7 @@
export let active = false;
export let type: "ghost" | "plain" = "ghost";
export let color: "primary" | "secondary" = "primary";
export let color: "primary" | "secondary" | "green" = "primary";
export let loading = false;
</script>
@ -30,13 +30,18 @@
}
button.plain.primary {
color: black;
background-color: #00ffd0;
color: black;
}
button.plain.secondary {
color: white;
background-color: #8000ff;
color: white;
}
button.plain.green {
background-color: #00a517;
color: white;
}
button.active {
color: black;
@ -52,4 +57,7 @@
button.secondary:hover {
background-color: #8000ff;
}
button.green:hover {
background-color: #00a517;
}
</style>

View file

@ -0,0 +1,81 @@
<script lang="ts">
export let label: string;
export let checked: boolean;
</script>
<label class="flex justify-between text-sm">
<div>{label}</div>
<div class="checkbox">
<input type="checkbox" bind:checked />
<span class="checkmark" />
</div>
</label>
<style>
label {
position: relative;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* Hide the browser's default checkbox */
label .checkbox {
position: relative;
width: 25px;
height: 25px;
}
label input {
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
/* Create a custom checkbox */
.checkmark {
position: absolute;
top: 2px;
left: 0;
height: 14px;
width: 14px;
background-color: #eee;
}
/* On mouse-over, add a grey background color */
label:hover input ~ .checkmark {
background-color: #ccc;
}
/* When the checkbox is checked, add a blue background */
label input:checked ~ .checkmark {
background-color: #00ffd0;
}
/* Create the checkmark/indicator (hidden when not checked) */
.checkmark:after {
content: "";
position: absolute;
display: none;
}
/* Show the checkmark when checked */
label input:checked ~ .checkmark:after {
display: block;
}
/* Style the checkmark/indicator */
label .checkmark:after {
left: 5px;
top: 2px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 3px 3px 0;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
</style>

View file

@ -10,37 +10,38 @@
export let ctaLabel: string;
export let progessLoading = 0;
export let ctaType: "ghost" | "plain" = "ghost";
export let ctaColor: "green" | "secondary" = "secondary";
export let onClickCTA = () => {
console.log("do nothing");
};
</script>
<section class="package-card relative h-auto border border-gray p-4">
<section class="package-card relative h-auto border border-gray">
<a href={link}>
<figure class="relative">
<ImgLoader
class="pkg-image"
class="pkg-image object-cover font-sono"
src={!pkg.thumb_image_url.includes("https://tea.xyz")
? "https://tea.xyz/Images/package-thumb-nolabel4.jpg"
: pkg.thumb_image_url}
alt={pkg.name}
/>
<article class="card-thumb-label">
<i class="icon-tea-logo-iconasset-1">
<!-- TODO: replace with icon.svg -->
</i>
<h3>{pkg.name}</h3>
{#if pkg.maintainer}
<h4>&#x2022;&nbsp;{pkg.maintainer}</h4>
{/if}
</article>
</figure>
<article class="card-thumb-label">
<h3 class="text-bold text-xl font-bold text-white">{pkg.name}</h3>
{#if pkg.maintainer}
<h4 class="text-sm font-light">&#x2022;&nbsp;{pkg.maintainer}</h4>
{/if}
{#if pkg.desc}
<p class="text-xs font-thin line-clamp-2">{pkg.desc}</p>
{/if}
</article>
</a>
<footer class="mt-4 flex items-stretch justify-between gap-2">
<footer class="absolute bottom-0 left-0 flex w-full items-stretch justify-between gap-2 p-2">
<div>
<p>
<span class="pk-version text-xs">v{pkg.version}</span>
<span class="pk-version text-xs font-extralight">v{pkg.version}</span>
<!--
TODO: uncomment once install counts improve
<br>
@ -48,10 +49,10 @@
</p>
</div>
<Button
class="w-1/2 border border-gray p-2 font-machina"
class="h-8 w-1/2 border border-gray p-2 font-machina text-xs"
onClick={onClickCTA}
type={ctaType}
color="secondary"
color={ctaColor}
>
{ctaLabel}
</Button>
@ -70,11 +71,12 @@
background-color: #1a1a1a;
transition: all 0.3s;
width: 100%;
height: 230px;
}
figure {
position: relative;
min-height: 150px;
height: 70px;
}
.package-card :global(.pkg-image) {
@ -82,50 +84,16 @@
width: 100%;
height: 100%;
}
.card-thumb-label i {
font-size: 1.5vw;
color: black;
}
.card-thumb-label h3 {
color: black;
font-size: 1.8vw;
line-height: 1.8vw;
margin: 0px 0px 0.5vw 0vw;
padding: 0px;
}
.card-thumb-label {
position: absolute;
background: rgba(255, 255, 255, 0.9);
left: 0;
bottom: 0vw;
background: rgba(0, 0, 0, 0.9);
padding: 1.116vw;
text-align: left;
width: 90%;
height: 40%;
width: 100%;
height: 110px;
}
.card-thumb-label h4 {
color: black;
font-size: 0.9vw;
line-height: 1vw;
margin: 0px;
padding: 0px;
.card-thumb-label p {
color: white;
}
/* button {
background-color: #1a1a1a;
border: 0.5px solid #ffffff;
color: #fff;
text-decoration: none;
text-transform: uppercase;
min-width: 120px;
transition: 0.1s linear;
}
button:hover {
background-color: #8000ff;
box-shadow: inset 0vw 0vw 0vw 0.223vw #1a1a1a !important;
} */
</style>

View file

@ -1,17 +1,20 @@
/** @type {import('tailwindcss').Config} */
const green = "#00ffd0";
const teal = "#00ffd0";
const black = "#1a1a1a";
const white = "#fff";
const gray = "#949494";
const purple = "#8000FF";
const green = "#00A517";
module.exports = {
content: ["./src/**/*.{html,js,svelte,ts}"],
theme: {
colors: {
primary: green,
primary: teal,
secondary: purple,
accent: purple,
green,
teal,
purple: {
700: purple,
900: "#B076EC"