mirror of
https://github.com/Snigdha-OS/package-browser.git
synced 2025-09-05 12:16:42 +02:00
Compare commits
18 Commits
064d4ae613
...
057697b0cd
Author | SHA1 | Date | |
---|---|---|---|
![]() |
057697b0cd | ||
![]() |
b15c1e1883 | ||
![]() |
022ac3f982 | ||
![]() |
7ea3b6ad9b | ||
![]() |
06e8e2360b | ||
![]() |
2bf2039807 | ||
![]() |
a039720a71 | ||
![]() |
39cc3065dd | ||
b468fc541b | |||
b53bed9e91 | |||
f29cdf51e7 | |||
1a99fb8265 | |||
960ca1e00d | |||
![]() |
982f0b8b07 | ||
![]() |
e33caa9ad6 | ||
![]() |
4cca03867c | ||
![]() |
317d67f810 | ||
![]() |
a9463ad10b |
25
package.json
25
package.json
@@ -12,6 +12,17 @@
|
||||
"twitter": "@d3v1l0n",
|
||||
"bio": "A passionate developer focused on open-source contributions and building tools for the community."
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "eshanized",
|
||||
"email": "eshan@snigdhaos.org",
|
||||
"url": "https://github.com/eshanized"
|
||||
},
|
||||
{
|
||||
"name": "XlebyllleK",
|
||||
"url": "https://github.com/XlebyllleK"
|
||||
}
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
@@ -21,15 +32,15 @@
|
||||
"deploy": "gh-pages -d dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"gh-pages": "^6.2.0",
|
||||
"lucide-react": "^0.344.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
"gh-pages": "^6.3.0",
|
||||
"lucide-react": "^0.471.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@types/react": "^19.0.4",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.17.0",
|
||||
@@ -40,6 +51,6 @@
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.18.2",
|
||||
"vite": "^5.4.11"
|
||||
"vite": "^6.0.7"
|
||||
}
|
||||
}
|
||||
|
917
pnpm-lock.yaml
generated
917
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
17
src/App.tsx
17
src/App.tsx
@@ -1,13 +1,22 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
JSX
|
||||
} from 'react';
|
||||
|
||||
import { Header } from './components/Header';
|
||||
import { SearchBar } from './components/SearchBar';
|
||||
import { PackageList } from './components/PackageList';
|
||||
import { usePackages } from './hooks/usePackages';
|
||||
|
||||
export default function App() {
|
||||
import {
|
||||
Repository
|
||||
} from './types';
|
||||
|
||||
export default function App(): JSX.Element {
|
||||
const { packages, loading, error } = usePackages();
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedRepository, setSelectedRepository] = useState<'core' | 'extra' | 'all'>('all');
|
||||
const [selectedRepository, setSelectedRepository] = useState('all');
|
||||
const [debouncedSearch, setDebouncedSearch] = useState(search);
|
||||
|
||||
// Debounce search to optimize performance
|
||||
@@ -34,7 +43,7 @@ export default function App() {
|
||||
setSearch(value);
|
||||
};
|
||||
|
||||
const handleRepositoryFilterChange = (repo: 'core' | 'extra' | 'all') => {
|
||||
const handleRepositoryFilterChange = (repo: Repository) => {
|
||||
setSelectedRepository(repo);
|
||||
};
|
||||
|
||||
|
@@ -1,11 +1,19 @@
|
||||
import {
|
||||
JSX
|
||||
} from 'react';
|
||||
|
||||
import { ThemeToggle } from './ThemeToggle';
|
||||
import { Logo } from './Logo';
|
||||
|
||||
import {
|
||||
Repository
|
||||
} from '../types';
|
||||
|
||||
interface HeaderProps {
|
||||
onRepositoryChange: (repo: 'core' | 'extra' | 'all') => void;
|
||||
onRepositoryChange: (repo: Repository) => void;
|
||||
}
|
||||
|
||||
export function Header({ onRepositoryChange }: HeaderProps) {
|
||||
export function Header({ onRepositoryChange }: HeaderProps): JSX.Element {
|
||||
return (
|
||||
<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">
|
||||
@@ -17,14 +25,12 @@ export function Header({ onRepositoryChange }: HeaderProps) {
|
||||
|
||||
{/* 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 onChange={
|
||||
(e) => onRepositoryChange(e.target.value as Repository)
|
||||
} 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">
|
||||
{Object.values(Repository).map((repository) => (
|
||||
<option key={repository} value={repository}>{((repository === Repository.ALL) ? 'All Repositories' : repository.charAt(0).toUpperCase() + repository.slice(1))}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
@@ -1,12 +1,16 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
useState,
|
||||
JSX
|
||||
} from 'react';
|
||||
|
||||
import { Terminal, Copy, Check } from 'lucide-react';
|
||||
|
||||
interface InstallGuideProps {
|
||||
packageName: string;
|
||||
}
|
||||
|
||||
export function InstallGuide({ packageName }: InstallGuideProps) {
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
export function InstallGuide({ packageName }: InstallGuideProps): JSX.Element {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const command = `sudo pacman -S ${packageName}`;
|
||||
|
||||
|
@@ -1,4 +1,8 @@
|
||||
export function Logo() {
|
||||
import {
|
||||
JSX
|
||||
} from 'react';
|
||||
|
||||
export function Logo(): JSX.Element {
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Replace this SVG with your custom logo */}
|
||||
|
@@ -1,4 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
useState,
|
||||
JSX
|
||||
} from 'react';
|
||||
|
||||
import { Box, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { Package } from '../types';
|
||||
import { InstallGuide } from './InstallGuide';
|
||||
@@ -7,7 +11,7 @@ interface PackageCardProps {
|
||||
package: Package;
|
||||
}
|
||||
|
||||
export function PackageCard({ package: pkg }: PackageCardProps) {
|
||||
export function PackageCard({ package: pkg }: PackageCardProps): JSX.Element {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
|
@@ -1,13 +1,38 @@
|
||||
import React from 'react';
|
||||
import React, {
|
||||
JSX
|
||||
} from 'react';
|
||||
|
||||
interface BadgeProps {
|
||||
children: React.ReactNode;
|
||||
color?: 'default' | 'success' | 'warning' | 'error'; // Add more colors as needed
|
||||
size?: 'small' | 'medium' | 'large'; // Size options
|
||||
ariaLabel?: string; // For accessibility
|
||||
}
|
||||
|
||||
export function Badge({ children }: BadgeProps) {
|
||||
const colorClasses = {
|
||||
default: 'bg-gradient-to-r from-nord-7 to-nord-8/80 dark:from-nord-8/50 dark:to-nord-9/80 text-nord-0 dark:text-nord-6',
|
||||
success: 'bg-green-500 text-white',
|
||||
warning: 'bg-yellow-500 text-black',
|
||||
error: 'bg-red-500 text-white',
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
small: 'text-xs px-2 py-1',
|
||||
medium: 'text-sm px-3 py-1.5',
|
||||
large: 'text-base px-4 py-2',
|
||||
};
|
||||
|
||||
export function Badge({ children, color = 'default', size = 'medium', ariaLabel }: BadgeProps): JSX.Element {
|
||||
const badgeColorClass = colorClasses[color] || colorClasses.default;
|
||||
const badgeSizeClass = sizeClasses[size] || sizeClasses.medium;
|
||||
|
||||
return (
|
||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-gradient-to-r from-nord-7 to-nord-8/80 dark:from-nord-8/50 dark:to-nord-9/80 text-nord-0 dark:text-nord-6 shadow-md hover:shadow-lg transition-all duration-300">
|
||||
<span
|
||||
className={`inline-flex items-center rounded-full font-semibold shadow-md hover:shadow-lg transition-all duration-300 ${badgeColorClass} ${badgeSizeClass}`}
|
||||
aria-label={ariaLabel}
|
||||
title={ariaLabel} // Optional: Add title for tooltips
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
JSX
|
||||
} from 'react';
|
||||
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
|
||||
interface ExpandButtonProps {
|
||||
@@ -5,7 +9,7 @@ interface ExpandButtonProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export function ExpandButton({ expanded, onClick }: ExpandButtonProps) {
|
||||
export function ExpandButton({ expanded, onClick }: ExpandButtonProps): JSX.Element {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
|
@@ -1,4 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
useState,
|
||||
JSX
|
||||
} from 'react';
|
||||
|
||||
import { Boxes } from 'lucide-react';
|
||||
import { Package } from '../../types';
|
||||
import { InstallGuide } from '../InstallGuide';
|
||||
@@ -9,7 +13,7 @@ interface PackageCardProps {
|
||||
package: Package;
|
||||
}
|
||||
|
||||
export function PackageCard({ package: pkg }: PackageCardProps) {
|
||||
export function PackageCard({ package: pkg }: PackageCardProps): JSX.Element {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -37,6 +41,7 @@ export function PackageCard({ package: pkg }: PackageCardProps) {
|
||||
<ExpandButton
|
||||
expanded={expanded}
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
aria-label={expanded ? 'Collapse package details' : 'Expand package details'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,4 +56,4 @@ export function PackageCard({ package: pkg }: PackageCardProps) {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
JSX
|
||||
} from 'react';
|
||||
|
||||
import { Package } from '../types';
|
||||
import { PackageCard } from './PackageCard';
|
||||
|
||||
@@ -6,7 +10,7 @@ interface PackageListProps {
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export function PackageList({ packages, loading }: PackageListProps) {
|
||||
export function PackageList({ packages, loading }: PackageListProps): JSX.Element {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
|
@@ -1,31 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
@@ -1,10 +1,14 @@
|
||||
import {
|
||||
JSX
|
||||
} from 'react';
|
||||
|
||||
import { Search } from 'lucide-react';
|
||||
|
||||
interface SearchBarProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
selectedRepository: 'core' | 'extra' | 'all';
|
||||
onFilterChange: (repo: 'core' | 'extra' | 'all') => void;
|
||||
//selectedRepository: 'core' | 'extra' | 'all';
|
||||
//onFilterChange: (repo: 'core' | 'extra' | 'all') => void;
|
||||
}
|
||||
|
||||
export function SearchBar({
|
||||
@@ -12,7 +16,7 @@ export function SearchBar({
|
||||
onChange,
|
||||
// selectedRepository,
|
||||
// onFilterChange,
|
||||
}: SearchBarProps) {
|
||||
}: SearchBarProps): JSX.Element {
|
||||
return (
|
||||
<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">
|
||||
|
@@ -1,7 +1,11 @@
|
||||
import {
|
||||
JSX
|
||||
} from 'react';
|
||||
|
||||
import { Moon, Sun } from 'lucide-react';
|
||||
import { useTheme } from '../hooks/useTheme';
|
||||
|
||||
export function ThemeToggle() {
|
||||
export function ThemeToggle(): JSX.Element {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
return (
|
||||
|
@@ -1,55 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
@@ -2,7 +2,12 @@ import { useState, useEffect, useCallback } from 'react';
|
||||
import { Package } from '../types';
|
||||
import { fetchPackages } from '../services/api';
|
||||
|
||||
export function usePackages() {
|
||||
export function usePackages(): {
|
||||
packages: Package[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
retryLoading: () => void;
|
||||
} {
|
||||
const [packages, setPackages] = useState<Package[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
@@ -1,13 +1,20 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
type Theme = 'light' | 'dark';
|
||||
import {
|
||||
Theme
|
||||
} from '../types';
|
||||
|
||||
export function useTheme() {
|
||||
const [theme, setTheme] = useState<Theme>(() => {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
export function useTheme(): {
|
||||
theme: Theme;
|
||||
toggleTheme: () => void;
|
||||
} {
|
||||
const getInitialTheme = (): Theme => {
|
||||
const savedTheme = localStorage.getItem('theme') as Theme;
|
||||
if (savedTheme === 'dark' || savedTheme === 'light') return savedTheme;
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
});
|
||||
};
|
||||
|
||||
const [theme, setTheme] = useState<Theme>(getInitialTheme);
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
@@ -17,8 +24,22 @@ export function useTheme() {
|
||||
}, [theme]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(prev => prev === 'light' ? 'dark' : 'light');
|
||||
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleMediaChange = (event: MediaQueryListEvent) => {
|
||||
setTheme(event.matches ? 'dark' : 'light');
|
||||
};
|
||||
|
||||
const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
mediaQueryList.addEventListener('change', handleMediaChange);
|
||||
|
||||
// Cleanup listener on unmount
|
||||
return () => {
|
||||
mediaQueryList.removeEventListener('change', handleMediaChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { theme, toggleTheme };
|
||||
}
|
11
src/types.ts
11
src/types.ts
@@ -1,5 +1,14 @@
|
||||
// Type alias for repository categories
|
||||
export type Repository = 'core' | 'extra' | 'community' | 'multilib';
|
||||
export enum Repository {
|
||||
ALL = 'all',
|
||||
CORE = 'core',
|
||||
EXTRA = 'extra',
|
||||
COMMUNITY = 'community',
|
||||
MULTILIB = 'multilib'
|
||||
}
|
||||
|
||||
// Type alias for UI themes
|
||||
export type Theme = ('light' | 'dark');
|
||||
|
||||
// Interface representing a single package
|
||||
export interface Package {
|
||||
|
Reference in New Issue
Block a user