Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ports): add filter functionality #75

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"astro": "^4.5.16",
"fuse.js": "^7.0.0",
"svelte": "^4.2.12",
"svelte-select": "^5.8.3",
"typescript": "^5.4.4",
"yaml": "^2.4.0"
},
Expand Down
111 changes: 99 additions & 12 deletions src/components/PortExplorer.svelte
Original file line number Diff line number Diff line change
@@ -1,40 +1,127 @@
<script lang="ts">
import type { Port, Userstyle } from "../lib/ports";
import type { CategoryWithPortCount, HandleCategory, Platform, Port, Userstyle } from "../lib/ports";
import SearchBar from "./SearchBar.svelte";
import PortGrid from "./PortGrid.svelte";
import Fuse from "fuse.js";
import CategorySelect from "./dropdowns/CategorySelect.svelte";
import PlatformSelect from "./dropdowns/PlatformSelect.svelte";

export let platforms: Array<Platform>;
export let ports: Array<Port | Userstyle>;
export let categories: Array<CategoryWithPortCount>;
let portGrid: Array<Port | Userstyle> | undefined = undefined;
let debounceTimeout: NodeJS.Timeout;

const fuse = new Fuse(ports, {
keys: [
{ name: "key", weight: 1 },
{ name: "categories.name", weight: 0.8 },
{ name: "name", weight: 0.4 },
{ name: "name", weight: 1 },
{ name: "key", weight: 0.5 },
],
includeScore: false,
threshold: 0.3,
});

const url = new URL(window.location.href);
let searchTerm = url.searchParams.get("q") ?? "";
handleInput();

function handleInput() {
// Keep the URL in sync with the search bar
/* --- Category Initial State --- */

// Default to category "everything"
const foundCategory = categories.find((c) => c.key === "everything")!;
let category: HandleCategory = { value: foundCategory.key, label: foundCategory.nameWithCount };
// If the URL has a category query parameter, use that instead
if (url.searchParams.has("category")) {
category.value = url.searchParams.get("category")!;
} else {
// Make sure the URL is in sync with the category
url.searchParams.set("category", category.value);
}

// Category is guaranteed to be set to atleast "everything" or another category by now
if (category.value === "everything") {
// No need to change the default view if the category is "everything"
portGrid = ports;
} else {
// If the category is not "everything", filter the ports to only show the selected category
portGrid = ports.filter((port) => port.categories.some((c) => c.key === category.value));
}
// Make sure to update the fuse collection to the only the filtered ports
fuse.setCollection(portGrid);

/* --- Category Initial State --- */

/* --- Search Initial State --- */

// Default to an empty search term
let searchTerm = "";
if (url.searchParams.has("q")) {
searchTerm = url.searchParams.get("q")!;
// If a search term is present in the URL, search the ports for that term.
// This is guaranteed to be only the selected category since we did that logic above
if (searchTerm) {
portGrid = fuse.search(searchTerm).map((key) => key.item);
}
}

/* --- Search Initial State --- */

/* --- Category Event Handler --- */

const handleCategory = (e: { detail: HandleCategory }) => {
console.log(e);
// Keep the URL in sync with the category dropdown
url.searchParams.set("category", e.detail.value);
window.history.pushState(null, "", url.toString());
category.value = e.detail.value;

// If the user selects "everything", show all the ports
if (category.value === "everything") {
portGrid = ports;
} else {
// Otherwise, filter the ports to only show the selected category
portGrid = ports.filter((port) => port.categories.some((c) => c.key === category.value));
}

// Make sure to update the fuse collection to only the filtered ports
fuse.setCollection(portGrid);
};

/* --- Category Event Handler --- */

/* --- Search Event Handler --- */

const handleSearch = () => {
// Keep the URL in sync with the search input
url.searchParams.set("q", searchTerm);
window.history.pushState(null, "", url.toString());

clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(() => {
searchTerm ? (portGrid = fuse.search(searchTerm).map((key) => key.item)) : (portGrid = ports);
// if the search term is empty, filter the ports to only show the selected category
if (searchTerm === "") {
if (category.value === "everything") {
portGrid = ports;
} else {
portGrid = ports.filter((port) => port.categories.some((c) => c.key === category.value));
}
} else {
// Otherwise, search within the selected category
portGrid = fuse.search(searchTerm).map((key) => key.item);
}
}, 25);
}
};

/* --- Search Event Handler --- */
</script>

<SearchBar bind:searchTerm {handleInput} />
<!-- LAYOUT/CSS TODO -->
<div style="display: flex; gap: 2rem; margin-block-end: var(--space-md); flex-wrap: wrap">
<div style="flex: 1; min-width: min(100%, 40em);">
<PlatformSelect {platforms} />
</div>
<div style="flex: 2; min-width: min(100%, 20em); height: 100%">
<CategorySelect {categories} bind:category {handleCategory} />
</div>
</div>
<SearchBar bind:searchTerm handleInput={handleSearch} />
<PortGrid bind:portGrid bind:searchTerm>
<svelte:fragment slot="no-results">
<slot name="no-results" />
Expand Down
4 changes: 2 additions & 2 deletions src/components/SearchBar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
type="text"
id="search-field"
aria-label="Search"
placeholder="Search port or category..."
placeholder="Search port..."
autocomplete="off"
bind:value={searchTerm}
on:input={handleInput} />
Expand All @@ -38,7 +38,7 @@
border: none;
outline: none;
width: 100%;
font-size: 2rem;
font-size: 2.2rem;
}
}

Expand Down
33 changes: 33 additions & 0 deletions src/components/dropdowns/CategorySelect.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<script lang="ts">
import Select from "svelte-select";
import type { CategoryWithPortCount } from "../../lib/ports.ts";
import type { HandleCategory } from "../../lib/ports.ts";

export let category: HandleCategory;
export let handleCategory;
export let categories: CategoryWithPortCount[];
let items = categories.map((c) => ({
value: c.key,
label: c.nameWithCount,
}));
</script>

<Select
--background="var(--mantle)"
--font-size="2.2rem"
--placeholder-color="var(--overlay2)"
--border-radius="var(--border-radius-normal)"
--border="none"
--border-hover="none"
--border-focused="2px solid var(--blue)"
--item-hover-bg="var(--surface1)"
--list-background="var(--mantle)"
--list-border-radius="var(--border-radius-normal)"
--clear-icon-color="var(--red)"
showChevron={true}
clearable={false}
placeholder="Select category"
{items}
bind:value={category}
on:change={handleCategory}>
</Select>
67 changes: 67 additions & 0 deletions src/components/dropdowns/PlatformSelect.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<script lang="ts">
import Select from "svelte-select";
import type { Platform } from "../../lib/ports.ts";

export let platforms: Platform[];
let items = platforms.map((p) => ({ value: p, label: p.charAt(0).toUpperCase() + p.slice(1) }));

let value = [];
let checked: Platform = [];
let isChecked = {};

$: computeValue(checked);
$: computeIsChecked(checked);

function computeIsChecked() {
isChecked = {};
checked.forEach((c) => (isChecked[c] = true));
}

function computeValue() {
value = checked.map((c) => items.find((i) => i.value === c));
}

function handleChange(e) {
if (e.type === "clear" && Array.isArray(e.detail)) checked = [];
else
checked.includes(e.detail.value)
? (checked = checked.filter((i) => i != e.detail.value))
: (checked = [...checked, e.detail.value]);
}
</script>

<Select
--background="var(--mantle)"
--font-size="2.1rem"
--placeholder-color="var(--overlay2)"
--border-radius="var(--border-radius-normal)"
--border="none"
--border-hover="none"
--border-focused="2px solid var(--blue)"
--item-hover-bg="var(--surface1)"
--multi-item-bg="color-mix(in srgb, var(--base), var(--subtext0) 10%)"
--multi-item-clear-icon-color="var(--maroon)"
--multi-item-border-radius="var(--border-radius-normal)"
--multi-item-active-outline="none"
--multi-item-outline="none"
--multi-item-gap="var(--space-xs)"
--multi-item-margin="var(--space-xxs)"
--list-background="var(--mantle)"
--list-border-radius="var(--border-radius-normal)"
--clear-icon-color="var(--red)"
{items}
{value}
placeholder="Select platform (TODO)"
multiple={true}
filterSelectedItems={false}
closeListOnChange={false}
showChevron={true}
on:select={handleChange}
on:clear={handleChange}>
<div class="item" slot="item" let:item>
<label for={item.value}>
<input type="checkbox" id={item.value} bind:checked={isChecked[item.value]} />
{item.label}
</label>
</div>
</Select>
2 changes: 1 addition & 1 deletion src/components/lists/Pills.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
@include utils.containerPadding(xxs-y);

border-radius: 9999px;
background-color: color-mix(in srgb, var(--base), var(--subtext0) 20%);
background-color: color-mix(in srgb, var(--base), var(--subtext0) 10%);

font-size: 80%;
}
Expand Down
33 changes: 31 additions & 2 deletions src/lib/ports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { PropertyBasedSet } from "./propertyBasedSet";
import { getIcon } from "./getIcon";
import type { ColorName } from "@catppuccin/palette";

export type HandleCategory = {
value: string;
label: string;
};

// Mostly auto-generated but have made manual edits to the types
// to stop TypeScript from complaining about the types. Future
// Hammy can deal with the consequences of this decision.
Expand All @@ -21,7 +26,7 @@ export interface ArchivedPort {
name: string;
reason: string;
categories: Category[];
platform: PlatformElement[] | "agnostic";
platform: Platform;
color: ColorName;
icon?: string;
key: string;
Expand All @@ -43,6 +48,8 @@ export enum PlatformElement {
Windows = "windows",
}

export type Platform = PlatformElement | "agnostic";

export interface Repository {
name: string;
url: string;
Expand All @@ -59,7 +66,7 @@ export interface Collaborator {
export interface Port {
name: string;
categories: Category[];
platform: PlatformElement[] | "agnostic";
platform: Platform;
color: ColorName;
key: string;
repository: Repository;
Expand Down Expand Up @@ -108,6 +115,8 @@ export interface FAQ {
answer: string;
}

export type CategoryWithPortCount = Category & { portCount: number; nameWithCount: string };

export const repositoriesYml = (await fetch(
"https://raw.githubusercontent.com/catppuccin/catppuccin/portscelain/pigeon/ports.porcelain.yml",
)
Expand All @@ -134,3 +143,23 @@ export const currentMaintainers: Collaborator[] = new PropertyBasedSet<Collabora
(p) => p["current-maintainers"],
),
).sorted();

export const categories = repositoriesYml.categories.map((category) => {
const portCount = ports.filter((port) => port.categories.some((c) => c.key === category.key)).length;
return {
...category,
portCount,
nameWithCount: `${category.name} (${portCount})`,
};
});
categories.push({
key: "everything",
name: "Ports",
description: "All ports and userstyles",
emoji: "🌐",
portCount: ports.length,
nameWithCount: `Ports (${ports.length})`,
});
categories.sort((a, b) => b.portCount - a.portCount);

export const platforms: Platform[] = [...Object.values(PlatformElement), "agnostic"];
5 changes: 2 additions & 3 deletions src/pages/ports.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
import { ports } from "../lib/ports";
import { platforms, categories, ports } from "../lib/ports";

import Layout from "../layouts/Layout.astro";

Expand All @@ -17,7 +17,6 @@ import Link from "../components/Link.astro";
Catppuccin offers ports for all sorts of different applications, tools, websites and just about anything that
comes to your mind!
</p>
<p>Total Ports: <strong>{ports.length}</strong></p>
</PageIntro>

<!-- Display ports without search bar for browsers without JavaScript enabled -->
Expand All @@ -27,7 +26,7 @@ import Link from "../components/Link.astro";
</div>
</noscript>

<PortExplorer {ports} client:only="svelte">
<PortExplorer {ports} {categories} {platforms} client:only="svelte">
<svelte:fragment slot="no-results">
<p>Sorry, we couldn't find any ports matching your search :(</p>
<p>
Expand Down