mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
feat: add favorites tools
This commit is contained in:
+6
-5
@@ -139,6 +139,9 @@
|
|||||||
удобнее чем sectioned buttons
|
удобнее чем sectioned buttons
|
||||||
- [ ] неудобно настраивать ползунками, некоторые инструменты удобнее настроить
|
- [ ] неудобно настраивать ползунками, некоторые инструменты удобнее настроить
|
||||||
через drag-n-drop. звучит сложно, но надо придумать процесс
|
через drag-n-drop. звучит сложно, но надо придумать процесс
|
||||||
|
- [ ] fav: текущий ui слишком дерганный — при клике на звёздочку элементы
|
||||||
|
мгновенно пересортировываются и под мышкой оказывается другой элемент.
|
||||||
|
Нужна или анимация, или механизм отложенного обновления позиций
|
||||||
- [ ] просмотр картинки в оригинальном размере, и возможно с зумом отдельных
|
- [ ] просмотр картинки в оригинальном размере, и возможно с зумом отдельных
|
||||||
областей, чтобы просмотреть мелкие артефакты, выбор разных подложек -
|
областей, чтобы просмотреть мелкие артефакты, выбор разных подложек -
|
||||||
черная\белая\цветные\серая для проверки прозрачных картинок
|
черная\белая\цветные\серая для проверки прозрачных картинок
|
||||||
@@ -175,11 +178,6 @@
|
|||||||
решения: node Vitest не имеет DOM/canvas, а e2e уже покрывает поведение.
|
решения: node Vitest не имеет DOM/canvas, а e2e уже покрывает поведение.
|
||||||
- [ ] Ошибка декодирования изображения из `createImageBitmap` пока показывается
|
- [ ] Ошибка декодирования изображения из `createImageBitmap` пока показывается
|
||||||
сырым сообщением браузера; нужен `ToolError`-маппер и i18n-ключ.
|
сырым сообщением браузера; нужен `ToolError`-маппер и i18n-ключ.
|
||||||
- [ ] **Идея 5. Избранные инструменты (fav tools)** — оценка S/M.
|
|
||||||
Тоггл-звёздочка на карточках каталога, результатах поиска и в шапке
|
|
||||||
страницы инструмента; список slug в localStorage. Избранное показывается
|
|
||||||
отдельной секцией сверху каталога и поднимается в выдаче поиска (бонус к
|
|
||||||
popularity при скоринге).
|
|
||||||
- [ ] **Идея 13. Контраст текста на акцентных кнопках — частично закрыт**. Тона
|
- [ ] **Идея 13. Контраст текста на акцентных кнопках — частично закрыт**. Тона
|
||||||
`--color-main`/`--color-accent` подняты под текущий порог
|
`--color-main`/`--color-accent` подняты под текущий порог
|
||||||
`palette.test.ts`; targeted-тест проходит. Light-тема всё ещё ниже AA 4.5
|
`palette.test.ts`; targeted-тест проходит. Light-тема всё ещё ниже AA 4.5
|
||||||
@@ -240,6 +238,9 @@
|
|||||||
|
|
||||||
## Платформа (API/PWA)
|
## Платформа (API/PWA)
|
||||||
|
|
||||||
|
- [ ] Vercel: переезд хостинга с GitHub Pages + глобальная статистика
|
||||||
|
популярности инструментов через Vercel KV (serverless-функция: INCR при
|
||||||
|
открытии инструмента, чтение топа для скоринга поиска).
|
||||||
- [ ] PWA: manifest + service worker, оффлайн-режим, установка (после S1)
|
- [ ] PWA: manifest + service worker, оффлайн-режим, установка (после S1)
|
||||||
- [ ] API: спайк серверного ядра (нативный Rust vs edge-wasm), ключи, free tier
|
- [ ] API: спайк серверного ядра (нативный Rust vs edge-wasm), ключи, free tier
|
||||||
(после wasm-фаз 5–7)
|
(после wasm-фаз 5–7)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { resolve } from "$app/paths";
|
import { resolve } from "$app/paths";
|
||||||
import { ArrowUpRight } from "@lucide/svelte";
|
import { ArrowUpRight, Star } from "@lucide/svelte";
|
||||||
import type { Component } from "svelte";
|
import type { Component } from "svelte";
|
||||||
import Icon from "$lib/components/ui/Icon.svelte";
|
import Icon from "$lib/components/ui/Icon.svelte";
|
||||||
|
import { isFav, toggleFav } from "$lib/favorites.svelte";
|
||||||
|
import { t } from "$lib/i18n/t";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -20,6 +22,20 @@
|
|||||||
<strong>{title}</strong>
|
<strong>{title}</strong>
|
||||||
{#if description}<span>{description}</span>{/if}
|
{#if description}<span>{description}</span>{/if}
|
||||||
</span>
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tool-fav"
|
||||||
|
class:active={isFav(slug)}
|
||||||
|
aria-label={isFav(slug) ? t("ui.removeFav") : t("ui.addFav")}
|
||||||
|
aria-pressed={isFav(slug)}
|
||||||
|
onclick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleFav(slug);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Star size={15} fill={isFav(slug) ? "currentColor" : "none"} />
|
||||||
|
</button>
|
||||||
<span class="tool-index">
|
<span class="tool-index">
|
||||||
{index !== undefined ? index.toString().padStart(2, "0") : ""}
|
{index !== undefined ? index.toString().padStart(2, "0") : ""}
|
||||||
</span>
|
</span>
|
||||||
@@ -31,7 +47,7 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns:
|
grid-template-columns:
|
||||||
var(--size-tool-icon) minmax(0, 1fr) var(--space-xxl)
|
var(--size-tool-icon) minmax(0, 1fr) var(--space-xxl)
|
||||||
var(--size-tool-arrow);
|
var(--space-xxl) var(--size-tool-arrow);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-l);
|
gap: var(--space-l);
|
||||||
min-height: var(--size-tool-min-height);
|
min-height: var(--size-tool-min-height);
|
||||||
@@ -54,6 +70,19 @@
|
|||||||
color: var(--color-main);
|
color: var(--color-main);
|
||||||
border-radius: var(--radius-m);
|
border-radius: var(--radius-m);
|
||||||
}
|
}
|
||||||
|
.tool-fav {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: var(--space-s);
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.tool-fav:hover,
|
||||||
|
.tool-fav.active {
|
||||||
|
color: var(--color-warning);
|
||||||
|
}
|
||||||
.tool-copy {
|
.tool-copy {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import StepCard from "$lib/components/display/StepCard.svelte";
|
import StepCard from "$lib/components/display/StepCard.svelte";
|
||||||
import Toggle from "$lib/components/ui/Toggle.svelte";
|
import Toggle from "$lib/components/ui/Toggle.svelte";
|
||||||
|
import { isFav, toggleFav } from "$lib/favorites.svelte";
|
||||||
|
import { Star } from "@lucide/svelte";
|
||||||
import { hasTransparency } from "$lib/core/analyze";
|
import { hasTransparency } from "$lib/core/analyze";
|
||||||
import { debounce } from "$lib/core/debounce";
|
import { debounce } from "$lib/core/debounce";
|
||||||
import { ToolError, type ErrorVars } from "$lib/core/errors";
|
import { ToolError, type ErrorVars } from "$lib/core/errors";
|
||||||
@@ -480,7 +482,19 @@
|
|||||||
<div class="schema-tool">
|
<div class="schema-tool">
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<div class="title-block">
|
<div class="title-block">
|
||||||
<h1>{pageTitle(page)}</h1>
|
<h1>
|
||||||
|
{pageTitle(page)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tool-fav"
|
||||||
|
class:active={isFav(page.slug)}
|
||||||
|
aria-label={isFav(page.slug) ? t("ui.removeFav") : t("ui.addFav")}
|
||||||
|
aria-pressed={isFav(page.slug)}
|
||||||
|
onclick={() => toggleFav(page.slug)}
|
||||||
|
>
|
||||||
|
<Star size={18} fill={isFav(page.slug) ? "currentColor" : "none"} />
|
||||||
|
</button>
|
||||||
|
</h1>
|
||||||
<p class="lede">{pageDescription(page)}</p>
|
<p class="lede">{pageDescription(page)}</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -701,11 +715,27 @@
|
|||||||
margin-bottom: var(--space-xxxl);
|
margin-bottom: var(--space-xxxl);
|
||||||
}
|
}
|
||||||
.title-block h1 {
|
.title-block h1 {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-m);
|
||||||
margin: var(--space-s) 0 0;
|
margin: var(--space-s) 0 0;
|
||||||
font-size: clamp(var(--font-size-xl), 4vw, var(--font-size-2xl));
|
font-size: clamp(var(--font-size-xl), 4vw, var(--font-size-2xl));
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
.tool-fav {
|
||||||
|
display: inline-grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: var(--space-s);
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.tool-fav:hover,
|
||||||
|
.tool-fav.active {
|
||||||
|
color: var(--color-warning);
|
||||||
|
}
|
||||||
.lede {
|
.lede {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
margin: var(--space-m) 0 0;
|
margin: var(--space-m) 0 0;
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { SvelteSet } from "svelte/reactivity";
|
||||||
|
|
||||||
|
const KEY = "fav-tools";
|
||||||
|
|
||||||
|
const slugs = new SvelteSet<string>();
|
||||||
|
|
||||||
|
function storage(): Storage | null {
|
||||||
|
return typeof localStorage === "undefined" ? null : localStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initFavorites(): void {
|
||||||
|
const raw = storage()?.getItem(KEY);
|
||||||
|
if (!raw) return;
|
||||||
|
try {
|
||||||
|
const list: unknown = JSON.parse(raw);
|
||||||
|
if (Array.isArray(list)) {
|
||||||
|
for (const s of list) if (typeof s === "string") slugs.add(s);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// мусор в localStorage — работаем с пустым набором
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFav(slug: string): boolean {
|
||||||
|
return slugs.has(slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleFav(slug: string): void {
|
||||||
|
if (slugs.has(slug)) {
|
||||||
|
slugs.delete(slug);
|
||||||
|
} else {
|
||||||
|
slugs.add(slug);
|
||||||
|
}
|
||||||
|
storage()?.setItem(KEY, JSON.stringify([...slugs]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Избранные страницы из списка (порядок сохраняется). */
|
||||||
|
export function favPages<T extends { slug: string }>(pages: readonly T[]): T[] {
|
||||||
|
return pages.filter((p) => slugs.has(p.slug));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Те же страницы, но избранные первыми (бонус в выдаче поиска). */
|
||||||
|
export function favFirst<T extends { slug: string }>(pages: readonly T[]): T[] {
|
||||||
|
return [...pages].sort(
|
||||||
|
(a, b) => Number(slugs.has(b.slug)) - Number(slugs.has(a.slug)),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
favFirst,
|
||||||
|
favPages,
|
||||||
|
initFavorites,
|
||||||
|
isFav,
|
||||||
|
toggleFav,
|
||||||
|
} from "./favorites.svelte";
|
||||||
|
|
||||||
|
describe("favorites", () => {
|
||||||
|
it("toggle добавляет и убирает, isFav отражает состояние", () => {
|
||||||
|
expect(isFav("flip-png")).toBe(false);
|
||||||
|
toggleFav("flip-png");
|
||||||
|
expect(isFav("flip-png")).toBe(true);
|
||||||
|
toggleFav("flip-png");
|
||||||
|
expect(isFav("flip-png")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("favPages фильтрует, favFirst поднимает избранное", () => {
|
||||||
|
toggleFav("resize-png");
|
||||||
|
const pages = [{ slug: "crop-png" }, { slug: "resize-png" }];
|
||||||
|
expect(favPages(pages)).toEqual([{ slug: "resize-png" }]);
|
||||||
|
expect(favFirst(pages).map((p) => p.slug)).toEqual([
|
||||||
|
"resize-png",
|
||||||
|
"crop-png",
|
||||||
|
]);
|
||||||
|
toggleFav("resize-png");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("initFavorites без localStorage и с мусором не падает", () => {
|
||||||
|
expect(() => initFavorites()).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -36,6 +36,7 @@ export const en: Dict = {
|
|||||||
lead: "Focused utilities for working with PNG. Inspect, transform, and export — locally in your browser.",
|
lead: "Focused utilities for working with PNG. Inspect, transform, and export — locally in your browser.",
|
||||||
toolsAvailable: "Tools available",
|
toolsAvailable: "Tools available",
|
||||||
toolsCount: "Tools",
|
toolsCount: "Tools",
|
||||||
|
favorites: "Favorites",
|
||||||
},
|
},
|
||||||
toolPage: {
|
toolPage: {
|
||||||
fallbackTitle: "Tool",
|
fallbackTitle: "Tool",
|
||||||
@@ -167,6 +168,8 @@ export const en: Dict = {
|
|||||||
removeColor: "Remove color",
|
removeColor: "Remove color",
|
||||||
themeToggle: "Toggle theme",
|
themeToggle: "Toggle theme",
|
||||||
language: "Language",
|
language: "Language",
|
||||||
|
addFav: "Add to favorites",
|
||||||
|
removeFav: "Remove from favorites",
|
||||||
},
|
},
|
||||||
errors: {
|
errors: {
|
||||||
noImageRun: "This tool does not process images",
|
noImageRun: "This tool does not process images",
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export const ru: Dict = {
|
|||||||
lead: "Утилиты для работы с PNG. Анализируйте, преобразуйте и экспортируйте — локально в браузере.",
|
lead: "Утилиты для работы с PNG. Анализируйте, преобразуйте и экспортируйте — локально в браузере.",
|
||||||
toolsAvailable: "Инструментов доступно",
|
toolsAvailable: "Инструментов доступно",
|
||||||
toolsCount: "Инструментов",
|
toolsCount: "Инструментов",
|
||||||
|
favorites: "Избранное",
|
||||||
},
|
},
|
||||||
toolPage: {
|
toolPage: {
|
||||||
fallbackTitle: "Инструмент",
|
fallbackTitle: "Инструмент",
|
||||||
@@ -168,6 +169,8 @@ export const ru: Dict = {
|
|||||||
removeColor: "Удалить цвет",
|
removeColor: "Удалить цвет",
|
||||||
themeToggle: "Переключить тему",
|
themeToggle: "Переключить тему",
|
||||||
language: "Язык",
|
language: "Язык",
|
||||||
|
addFav: "В избранное",
|
||||||
|
removeFav: "Убрать из избранного",
|
||||||
},
|
},
|
||||||
errors: {
|
errors: {
|
||||||
noImageRun: "Этот инструмент не обрабатывает изображения",
|
noImageRun: "Этот инструмент не обрабатывает изображения",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { page } from "$app/state";
|
import { page } from "$app/state";
|
||||||
import Footer from "$lib/components/layout/Footer.svelte";
|
import Footer from "$lib/components/layout/Footer.svelte";
|
||||||
import TopBar from "$lib/components/layout/TopBar.svelte";
|
import TopBar from "$lib/components/layout/TopBar.svelte";
|
||||||
|
import { initFavorites } from "$lib/favorites.svelte";
|
||||||
import { initLocale } from "$lib/i18n/locale.svelte";
|
import { initLocale } from "$lib/i18n/locale.svelte";
|
||||||
import { pageTitle } from "$lib/i18n/schema-tool-strings";
|
import { pageTitle } from "$lib/i18n/schema-tool-strings";
|
||||||
import { t } from "$lib/i18n/t";
|
import { t } from "$lib/i18n/t";
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
onMount(() => {
|
onMount(() => {
|
||||||
initLocale();
|
initLocale();
|
||||||
initTheme();
|
initTheme();
|
||||||
|
initFavorites();
|
||||||
});
|
});
|
||||||
|
|
||||||
let crumb = $derived(toCrumb(page.url.pathname));
|
let crumb = $derived(toCrumb(page.url.pathname));
|
||||||
|
|||||||
@@ -4,23 +4,34 @@
|
|||||||
import CatalogHeader from "$lib/components/catalog/CatalogHeader.svelte";
|
import CatalogHeader from "$lib/components/catalog/CatalogHeader.svelte";
|
||||||
import CatalogToolbar from "$lib/components/catalog/CatalogToolbar.svelte";
|
import CatalogToolbar from "$lib/components/catalog/CatalogToolbar.svelte";
|
||||||
import ToolCard from "$lib/components/catalog/ToolCard.svelte";
|
import ToolCard from "$lib/components/catalog/ToolCard.svelte";
|
||||||
|
import { favFirst, isFav } from "$lib/favorites.svelte";
|
||||||
import {
|
import {
|
||||||
pageDescription,
|
pageDescription,
|
||||||
pageTitle,
|
pageTitle,
|
||||||
searchPages,
|
searchPages,
|
||||||
} from "$lib/i18n/schema-tool-strings";
|
} from "$lib/i18n/schema-tool-strings";
|
||||||
import { t } from "$lib/i18n/t";
|
import { t } from "$lib/i18n/t";
|
||||||
|
import { PAGES } from "$lib/registry";
|
||||||
import { TOOL_ICONS } from "$lib/tool-icons";
|
import { TOOL_ICONS } from "$lib/tool-icons";
|
||||||
|
|
||||||
let query = $state("");
|
let query = $state("");
|
||||||
let category = $state<string>("all");
|
let category = $state<string>("all");
|
||||||
|
|
||||||
|
const favorites = $derived(
|
||||||
|
searchPages(
|
||||||
|
PAGES.filter((p) => isFav(p.slug)),
|
||||||
|
query,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const groups = $derived(
|
const groups = $derived(
|
||||||
PREVIEW_GROUPS.map((g) => ({
|
PREVIEW_GROUPS.map((g) => ({
|
||||||
id: g.id,
|
id: g.id,
|
||||||
label: t(`categories.${g.id}`),
|
label: t(`categories.${g.id}`),
|
||||||
pages: searchPages(g.pages, query).filter(
|
pages: favFirst(
|
||||||
() => category === "all" || category === g.id,
|
searchPages(g.pages, query).filter(
|
||||||
|
() => category === "all" || category === g.id,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
})).filter((g) => g.pages.length > 0),
|
})).filter((g) => g.pages.length > 0),
|
||||||
);
|
);
|
||||||
@@ -34,6 +45,19 @@
|
|||||||
<CatalogHeader total={PREVIEW_TOTAL} />
|
<CatalogHeader total={PREVIEW_TOTAL} />
|
||||||
<CatalogToolbar bind:query bind:category />
|
<CatalogToolbar bind:query bind:category />
|
||||||
<div class="catalog-groups">
|
<div class="catalog-groups">
|
||||||
|
{#if favorites.length > 0}
|
||||||
|
<CatalogGroup label={t("catalog.favorites")} count={favorites.length}>
|
||||||
|
{#each favorites as page, i (page.slug)}
|
||||||
|
<ToolCard
|
||||||
|
title={pageTitle(page)}
|
||||||
|
slug={page.slug}
|
||||||
|
description={pageDescription(page)}
|
||||||
|
index={i + 1}
|
||||||
|
icon={TOOL_ICONS[page.slug]}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</CatalogGroup>
|
||||||
|
{/if}
|
||||||
{#each groups as group (group.id)}
|
{#each groups as group (group.id)}
|
||||||
<CatalogGroup label={group.label} count={group.pages.length}>
|
<CatalogGroup label={group.label} count={group.pages.length}>
|
||||||
{#each group.pages as page, i (page.slug)}
|
{#each group.pages as page, i (page.slug)}
|
||||||
|
|||||||
@@ -4,23 +4,34 @@
|
|||||||
import CatalogHeader from "$lib/components/catalog/CatalogHeader.svelte";
|
import CatalogHeader from "$lib/components/catalog/CatalogHeader.svelte";
|
||||||
import CatalogToolbar from "$lib/components/catalog/CatalogToolbar.svelte";
|
import CatalogToolbar from "$lib/components/catalog/CatalogToolbar.svelte";
|
||||||
import ToolCard from "$lib/components/catalog/ToolCard.svelte";
|
import ToolCard from "$lib/components/catalog/ToolCard.svelte";
|
||||||
|
import { favFirst, isFav } from "$lib/favorites.svelte";
|
||||||
import {
|
import {
|
||||||
pageDescription,
|
pageDescription,
|
||||||
pageTitle,
|
pageTitle,
|
||||||
searchPages,
|
searchPages,
|
||||||
} from "$lib/i18n/schema-tool-strings";
|
} from "$lib/i18n/schema-tool-strings";
|
||||||
import { t } from "$lib/i18n/t";
|
import { t } from "$lib/i18n/t";
|
||||||
|
import { PAGES } from "$lib/registry";
|
||||||
import { TOOL_ICONS } from "$lib/tool-icons";
|
import { TOOL_ICONS } from "$lib/tool-icons";
|
||||||
|
|
||||||
let query = $state("");
|
let query = $state("");
|
||||||
let category = $state<string>("all");
|
let category = $state<string>("all");
|
||||||
|
|
||||||
|
const favorites = $derived(
|
||||||
|
searchPages(
|
||||||
|
PAGES.filter((p) => isFav(p.slug)),
|
||||||
|
query,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const groups = $derived(
|
const groups = $derived(
|
||||||
PREVIEW_GROUPS.map((g) => ({
|
PREVIEW_GROUPS.map((g) => ({
|
||||||
id: g.id,
|
id: g.id,
|
||||||
label: t(`categories.${g.id}`),
|
label: t(`categories.${g.id}`),
|
||||||
pages: searchPages(g.pages, query).filter(
|
pages: favFirst(
|
||||||
() => category === "all" || category === g.id,
|
searchPages(g.pages, query).filter(
|
||||||
|
() => category === "all" || category === g.id,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
})).filter((g) => g.pages.length > 0),
|
})).filter((g) => g.pages.length > 0),
|
||||||
);
|
);
|
||||||
@@ -35,6 +46,19 @@
|
|||||||
<CatalogHeader total={PREVIEW_TOTAL} />
|
<CatalogHeader total={PREVIEW_TOTAL} />
|
||||||
<CatalogToolbar bind:query bind:category />
|
<CatalogToolbar bind:query bind:category />
|
||||||
<div class="catalog-groups">
|
<div class="catalog-groups">
|
||||||
|
{#if favorites.length > 0}
|
||||||
|
<CatalogGroup label={t("catalog.favorites")} count={favorites.length}>
|
||||||
|
{#each favorites as page, i (page.slug)}
|
||||||
|
<ToolCard
|
||||||
|
title={pageTitle(page)}
|
||||||
|
slug={page.slug}
|
||||||
|
description={pageDescription(page)}
|
||||||
|
index={i + 1}
|
||||||
|
icon={TOOL_ICONS[page.slug]}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</CatalogGroup>
|
||||||
|
{/if}
|
||||||
{#each groups as group (group.id)}
|
{#each groups as group (group.id)}
|
||||||
<CatalogGroup label={group.label} count={group.pages.length}>
|
<CatalogGroup label={group.label} count={group.pages.length}>
|
||||||
{#each group.pages as page, i (page.slug)}
|
{#each group.pages as page, i (page.slug)}
|
||||||
|
|||||||
Reference in New Issue
Block a user