mirror of
https://github.com/Snigdha-OS/package-browser.git
synced 2025-09-08 05:35:02 +02:00
🚀 feat: add #1 repo filter option. credit: @XlebyllleK
This commit is contained in:
58
src/App.tsx
58
src/App.tsx
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Header } from './components/Header';
|
import { Header } from './components/Header';
|
||||||
import { SearchBar } from './components/SearchBar';
|
import { SearchBar } from './components/SearchBar';
|
||||||
import { PackageList } from './components/PackageList';
|
import { PackageList } from './components/PackageList';
|
||||||
@@ -7,43 +7,57 @@ import { usePackages } from './hooks/usePackages';
|
|||||||
export default function App() {
|
export default function App() {
|
||||||
const { packages, loading, error } = usePackages();
|
const { packages, loading, error } = usePackages();
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
const [selectedRepository, setSelectedRepository] = useState<'core' | 'extra' | 'all'>('all');
|
||||||
|
const [debouncedSearch, setDebouncedSearch] = useState(search);
|
||||||
|
|
||||||
// Filter packages based on search query
|
// Debounce search to optimize performance
|
||||||
const filteredPackages = packages.filter((pkg) =>
|
useEffect(() => {
|
||||||
pkg.name.toLowerCase().includes(search.toLowerCase()) ||
|
const timeoutId = setTimeout(() => {
|
||||||
pkg.description.toLowerCase().includes(search.toLowerCase())
|
setDebouncedSearch(search);
|
||||||
);
|
}, 300); // Wait for 300ms after the last keystroke
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}, [search]);
|
||||||
|
|
||||||
|
// Filter packages based on search query and selected repository
|
||||||
|
const filteredPackages = packages
|
||||||
|
.filter((pkg) => {
|
||||||
|
// Filter by repository
|
||||||
|
if (selectedRepository !== 'all' && pkg.repository !== selectedRepository) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Filter by search query
|
||||||
|
return pkg.name.toLowerCase().includes(debouncedSearch.toLowerCase()) ||
|
||||||
|
pkg.description.toLowerCase().includes(debouncedSearch.toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSearchChange = (value: string) => {
|
||||||
|
setSearch(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRepositoryFilterChange = (repo: 'core' | 'extra' | 'all') => {
|
||||||
|
setSelectedRepository(repo);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="min-h-screen bg-nord-6 dark:bg-nord-0 transition-colors" role="main">
|
||||||
className="min-h-screen bg-nord-6 dark:bg-nord-0 transition-colors"
|
<Header onRepositoryChange={handleRepositoryFilterChange} />
|
||||||
role="main"
|
|
||||||
>
|
|
||||||
<Header />
|
|
||||||
|
|
||||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
{/* Search Bar */}
|
{/* Search Bar */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<SearchBar value={search} onChange={setSearch} />
|
<SearchBar value={search} onChange={handleSearchChange} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Package Counter */}
|
{/* Package Counter */}
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<p
|
<p className="text-sm text-nord-3 dark:text-nord-4" aria-live="polite">
|
||||||
className="text-sm text-nord-3 dark:text-nord-4"
|
Showing {filteredPackages.length} package{filteredPackages.length !== 1 ? 's' : ''}
|
||||||
aria-live="polite"
|
|
||||||
>
|
|
||||||
Showing {filteredPackages.length} package
|
|
||||||
{filteredPackages.length !== 1 ? 's' : ''}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Error State */}
|
{/* Error State */}
|
||||||
{error ? (
|
{error ? (
|
||||||
<div
|
<div className="rounded-lg bg-nord-11/10 dark:bg-nord-11/20 p-4 text-nord-11" role="alert">
|
||||||
className="rounded-lg bg-nord-11/10 dark:bg-nord-11/20 p-4 text-nord-11"
|
|
||||||
role="alert"
|
|
||||||
>
|
|
||||||
<p>An error occurred while fetching packages: {error}</p>
|
<p>An error occurred while fetching packages: {error}</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => window.location.reload()}
|
onClick={() => window.location.reload()}
|
||||||
|
@@ -1,7 +1,11 @@
|
|||||||
import { ThemeToggle } from './ThemeToggle';
|
import { ThemeToggle } from './ThemeToggle';
|
||||||
import { Logo } from './Logo';
|
import { Logo } from './Logo';
|
||||||
|
|
||||||
export function Header() {
|
interface HeaderProps {
|
||||||
|
onRepositoryChange: (repo: 'core' | 'extra' | 'all') => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Header({ onRepositoryChange }: HeaderProps) {
|
||||||
return (
|
return (
|
||||||
<header className="bg-gradient-to-r from-nord-9 to-nord-8 via-nord-10 text-nord-6 shadow-lg">
|
<header className="bg-gradient-to-r from-nord-9 to-nord-8 via-nord-10 text-nord-6 shadow-lg">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||||
@@ -11,7 +15,20 @@ export function Header() {
|
|||||||
<Logo />
|
<Logo />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Theme Toggle with Button Styling */}
|
{/* Repository Filter Dropdown */}
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
onChange={(e) => onRepositoryChange(e.target.value as 'core' | 'extra' | 'all')}
|
||||||
|
defaultValue="all"
|
||||||
|
className="bg-nord-5 dark:bg-nord-1 text-black dark:text-white border-2 border-nord-4 dark:border-nord-2 rounded-lg py-2 px-4 focus:ring-2 focus:ring-nord-8"
|
||||||
|
>
|
||||||
|
<option value="all">All Repositories</option>
|
||||||
|
<option value="core">Core</option>
|
||||||
|
<option value="extra">Extra</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Theme Toggle */}
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
31
src/components/RepositorySelector.tsx
Normal file
31
src/components/RepositorySelector.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
type RepositorySelectorProps = {
|
||||||
|
onSelectRepository: (repo: 'core' | 'extra' | 'all') => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function RepositorySelector({ onSelectRepository }: RepositorySelectorProps) {
|
||||||
|
const [selectedRepository, setSelectedRepository] = useState<'core' | 'extra' | 'all'>('all');
|
||||||
|
|
||||||
|
const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const selected = event.target.value as 'core' | 'extra' | 'all';
|
||||||
|
setSelectedRepository(selected);
|
||||||
|
onSelectRepository(selected); // Pass the selection back to parent
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label htmlFor="repository-selector" className="mr-2 text-nord-6">Select Repository:</label>
|
||||||
|
<select
|
||||||
|
id="repository-selector"
|
||||||
|
value={selectedRepository}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="bg-nord-1 text-nord-6 border border-nord-4 rounded-md p-2"
|
||||||
|
>
|
||||||
|
<option value="all">All</option>
|
||||||
|
<option value="core">Core</option>
|
||||||
|
<option value="extra">Extra</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@@ -3,9 +3,16 @@ import { Search } from 'lucide-react';
|
|||||||
interface SearchBarProps {
|
interface SearchBarProps {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
|
selectedRepository: 'core' | 'extra' | 'all';
|
||||||
|
onFilterChange: (repo: 'core' | 'extra' | 'all') => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SearchBar({ value, onChange }: SearchBarProps) {
|
export function SearchBar({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
// selectedRepository,
|
||||||
|
// onFilterChange,
|
||||||
|
}: SearchBarProps) {
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full max-w-md mx-auto">
|
<div className="relative w-full max-w-md mx-auto">
|
||||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
@@ -18,6 +25,17 @@ export function SearchBar({ value, onChange }: SearchBarProps) {
|
|||||||
placeholder="Search packages..."
|
placeholder="Search packages..."
|
||||||
className="block w-full pl-10 pr-4 py-3 border-2 border-nord-4 dark:border-nord-2 rounded-xl bg-nord-5 dark:bg-nord-1 focus:ring-2 focus:ring-nord-8 focus:border-transparent text-nord-0 dark:text-nord-6 placeholder-nord-3 dark:placeholder-nord-4 transition-all ease-in-out duration-200 shadow-md hover:shadow-lg"
|
className="block w-full pl-10 pr-4 py-3 border-2 border-nord-4 dark:border-nord-2 rounded-xl bg-nord-5 dark:bg-nord-1 focus:ring-2 focus:ring-nord-8 focus:border-transparent text-nord-0 dark:text-nord-6 placeholder-nord-3 dark:placeholder-nord-4 transition-all ease-in-out duration-200 shadow-md hover:shadow-lg"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Dropdown for repository filter */}
|
||||||
|
{/* <select
|
||||||
|
value={selectedRepository}
|
||||||
|
onChange={(e) => onFilterChange(e.target.value as 'core' | 'extra' | 'all')}
|
||||||
|
className="absolute right-3 top-3 bg-transparent border-none text-nord-6 dark:text-nord-5 cursor-pointer focus:outline-none"
|
||||||
|
>
|
||||||
|
<option value="all">All</option>
|
||||||
|
<option value="core">Core</option>
|
||||||
|
<option value="extra">Extra</option>
|
||||||
|
</select> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
55
src/components/fetchPackages.tsx
Normal file
55
src/components/fetchPackages.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
export interface Package {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
description: string;
|
||||||
|
repository: 'core' | 'extra'; // This can be 'core', 'extra', or 'all'
|
||||||
|
}
|
||||||
|
|
||||||
|
const MIRRORS = [
|
||||||
|
'https://raw.githubusercontent.com/Snigdha-OS/snigdhaos-core/refs/heads/master/packages.txt',
|
||||||
|
'https://raw.githubusercontent.com/Snigdha-OS/snigdhaos-extra/refs/heads/master/packages.txt'
|
||||||
|
];
|
||||||
|
|
||||||
|
async function fetchFromMirror(url: string): Promise<Package[]> {
|
||||||
|
const response = await fetch(url);
|
||||||
|
const text = await response.text();
|
||||||
|
|
||||||
|
// Determine the repository based on the URL
|
||||||
|
const repository = url.includes('snigdhaos-core') ? 'core' : 'extra';
|
||||||
|
|
||||||
|
return text
|
||||||
|
.split('\n')
|
||||||
|
.filter(Boolean) // Remove empty lines
|
||||||
|
.map((line) => {
|
||||||
|
const [name, version, ...descParts] = line.split(' ');
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
description: descParts.join(' '),
|
||||||
|
repository, // Set the repository name dynamically
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPackages(): Promise<Package[]> {
|
||||||
|
let packages: Package[] = [];
|
||||||
|
|
||||||
|
// Try fetching from each mirror
|
||||||
|
for (const mirror of MIRRORS) {
|
||||||
|
try {
|
||||||
|
const result = await fetchFromMirror(mirror);
|
||||||
|
packages = packages.concat(result); // Append fetched packages
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to fetch from mirror ${mirror}:`, error);
|
||||||
|
continue; // Continue to next mirror if one fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no packages were fetched, throw an error
|
||||||
|
if (packages.length === 0) {
|
||||||
|
throw new Error('All mirrors failed to respond');
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages;
|
||||||
|
}
|
||||||
|
|
@@ -1,39 +1,43 @@
|
|||||||
import { Package } from '../types';
|
import { Package } from '../types';
|
||||||
|
|
||||||
|
// Define the mirrors from which packages will be fetched
|
||||||
const MIRRORS = [
|
const MIRRORS = [
|
||||||
'https://raw.githubusercontent.com/Snigdha-OS/snigdhaos-core/refs/heads/master/packages.txt',
|
'https://raw.githubusercontent.com/Snigdha-OS/snigdhaos-core/refs/heads/master/packages.txt',
|
||||||
'https://raw.githubusercontent.com/Snigdha-OS/snigdhaos-extra/refs/heads/master/packages.txt'
|
'https://raw.githubusercontent.com/Snigdha-OS/snigdhaos-extra/refs/heads/master/packages.txt'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Fetch data from a single mirror (Core or Extra repository)
|
||||||
async function fetchFromMirror(url: string): Promise<Package[]> {
|
async function fetchFromMirror(url: string): Promise<Package[]> {
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
|
|
||||||
// determine the repository from the mirror URL (core or extra)
|
// Determine the repository name (core or extra) from the URL
|
||||||
const repository = url.includes('snigdhaos-core') ? 'core' : 'extra';
|
const repository = url.includes('snigdhaos-core') ? 'core' : 'extra';
|
||||||
|
|
||||||
|
// Parse the text file content and return a list of packages
|
||||||
return text
|
return text
|
||||||
.split('\n')
|
.split('\n') // Split by line
|
||||||
.filter(Boolean)
|
.filter(Boolean) // Remove empty lines
|
||||||
.map((line) => {
|
.map((line) => {
|
||||||
const [name, version, ...descParts] = line.split(' ');
|
const [name, version, ...descParts] = line.split(' ');
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
version,
|
version,
|
||||||
description: descParts.join(' '),
|
description: descParts.join(' '),
|
||||||
repository, // use the determined repository name
|
repository, // Attach the repository name to each package
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch packages from all mirrors and combine them
|
||||||
export async function fetchPackages(): Promise<Package[]> {
|
export async function fetchPackages(): Promise<Package[]> {
|
||||||
let packages: Package[] = [];
|
let packages: Package[] = [];
|
||||||
|
|
||||||
// Try each mirror and accumulate results
|
// Try fetching from each mirror and accumulate the results
|
||||||
for (const mirror of MIRRORS) {
|
for (const mirror of MIRRORS) {
|
||||||
try {
|
try {
|
||||||
const result = await fetchFromMirror(mirror);
|
const result = await fetchFromMirror(mirror);
|
||||||
packages = packages.concat(result); // Append to packages array
|
packages = packages.concat(result); // Append the packages from this mirror
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Failed to fetch from mirror ${mirror}:`, error);
|
console.warn(`Failed to fetch from mirror ${mirror}:`, error);
|
||||||
continue; // Continue to the next mirror if this one fails
|
continue; // Continue to the next mirror if this one fails
|
||||||
@@ -44,6 +48,6 @@ export async function fetchPackages(): Promise<Package[]> {
|
|||||||
if (packages.length === 0) {
|
if (packages.length === 0) {
|
||||||
throw new Error('All mirrors failed to respond');
|
throw new Error('All mirrors failed to respond');
|
||||||
}
|
}
|
||||||
|
|
||||||
return packages;
|
return packages;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user