mirror of
https://github.com/Snigdha-OS/package-browser.git
synced 2025-09-05 20:26:42 +02:00
chore: initiate the website file
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
28
eslint.config.js
Normal file
28
eslint.config.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import js from '@eslint/js';
|
||||||
|
import globals from 'globals';
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks';
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
14
index.html
Normal file
14
index.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="light">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Fira+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<title>Arch Linux Packages</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
4051
package-lock.json
generated
Normal file
4051
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
package.json
Normal file
33
package.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "vite-react-typescript-starter",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lucide-react": "^0.344.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.9.1",
|
||||||
|
"@types/react": "^18.3.5",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
|
"autoprefixer": "^10.4.18",
|
||||||
|
"eslint": "^9.9.1",
|
||||||
|
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.11",
|
||||||
|
"globals": "^15.9.0",
|
||||||
|
"postcss": "^8.4.35",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5.5.3",
|
||||||
|
"typescript-eslint": "^8.3.0",
|
||||||
|
"vite": "^5.4.2"
|
||||||
|
}
|
||||||
|
}
|
2552
pnpm-lock.yaml
generated
Normal file
2552
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
57
src/App.tsx
Normal file
57
src/App.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import React, { useState } 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() {
|
||||||
|
const { packages, loading, error } = usePackages();
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
|
const filteredPackages = packages.filter((pkg) =>
|
||||||
|
pkg.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||||
|
pkg.description.toLowerCase().includes(search.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-nord-6 dark:bg-nord-0 transition-colors">
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<div className="mb-8">
|
||||||
|
<SearchBar value={search} onChange={setSearch} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<p className="text-sm text-nord-3 dark:text-nord-4">
|
||||||
|
Showing {filteredPackages.length} packages
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error ? (
|
||||||
|
<div className="rounded-lg bg-nord-11/10 dark:bg-nord-11/20 p-4 text-nord-11">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<PackageList packages={filteredPackages} loading={loading} />
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer className="bg-nord-5 dark:bg-nord-1 border-t border-nord-4 dark:border-nord-2 mt-12 transition-colors">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
|
<p className="text-center text-sm text-nord-3 dark:text-nord-4">
|
||||||
|
Data sourced from the{' '}
|
||||||
|
<a
|
||||||
|
href="https://github.com/archlinux/svntogit-packages"
|
||||||
|
className="text-nord-10 hover:text-nord-9 dark:text-nord-8 dark:hover:text-nord-7"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Arch Linux Package Repository
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
17
src/components/Header.tsx
Normal file
17
src/components/Header.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Database } from 'lucide-react';
|
||||||
|
import { ThemeToggle } from './ThemeToggle';
|
||||||
|
import { Logo } from './Logo';
|
||||||
|
|
||||||
|
export function Header() {
|
||||||
|
return (
|
||||||
|
<header className="bg-gradient-to-r from-nord-9 to-nord-10 text-nord-6 shadow-lg">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Logo />
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
37
src/components/InstallGuide.tsx
Normal file
37
src/components/InstallGuide.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Terminal, Copy, Check } from 'lucide-react';
|
||||||
|
|
||||||
|
interface InstallGuideProps {
|
||||||
|
packageName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InstallGuide({ packageName }: InstallGuideProps) {
|
||||||
|
const [copied, setCopied] = React.useState(false);
|
||||||
|
|
||||||
|
const command = `sudo pacman -S ${packageName}`;
|
||||||
|
|
||||||
|
const copyCommand = () => {
|
||||||
|
navigator.clipboard.writeText(command);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-900 rounded-lg p-4 mt-4">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Terminal className="h-4 w-4 text-gray-400" />
|
||||||
|
<span className="text-gray-400 text-sm">Installation Command</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between bg-gray-800 rounded px-4 py-2">
|
||||||
|
<code className="text-blue-400 font-mono">{command}</code>
|
||||||
|
<button
|
||||||
|
onClick={copyCommand}
|
||||||
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
|
title="Copy to clipboard"
|
||||||
|
>
|
||||||
|
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
16
src/components/Logo.tsx
Normal file
16
src/components/Logo.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Database } from 'lucide-react';
|
||||||
|
|
||||||
|
export function Logo() {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-3 bg-white/10 rounded-lg">
|
||||||
|
<Database className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold tracking-tight">Snigdha OS Package List</h1>
|
||||||
|
<p className="text-nord-4 text-sm mt-1">Browse and search through the official Snigdha OS package repository</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
57
src/components/PackageCard.tsx
Normal file
57
src/components/PackageCard.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Boxes, ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
|
import { Package } from '../types';
|
||||||
|
import { InstallGuide } from './InstallGuide';
|
||||||
|
|
||||||
|
interface PackageCardProps {
|
||||||
|
package: Package;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PackageCard({ package: pkg }: PackageCardProps) {
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-nord-5 dark:bg-nord-1 rounded-lg shadow-sm border border-nord-4 dark:border-nord-2 hover:shadow-md transition-all overflow-hidden">
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="p-2 bg-nord-7/10 dark:bg-nord-7/20 rounded-lg">
|
||||||
|
<Boxes className="h-5 w-5 text-nord-7" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="font-medium text-nord-0 dark:text-nord-6">{pkg.name}</h3>
|
||||||
|
<span className="text-sm text-nord-3 dark:text-nord-4">{pkg.version}</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-sm text-nord-2 dark:text-nord-4">{pkg.description}</p>
|
||||||
|
<div className="mt-2 flex items-center justify-between">
|
||||||
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-nord-8/10 dark:bg-nord-8/20 text-nord-10 dark:text-nord-8">
|
||||||
|
{pkg.repository}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setExpanded(!expanded)}
|
||||||
|
className="text-nord-9 dark:text-nord-8 hover:text-nord-10 dark:hover:text-nord-7 flex items-center gap-1 text-sm"
|
||||||
|
>
|
||||||
|
{expanded ? (
|
||||||
|
<>
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
Hide installation
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
Show installation
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{expanded && (
|
||||||
|
<div className="border-t border-nord-4 dark:border-nord-2 p-4 bg-nord-6 dark:bg-nord-0">
|
||||||
|
<InstallGuide packageName={pkg.name} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
13
src/components/PackageCard/Badge.tsx
Normal file
13
src/components/PackageCard/Badge.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface BadgeProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Badge({ children }: BadgeProps) {
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-nord-8/10 dark:bg-nord-8/20 text-nord-10 dark:text-nord-8">
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
28
src/components/PackageCard/ExpandButton.tsx
Normal file
28
src/components/PackageCard/ExpandButton.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
|
|
||||||
|
interface ExpandButtonProps {
|
||||||
|
expanded: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ExpandButton({ expanded, onClick }: ExpandButtonProps) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className="text-nord-9 dark:text-nord-8 hover:text-nord-10 dark:hover:text-nord-7 flex items-center gap-1 text-sm"
|
||||||
|
>
|
||||||
|
{expanded ? (
|
||||||
|
<>
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
Hide installation
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
Show installation
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
42
src/components/PackageCard/index.tsx
Normal file
42
src/components/PackageCard/index.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Boxes } from 'lucide-react';
|
||||||
|
import { Package } from '../../types';
|
||||||
|
import { InstallGuide } from '../InstallGuide';
|
||||||
|
import { Badge } from './Badge';
|
||||||
|
import { ExpandButton } from './ExpandButton';
|
||||||
|
|
||||||
|
interface PackageCardProps {
|
||||||
|
package: Package;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PackageCard({ package: pkg }: PackageCardProps) {
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-nord-5 dark:bg-nord-1 rounded-lg shadow-sm border border-nord-4 dark:border-nord-2 hover:shadow-md transition-all overflow-hidden">
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="p-2 bg-nord-7/10 dark:bg-nord-7/20 rounded-lg">
|
||||||
|
<Boxes className="h-5 w-5 text-nord-7" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="font-medium text-nord-0 dark:text-nord-6">{pkg.name}</h3>
|
||||||
|
<span className="text-sm text-nord-3 dark:text-nord-4">{pkg.version}</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-sm text-nord-2 dark:text-nord-4">{pkg.description}</p>
|
||||||
|
<div className="mt-2 flex items-center justify-between">
|
||||||
|
<Badge>{pkg.repository}</Badge>
|
||||||
|
<ExpandButton expanded={expanded} onClick={() => setExpanded(!expanded)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{expanded && (
|
||||||
|
<div className="border-t border-nord-4 dark:border-nord-2 p-4 bg-nord-6 dark:bg-nord-0">
|
||||||
|
<InstallGuide packageName={pkg.name} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
26
src/components/PackageList.tsx
Normal file
26
src/components/PackageList.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Package } from '../types';
|
||||||
|
import { PackageCard } from './PackageCard';
|
||||||
|
|
||||||
|
interface PackageListProps {
|
||||||
|
packages: Package[];
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PackageList({ packages, loading }: PackageListProps) {
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{packages.map((pkg) => (
|
||||||
|
<PackageCard key={pkg.name} package={pkg} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
24
src/components/SearchBar.tsx
Normal file
24
src/components/SearchBar.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Search } from 'lucide-react';
|
||||||
|
|
||||||
|
interface SearchBarProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SearchBar({ value, onChange }: SearchBarProps) {
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<Search className="h-5 w-5 text-nord-3 dark:text-nord-4" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
placeholder="Search packages..."
|
||||||
|
className="block w-full pl-10 pr-4 py-3 border border-nord-4 dark:border-nord-2 rounded-lg bg-nord-5 dark:bg-nord-1 focus:ring-2 focus:ring-nord-8 focus:border-transparent shadow-sm text-nord-0 dark:text-nord-6 placeholder-nord-3 dark:placeholder-nord-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
21
src/components/ThemeToggle.tsx
Normal file
21
src/components/ThemeToggle.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Moon, Sun } from 'lucide-react';
|
||||||
|
import { useTheme } from '../hooks/useTheme';
|
||||||
|
|
||||||
|
export function ThemeToggle() {
|
||||||
|
const { theme, toggleTheme } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={toggleTheme}
|
||||||
|
className="p-2 rounded-lg bg-nord-8/10 hover:bg-nord-8/20 transition-colors"
|
||||||
|
aria-label="Toggle theme"
|
||||||
|
>
|
||||||
|
{theme === 'dark' ? (
|
||||||
|
<Sun className="h-5 w-5 text-nord-4" />
|
||||||
|
) : (
|
||||||
|
<Moon className="h-5 w-5 text-nord-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
28
src/hooks/usePackages.ts
Normal file
28
src/hooks/usePackages.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Package } from '../types';
|
||||||
|
import { fetchPackages } from '../services/api';
|
||||||
|
|
||||||
|
export function usePackages() {
|
||||||
|
const [packages, setPackages] = useState<Package[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadPackages = async () => {
|
||||||
|
try {
|
||||||
|
const data = await fetchPackages();
|
||||||
|
setPackages(data);
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to load packages. Please try again later.');
|
||||||
|
console.error('Failed to fetch packages:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadPackages();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { packages, loading, error };
|
||||||
|
}
|
24
src/hooks/useTheme.ts
Normal file
24
src/hooks/useTheme.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
type Theme = 'light' | 'dark';
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
const [theme, setTheme] = useState<Theme>(() => {
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (savedTheme === 'dark' || savedTheme === 'light') return savedTheme;
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = window.document.documentElement;
|
||||||
|
root.classList.remove('light', 'dark');
|
||||||
|
root.classList.add(theme);
|
||||||
|
localStorage.setItem('theme', theme);
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
setTheme(prev => prev === 'light' ? 'dark' : 'light');
|
||||||
|
};
|
||||||
|
|
||||||
|
return { theme, toggleTheme };
|
||||||
|
}
|
3
src/index.css
Normal file
3
src/index.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import App from './App.tsx';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>
|
||||||
|
);
|
36
src/services/api.ts
Normal file
36
src/services/api.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Package } from '../types';
|
||||||
|
|
||||||
|
const MIRRORS = [
|
||||||
|
'https://raw.githubusercontent.com/d3v1l0n/snigdhaos-core/refs/heads/master/packages.txt',
|
||||||
|
'https://raw.githubusercontent.com/archlinux/svntogit-packages/master/packages.txt'
|
||||||
|
];
|
||||||
|
|
||||||
|
async function fetchFromMirror(url: string): Promise<Package[]> {
|
||||||
|
const response = await fetch(url);
|
||||||
|
const text = await response.text();
|
||||||
|
|
||||||
|
return text
|
||||||
|
.split('\n')
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((line) => {
|
||||||
|
const [name, version, ...descParts] = line.split(' ');
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
description: descParts.join(' '),
|
||||||
|
repository: 'core' as const,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPackages(): Promise<Package[]> {
|
||||||
|
for (const mirror of MIRRORS) {
|
||||||
|
try {
|
||||||
|
return await fetchFromMirror(mirror);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to fetch from mirror ${mirror}:`, error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('All mirrors failed to respond');
|
||||||
|
}
|
11
src/types.ts
Normal file
11
src/types.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export interface Package {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
description: string;
|
||||||
|
repository: 'core' | 'extra' | 'community' | 'multilib';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PackageResponse {
|
||||||
|
packages: Package[];
|
||||||
|
total: number;
|
||||||
|
}
|
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
33
tailwind.config.js
Normal file
33
tailwind.config.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Fira Sans', 'system-ui', 'sans-serif'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
nord: {
|
||||||
|
0: '#2E3440', // Polar Night
|
||||||
|
1: '#3B4252',
|
||||||
|
2: '#434C5E',
|
||||||
|
3: '#4C566A',
|
||||||
|
4: '#D8DEE9', // Snow Storm
|
||||||
|
5: '#E5E9F0',
|
||||||
|
6: '#ECEFF4',
|
||||||
|
7: '#8FBCBB', // Frost
|
||||||
|
8: '#88C0D0',
|
||||||
|
9: '#81A1C1',
|
||||||
|
10: '#5E81AC',
|
||||||
|
11: '#BF616A', // Aurora
|
||||||
|
12: '#D08770',
|
||||||
|
13: '#EBCB8B',
|
||||||
|
14: '#A3BE8C',
|
||||||
|
15: '#B48EAD',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
24
tsconfig.app.json
Normal file
24
tsconfig.app.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
22
tsconfig.node.json
Normal file
22
tsconfig.node.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
10
vite.config.ts
Normal file
10
vite.config.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: ['lucide-react'],
|
||||||
|
},
|
||||||
|
});
|
Reference in New Issue
Block a user