mirror of
https://github.com/zen-browser/www.git
synced 2025-07-08 01:10:02 +02:00
feat(modlist): added filtering and search bar for modlist
This commit is contained in:
parent
ed060c8e03
commit
75d75a9255
3 changed files with 227 additions and 54 deletions
161
src/components/ModsList.tsx
Normal file
161
src/components/ModsList.tsx
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
import React, { useState, useMemo } from 'react'
|
||||||
|
import type { ZenTheme } from '../mods'
|
||||||
|
import { library, icon } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import { faSort, faSortUp, faSortDown } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
// Add icons to the library
|
||||||
|
library.add(faSort, faSortUp, faSortDown)
|
||||||
|
|
||||||
|
// Create icon objects
|
||||||
|
const defaultSortIcon = icon({ prefix: 'fas', iconName: 'sort' })
|
||||||
|
const ascSortIcon = icon({ prefix: 'fas', iconName: 'sort-up' })
|
||||||
|
const descSortIcon = icon({ prefix: 'fas', iconName: 'sort-down' })
|
||||||
|
|
||||||
|
interface ModsListProps {
|
||||||
|
mods: ZenTheme[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ModsList({ mods }: ModsListProps) {
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
const [createdSort, setCreatedSort] = useState<'default' | 'asc' | 'desc'>(
|
||||||
|
'default',
|
||||||
|
)
|
||||||
|
const [updatedSort, setUpdatedSort] = useState<'default' | 'asc' | 'desc'>(
|
||||||
|
'default',
|
||||||
|
)
|
||||||
|
|
||||||
|
const toggleCreatedSort = () => {
|
||||||
|
setCreatedSort((prev) => {
|
||||||
|
if (prev === 'default') return 'asc'
|
||||||
|
if (prev === 'asc') return 'desc'
|
||||||
|
return 'default'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleUpdatedSort = () => {
|
||||||
|
setUpdatedSort((prev) => {
|
||||||
|
if (prev === 'default') return 'asc'
|
||||||
|
if (prev === 'asc') return 'desc'
|
||||||
|
return 'default'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSortIcon(state: 'default' | 'asc' | 'desc') {
|
||||||
|
if (state === 'asc') return ascSortIcon
|
||||||
|
if (state === 'desc') return descSortIcon
|
||||||
|
return defaultSortIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredAndSortedMods = useMemo(() => {
|
||||||
|
let filtered = [...mods]
|
||||||
|
|
||||||
|
// Filter by search
|
||||||
|
const searchTerm = search.toLowerCase()
|
||||||
|
if (searchTerm) {
|
||||||
|
filtered = filtered.filter(
|
||||||
|
(mod) =>
|
||||||
|
mod.name.toLowerCase().includes(searchTerm) ||
|
||||||
|
mod.description.toLowerCase().includes(searchTerm) ||
|
||||||
|
mod.author.toLowerCase().includes(searchTerm) ||
|
||||||
|
(mod.tags?.some((tag) => tag.toLowerCase().includes(searchTerm)) ??
|
||||||
|
false),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by createdAt if chosen
|
||||||
|
if (createdSort !== 'default') {
|
||||||
|
filtered.sort((a, b) => {
|
||||||
|
const diff =
|
||||||
|
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||||
|
return createdSort === 'asc' ? diff : -diff
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by updatedAt if chosen
|
||||||
|
if (updatedSort !== 'default') {
|
||||||
|
filtered.sort((a, b) => {
|
||||||
|
const diff =
|
||||||
|
new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime()
|
||||||
|
return updatedSort === 'asc' ? diff : -diff
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
}, [mods, search, createdSort, updatedSort])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mx-auto mb-8 flex flex-col items-start gap-4 px-8 lg:w-1/2">
|
||||||
|
<div className="flex w-full flex-col items-center gap-6">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="search"
|
||||||
|
className="w-full rounded-full border-2 border-dark bg-transparent px-6 py-2 text-lg"
|
||||||
|
placeholder="Type to search..."
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-4">
|
||||||
|
<div className="flex flex-col items-start gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={toggleCreatedSort}
|
||||||
|
className="text-md flex items-center gap-2 px-4 py-2 font-semibold"
|
||||||
|
>
|
||||||
|
Last created
|
||||||
|
<span
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: getSortIcon(createdSort).html[0],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={toggleUpdatedSort}
|
||||||
|
className="text-md flex items-center gap-2 px-4 py-2 font-semibold"
|
||||||
|
>
|
||||||
|
Last updated
|
||||||
|
<span
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: getSortIcon(updatedSort).html[0],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx-auto grid grid-cols-1 gap-12 p-10 md:grid-cols-2 lg:grid-cols-3 lg:p-24 lg:px-24">
|
||||||
|
{filteredAndSortedMods.map((mod) => (
|
||||||
|
<a
|
||||||
|
key={mod.id}
|
||||||
|
href={`/mods/${mod.id}`}
|
||||||
|
className="mb-6 flex flex-col gap-4 border-transparent transition-colors duration-100 hover:opacity-90"
|
||||||
|
>
|
||||||
|
<div className="relative mb-0 block aspect-[1.85/1] h-48 overflow-hidden rounded-md border-2 border-dark object-cover shadow-md lg:h-auto">
|
||||||
|
<img
|
||||||
|
src={mod.image}
|
||||||
|
alt={mod.name}
|
||||||
|
className="h-full w-full object-cover transition-transform duration-100 hover:scale-105"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-bold">
|
||||||
|
{mod.name}{' '}
|
||||||
|
<span className="ml-1 text-sm font-normal">
|
||||||
|
by @{mod.author}
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm font-thin">{mod.description}</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
76
src/mods.ts
76
src/mods.ts
|
@ -1,35 +1,65 @@
|
||||||
export interface ZenTheme {
|
export interface ZenTheme {
|
||||||
name: string;
|
name: string
|
||||||
description: string;
|
description: string
|
||||||
image: string;
|
image: string
|
||||||
downloadUrl: string;
|
downloadUrl: string
|
||||||
id: string;
|
id: string
|
||||||
homepage?: string;
|
homepage?: string
|
||||||
readme: string;
|
readme: string
|
||||||
preferences?: string;
|
preferences?: string
|
||||||
isColorTheme: boolean;
|
isColorTheme: boolean
|
||||||
author: string;
|
author: string
|
||||||
version: string;
|
version: string
|
||||||
tags: string[];
|
tags: string[]
|
||||||
createdAt: Date;
|
createdAt: Date
|
||||||
updatedAt: Date;
|
updatedAt: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
const THEME_API = "https://zen-browser.github.io/theme-store/themes.json";
|
const THEME_API = 'https://zen-browser.github.io/theme-store/themes.json'
|
||||||
|
|
||||||
export async function getAllMods(): Promise<ZenTheme[]> {
|
interface FilterOptions {
|
||||||
|
createdAt?: Date
|
||||||
|
updatedAt?: Date
|
||||||
|
search?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllMods(filters?: FilterOptions): Promise<ZenTheme[]> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(THEME_API);
|
const res = await fetch(THEME_API)
|
||||||
const json = await res.json();
|
const json = await res.json()
|
||||||
// convert dict to array
|
// convert dict to array
|
||||||
const mods = Object.keys(json).map((key) => json[key]);
|
let mods: ZenTheme[] = Object.keys(json).map((key) => json[key])
|
||||||
return mods;
|
|
||||||
|
if (filters) {
|
||||||
|
if (filters.createdAt) {
|
||||||
|
mods = mods.filter(
|
||||||
|
(mod) => new Date(mod.createdAt) >= filters.createdAt!,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (filters.updatedAt) {
|
||||||
|
mods = mods.filter(
|
||||||
|
(mod) => new Date(mod.updatedAt) >= filters.updatedAt!,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (filters.search) {
|
||||||
|
const searchLower = filters.search.toLowerCase()
|
||||||
|
mods = mods.filter(
|
||||||
|
(mod) =>
|
||||||
|
mod.name.toLowerCase().includes(searchLower) ||
|
||||||
|
mod.description.toLowerCase().includes(searchLower) ||
|
||||||
|
mod.author.toLowerCase().includes(searchLower) ||
|
||||||
|
mod.tags.some((tag) => tag.toLowerCase().includes(searchLower)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mods
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error)
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAuthorLink(author: string): string {
|
export function getAuthorLink(author: string): string {
|
||||||
return `https://github.com/${author}`;
|
return `https://github.com/${author}`
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,46 +1,28 @@
|
||||||
---
|
---
|
||||||
import Description from '../../components/Description.astro';
|
import Description from '../../components/Description.astro'
|
||||||
import Title from '../../components/Title.astro';
|
import Title from '../../components/Title.astro'
|
||||||
import Layout from '../../layouts/Layout.astro';
|
import Layout from '../../layouts/Layout.astro'
|
||||||
import { getAllMods, type ZenTheme } from '../../mods';
|
import ModsList from '../../components/ModsList'
|
||||||
|
import { getAllMods } from '../../mods'
|
||||||
|
|
||||||
const mods = await getAllMods() || [];
|
const mods = (await getAllMods({})) || []
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Zen Mods">
|
<Layout title="Zen Mods">
|
||||||
<main class="mt-32 2xl:mt-0">
|
<main class="mt-32 2xl:mt-0">
|
||||||
<header class="mt-52 mb-32 flex flex-col justify-center w-full border-dark">
|
<header class="mb-10 mt-52 flex w-full flex-col justify-center border-dark">
|
||||||
<div class="px-8 mx-auto flex flex-col lg:w-1/2 gap-6">
|
<div class="mx-auto flex flex-col gap-6 px-8 lg:w-1/2">
|
||||||
<div>
|
<div>
|
||||||
<Title>Zen Mods</Title>
|
<Title>Zen Mods</Title>
|
||||||
<Description>
|
<Description>
|
||||||
Zen Mods is a collection of themes and plugins for Zen Browser. You can
|
Zen Mods is a collection of themes and plugins for Zen Browser. You
|
||||||
find a theme for every mood and a plugin for every need.
|
can find a theme for every mood and a plugin for every need.
|
||||||
</Description>
|
</Description>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden rounded-full border-2 border-dark p-3 flex gap-2">
|
|
||||||
<input type="text" class="border-none bg-transparent w-full !outline-none" placeholder="Search for mods" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-12 p-10 lg:p-24 lg:px-24 mx-auto">
|
|
||||||
{mods.map((mod: ZenTheme) => (
|
<!-- Importing ModList component -->
|
||||||
<a href={`/mods/${mod.id}`} class="block border-transparent transition-colors duration-100 flex flex-col gap-4 hover:opacity-90 mb-6">
|
<ModsList mods={mods} client:visible />
|
||||||
<div class="relative mb-0 hidden aspect-[1.85/1] h-48 overflow-hidden rounded-md border-2 object-cover shadow-md border-dark lg:block lg:h-auto">
|
|
||||||
<img
|
|
||||||
src={mod.image}
|
|
||||||
alt={mod.name}
|
|
||||||
class="h-full w-full object-cover hover:scale-105 transition-transform duration-100"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="">
|
|
||||||
<h2 class="font-bold text-lg">
|
|
||||||
{mod.name} <span class="font-normal text-sm ml-1">by @{mod.author}</span>
|
|
||||||
</h2>
|
|
||||||
<p class="text-sm font-thin">{mod.description}</p>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue