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 search functionality #73

Merged
merged 19 commits into from
Jun 17, 2024
Merged
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
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[svelte]": {
"editor.defaultFormatter": "svelte.svelte-vscode"
}
}
Binary file modified bun.lockb
Binary file not shown.
15 changes: 10 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro",
"prepare": "husky"
"prepare": "husky",
"convertCustomIconsToJson": "bun src/lib/iconify/convertCustomIconsToJson.ts"
},
"lint-staged": {
"src/**/*": "bun prettier --plugin=prettier-plugin-astro . --write"
Expand All @@ -19,16 +20,20 @@
"@astrojs/check": "^0.5.0",
"@astrojs/sitemap": "^3.1.1",
"@astrojs/svelte": "^5.3.0",
"@catppuccin/palette": "^1.1.0",
"@iconify-json/ph": "^1.1.12",
"@iconify-json/simple-icons": "^1.1.102",
"astro": "^4.5.16",
"astro-icon": "^1.1.0",
"fuse.js": "^7.0.0",
"svelte": "^4.2.12",
"typescript": "^5.4.4",
"yaml": "^2.4.0"
},
"devDependencies": {
"@catppuccin/palette": "^1.1.0",
"astro-icon": "^1.1.0",
"@iconify-json/ph": "^1.1.13",
"@iconify-json/simple-icons": "^1.1.102",
"@iconify/svelte": "^4.0.2",
"@iconify/tools": "^4.0.4",
"@iconify/types": "^2.0.0",
"@rollup/plugin-yaml": "^4.1.2",
"husky": "latest",
"lint-staged": "^15.2.2",
Expand Down
42 changes: 42 additions & 0 deletions src/components/PortExplorer.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<script lang="ts">
import type { Port, Userstyle } from "../lib/ports";
import SearchBar from "./SearchBar.svelte";
import PortGrid from "./PortGrid.svelte";
import Fuse from "fuse.js";

export let ports: Array<Port | Userstyle>;
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 },
],
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
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);
}, 25);
}
</script>

<SearchBar bind:searchTerm {handleInput} />
<PortGrid bind:portGrid bind:searchTerm>
<svelte:fragment slot="no-results">
<slot name="no-results" />
</svelte:fragment>
</PortGrid>
19 changes: 19 additions & 0 deletions src/components/PortGrid.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script lang="ts">
import PortCard from "./cards/Port.svelte";
import type { Port, Userstyle } from "../lib/ports";

export let portGrid: Array<Port | Userstyle> | undefined;
export let searchTerm: string;
</script>

<div class="port-grid">
{#key portGrid}
{#if searchTerm && portGrid && portGrid.length === 0}
<slot name="no-results" />
{:else if portGrid && portGrid.length > 0}
{#each portGrid as port (port.key)}
<PortCard {port} />
{/each}
{/if}
{/key}
</div>
54 changes: 54 additions & 0 deletions src/components/SearchBar.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<script lang="ts">
import MagnifyingGlass from "./svgs/MagnifyingGlass.svelte";

export let searchTerm: string;
export let handleInput: () => void;
</script>

<div class="search-bar">
<MagnifyingGlass />
<input
type="text"
id="search-field"
aria-label="Search"
placeholder="Search port or category..."
autocomplete="off"
bind:value={searchTerm}
on:input={handleInput} />
</div>

<style lang="scss">
@use "../styles/utils";

.search-bar {
background-color: var(--mantle);
display: flex;
gap: var(--space-xs);
align-items: center;
margin-block-end: var(--space-md);
border-radius: var(--border-radius-normal);
padding-inline: var(--space-xs);

input {
padding: var(--space-sm) 0;
background-color: inherit;
border-color: inherit;
color: inherit;
border-radius: inherit;
border: none;
outline: none;
width: 100%;
font-size: 2rem;
}
}

.search-bar:focus-within {
outline: solid;
outline-width: 2px;
outline-color: var(--blue);
}

input::placeholder {
color: var(--overlay2);
}
</style>
Original file line number Diff line number Diff line change
@@ -1,36 +1,25 @@
---
import type { ColorName } from "@catppuccin/palette";
import { type Category } from "../../lib/ports";

import { Icon } from "astro-icon/components";
import PillList from "../lists/Pills.astro";

interface Props {
title: string[] | string;
link: string;
icon: string | undefined;
categories: Category[];
color?: ColorName;
}

const { title, link, icon, categories, color } = Astro.props;

function getIcon(icon: string | undefined) {
if (!icon) return "ph:cube-fill";
if (icon.endsWith(".svg")) return `ports/${icon.split(".")[0]}`;
return `simple-icons:${icon}`;
}
---

<a href={link} class="port-card">
<script lang="ts">
import type { Port, Userstyle } from "../../lib/ports";
import PillList from "../lists/Pills.svelte";
import Icon from "@iconify/svelte";
export let port: Port | Userstyle;
</script>

<a href={port.link} class="port-card">
<div class="port-header">
<p class="port-name">{Array.isArray(title) ? title.join(", ") : title}</p>
<div class="port-icon" style=`color: var(--${color});`>
<Icon name={getIcon(icon)} size={24} />
</div>
<p class="port-name">{Array.isArray(port.name) ? port.name.join(", ") : port.name}</p>
<Icon
color="var(--{port.color})"
width={24}
height={24}
icon={{
body: port.icon ?? "",
width: 24,
height: 24,
}} />
</div>

<PillList list={categories.map((category: Category) => `${category.name}`)} />
<PillList list={port.categories.map((category) => `${category.name}`)} />
</a>

<style lang="scss">
Expand Down Expand Up @@ -62,7 +51,6 @@ function getIcon(icon: string | undefined) {
}
}

figure,
p {
margin: 0;
padding: 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
---
import type { ColorName } from "@catppuccin/palette";

interface Props {
list: string[];
color?: ColorName;
}

const { list } = Astro.props;
---
<script lang="ts">
export let list: string[] = [];
</script>

<ul class="pill-list">
{list.map((entry) => <li class="pill">{entry}</li>)}
{#each list as entry}
<li class="pill">{entry}</li>
{/each}
</ul>

<style lang="scss">
Expand Down
File renamed without changes.
15 changes: 15 additions & 0 deletions src/components/svgs/MagnifyingGlass.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="url(#accent)" viewBox="0 0 256 256">
<defs>
<linearGradient id="accent" x1="0%" x2="0%" y1="0%" y2="100%">
<stop offset="0%" stop-color="var(--red)" />
<stop offset="20%" stop-color="var(--peach)" />
<stop offset="40%" stop-color="var(--yellow)" />
<stop offset="60%" stop-color="var(--green)" />
<stop offset="80%" stop-color="var(--sapphire)" />
<stop offset="100%" stop-color="var(--lavender)" />
</linearGradient>
</defs>
<path
d="M232.49,215.51,185,168a92.12,92.12,0,1,0-17,17l47.53,47.54a12,12,0,0,0,17-17ZM44,112a68,68,0,1,1,68,68A68.07,68.07,0,0,1,44,112Z"
></path>
</svg>
2 changes: 1 addition & 1 deletion src/layouts/Home.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import "../styles/global.scss";

import Footer from "./imports/Footer.astro";

import Logo from "../components/Logo.astro";
import Logo from "../components/svgs/Logo.astro";
import AccentBar from "../components/AccentBar.astro";

interface Props {
Expand Down
2 changes: 1 addition & 1 deletion src/layouts/Layout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const isVercelProd = import.meta.env.VERCEL_PROD;
</div>
</section>

<div class="content-wrapper">
<div class="content-wrapper main-container">
<slot />
</div>

Expand Down
2 changes: 1 addition & 1 deletion src/layouts/imports/Footer.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
import Logo from "../../components/Logo.astro";
import Logo from "../../components/svgs/Logo.astro";
import Link from "../../components/Link.astro";

import { footer as navLinkList } from "../../contents/navigation";
Expand Down
2 changes: 1 addition & 1 deletion src/layouts/imports/Header.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
import Logo from "../../components/Logo.astro";
import Logo from "../../components/svgs/Logo.astro";

import { header as navLinkList } from "../../contents/navigation";
---
Expand Down
29 changes: 29 additions & 0 deletions src/lib/getIcon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { IconifyJSON } from "@iconify/types";

import customIconsJson from "./iconify/icons.json";
import simpleIconsJson from "@iconify-json/simple-icons/icons.json";
const customIcons = customIconsJson as IconifyJSON;
const simpleIcons = simpleIconsJson as IconifyJSON;

// For whatever reason, importing ph-icons.json doesn't work, so I'm just going to copy the contents of the icon here.
const cubeFillIcon =
'<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" width="24" height="24" viewBox="0 0 256 256"><path d="M223.68,66.15,135.68,18a15.88,15.88,0,0,0-15.36,0l-88,48.17a16,16,0,0,0-8.32,14v95.64a16,16,0,0,0,8.32,14l88,48.17a15.88,15.88,0,0,0,15.36,0l88-48.17a16,16,0,0,0,8.32-14V80.18A16,16,0,0,0,223.68,66.15ZM128,120,47.65,76,128,32l80.35,44Zm8,99.64V133.83l80-43.78v85.76Z"></path>';

export const getIcon = (icon: string | undefined) => {
// When there's no icon provided
if (!icon) return cubeFillIcon;
// When a custom icon is provided
if (icon.endsWith(".svg")) {
return customIcons.icons[icon.split(".")[0]].body;
}
// When a simple icon exists for the port
if (icon in simpleIcons.icons) {
return simpleIcons.icons[icon].body;
}
// When a simple icon exists as an alias for the port, the parent must be used to get the body of the SVG.
if (simpleIcons.aliases && icon in simpleIcons.aliases) {
return simpleIcons.icons[simpleIcons.aliases[icon].parent].body;
}
// If all else fails, return the cube fill icon
return cubeFillIcon;
};
50 changes: 50 additions & 0 deletions src/lib/iconify/convertCustomIconsToJson.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { promises as fs } from "fs";
import { cleanupSVG, parseColors, isEmptyColor, runSVGO, deOptimisePaths, importDirectory } from "@iconify/tools";

(async () => {
const source = "src/icons/ports";
const prefix = "ctp";
const target = "src/lib/iconify/icons.json";

// Load icon set
const iconSet = await importDirectory(source, { prefix });

// Parse all icons
await iconSet.forEach((name, type) => {
// Do not parse aliases
if (type !== "icon") {
return;
}

const svg = iconSet.toSVG(name);
if (!svg) {
return;
}

// This will throw an exception if icon is invalid
cleanupSVG(svg);

parseColors(svg, {
defaultColor: "currentColor",
callback: (_, colorStr, color) => {
return !color || isEmptyColor(color) ? colorStr : "currentColor";
},
});

// Optimise
runSVGO(svg);

// Update paths for compatibility with old software
deOptimisePaths(svg);

// SVG instance is detached from icon set, so changes to
// icon are not stored in icon set automatically.

// Update icon in icon set
iconSet.fromSVG(name, svg);
});

// Save icon set
const iconSetContent = iconSet.export();
await fs.writeFile(target, JSON.stringify(iconSetContent, null, "\t"), "utf8");
})();
Loading