Compare commits

...

19 Commits

Author SHA1 Message Date
CELESTIFYX Team
0d1f1c5e74 Add @Xlebylllek as an author in metadata 2025-01-14 19:42:25 +02:00
CELESTIFYX Team
9b968adbff change packages.json file url 2025-01-14 19:07:24 +02:00
CELESTIFYX Team
751a77ac42 feat: Refactor package parsing from text format to JSON 2025-01-14 11:14:26 +02:00
CELESTIFYX Team
0e824e3a04 Update README.md 2025-01-12 22:35:37 +02:00
CELESTIFYX Team
16611f41c2 Update README.md 2025-01-12 22:33:23 +02:00
CELESTIFYX Team
b6084343da Refactoring i18n 2025-01-12 22:32:32 +02:00
XlebyllleK
c09cd6bd43 Add Polish language support 2025-01-12 19:37:36 +02:00
XlebyllleK
b2485151b6 Add footer to translation 2025-01-12 19:34:27 +02:00
eshanized
154cc8a6b0 🐛 fix: remove action status 2025-01-12 21:35:49 +05:30
eshanized
f1022a5a41 style: change to rich md 2025-01-12 21:35:15 +05:30
eshanized
7f264113c7 style: change style of the badge 2025-01-12 21:34:00 +05:30
eshanized
c9b4e6615a style: change badge style 2025-01-12 21:32:15 +05:30
eshanized
073d31a3b7 📝 docs: update repo structure 2025-01-12 21:29:22 +05:30
XlebyllleK
b21d623aa0 feat: Add language support (i18n) 2025-01-12 17:12:43 +02:00
eshanized
fc5b5ba217 📝 docs: add badges 2025-01-12 19:41:55 +05:30
Eshan Roy
6bc0c56de2 Merge PR #7 from XlebyllleK/master
feat: sort packages alphabetically
2025-01-12 10:57:28 +05:30
XlebyllleK
176bd3ef9d Update PackageList.tsx 2025-01-11 23:17:11 +02:00
XlebyllleK
3f2a66057a feat: Optimize package sorting with useMemo 2025-01-11 23:15:54 +02:00
XlebyllleK
9ff67af9f2 Sort packages alphabetically 2025-01-11 23:08:04 +02:00
23 changed files with 524 additions and 4267 deletions

View File

@@ -1,4 +1,12 @@
# 📦 Package Browser - Search Packages
# 📦 Package Browser - Search Packages
<p align="center">
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge" alt="License: MIT"></a>
<a href="https://github.com/Snigdha-OS/package-browser/releases"><img src="https://img.shields.io/github/package-json/v/Snigdha-OS/package-browser?style=for-the-badge" alt="Version"></a>
<a href="https://github.com/Snigdha-OS/package-browser/issues"><img src="https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=for-the-badge" alt="Contributions Welcome"></a>
<!-- <a href="https://github.com/Snigdha-OS/package-browser/actions"><img src="https://img.shields.io/github/actions/workflow/status/Snigdha-OS/package-browser/ci.yml?branch=main&style=for-the-badge" alt="Build Status"></a> -->
<a href="https://snigdha-os.github.io"><img src="https://img.shields.io/website?url=https%3A%2F%2Fsnigdha-os.github.io&style=for-the-badge" alt="Website Status"></a>
</p>
🚀 **Package Browser** is a user-friendly web application designed to simplify browsing, searching, and exploring packages available for Snigdha OS. This tool helps users find the right software with ease, whether for development, daily use, or penetration testing.
@@ -11,25 +19,27 @@
## 🗂️ Repository Structure
```
```plaintext
package-browser/
.
├── CODE_OF_CONDUCT.md
├── config.sh
├── eslint.config.js
├── index.html
├── package.json
├── package-lock.json
├── pnpm-lock.yaml
├── postcss.config.js
├── public
│ ├── favicon.ico
│ └── snigdhaos-og-image.png
├── push.sh
├── README.md
├── src
│ ├── App.tsx
│ ├── components
│ │ ├── Header.tsx
│ │ ├── InstallGuide.tsx
│ │ ├── Logo.tsx
│ │ ├── PackageCard
│ │ │ ├── Badge.tsx
│ │ │ ├── ExpandButton.tsx
│ │ │ └── index.tsx
│ │ ├── PackageCard.tsx
│ │ ├── PackageList.tsx
│ │ ├── SearchBar.tsx
@@ -37,7 +47,13 @@ package-browser/
│ ├── hooks
│ │ ├── usePackages.ts
│ │ └── useTheme.ts
│ ├── i18n.tsx
│ ├── index.css
│ ├── locales
│ │ ├── index.tsx
│ │ ├── en.tsx
│ │ ├── uk.tsx
│ │ └── pl.tsx
│ ├── main.tsx
│ ├── services
│ │ └── api.ts
@@ -47,7 +63,7 @@ package-browser/
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
└── vite.config.ts
```
## 🛠️ Installation
@@ -89,4 +105,4 @@ This project is licensed under the [MIT License](LICENSE).
## 💬 Feedback
Wed love to hear your feedback and suggestions! Feel free to open an issue or contact us directly via the [Snigdha OS](https://github.com/Snigdha-OS) organization.
Wed love to hear your feedback and suggestions! Feel free to open an issue or contact us directly via the [Snigdha OS](https://github.com/Snigdha-OS) organization.

View File

@@ -6,12 +6,12 @@
<meta name="description" content="Browse and explore Snigdha OS packages easily with this lightweight package viewer." />
<!-- Author Metadata -->
<meta name="author" content="Eshan Roy, d3v1l0n" />
<meta name="author:website" content="https://www.eshanized.github.io/, https://www.d3v1l0n.github.io/" />
<meta name="author:github" content="https://github.com/eshanized, https://github.com/d3v1l0n" />
<meta name="author" content="Eshan Roy, d3v1l0n, XlebyllleK" />
<meta name="author:website" content="https://www.eshanized.github.io/, https://www.d3v1l0n.github.io/, https://github.com/Xlebylllek/" />
<meta name="author:github" content="https://github.com/eshanized, https://github.com/d3v1l0n, https://github.com/XlebyllleK" />
<meta name="author:twitter" content="https://twitter.com/eshanized, https://twitter.com/d3v1l0n" />
<meta name="author:bio" content="Eshan Roy is a software developer, open-source enthusiast, and the creator of Snigdha OS. d3v1l0n is a cybersecurity expert and open-source contributor working on various security-focused tools." />
<meta name="author:email" content="m.eshanized@gmail.com, d3v1l0n@outlook.in" />
<meta name="author:bio" content="Eshan Roy is a software developer, open-source enthusiast, and the creator of Snigdha OS. d3v1l0n is a cybersecurity expert and open-source contributor working on various security-focused tools. XlebyllleK is a software developer, open-source enthusiast, and the creator of Snigdha OS." />
<meta name="author:email" content="m.eshanized@gmail.com, d3v1l0n@outlook.in, celestifyx@gmail.com" />
<!-- Open Graph Meta Tags for social media sharing -->
<meta property="og:title" content="Snigdha OS Packages" />

4051
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -35,7 +35,11 @@
"gh-pages": "^6.3.0",
"lucide-react": "^0.471.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"i18next": "^24.2.1",
"react-i18next": "^15.4.0",
"i18next-http-backend": "^3.0.1",
"i18next-browser-languagedetector": "^8.0.2"
},
"devDependencies": {
"@eslint/js": "^9.17.0",

130
pnpm-lock.yaml generated
View File

@@ -11,6 +11,15 @@ importers:
gh-pages:
specifier: ^6.3.0
version: 6.3.0
i18next:
specifier: ^24.2.1
version: 24.2.1(typescript@5.7.3)
i18next-browser-languagedetector:
specifier: ^8.0.2
version: 8.0.2
i18next-http-backend:
specifier: ^3.0.1
version: 3.0.1
lucide-react:
specifier: ^0.471.0
version: 0.471.0(react@19.0.0)
@@ -20,6 +29,9 @@ importers:
react-dom:
specifier: ^19.0.0
version: 19.0.0(react@19.0.0)
react-i18next:
specifier: ^15.4.0
version: 15.4.0(i18next@24.2.1(typescript@5.7.3))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
devDependencies:
'@eslint/js':
specifier: ^9.17.0
@@ -141,6 +153,10 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/runtime@7.26.0':
resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==}
engines: {node: '>=6.9.0'}
'@babel/template@7.25.9':
resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==}
engines: {node: '>=6.9.0'}
@@ -690,6 +706,9 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
cross-fetch@4.0.0:
resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -930,6 +949,23 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
html-parse-stringify@3.0.1:
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
i18next-browser-languagedetector@8.0.2:
resolution: {integrity: sha512-shBvPmnIyZeD2VU5jVGIOWP7u9qNG3Lj7mpaiPFpbJ3LVfHZJvVzKR4v1Cb91wAOFpNw442N+LGPzHOHsten2g==}
i18next-http-backend@3.0.1:
resolution: {integrity: sha512-XT2lYSkbAtDE55c6m7CtKxxrsfuRQO3rUfHzj8ZyRtY9CkIX3aRGwXGTkUhpGWce+J8n7sfu3J0f2wTzo7Lw0A==}
i18next@24.2.1:
resolution: {integrity: sha512-Q2wC1TjWcSikn1VAJg13UGIjc+okpFxQTxjVAymOnSA3RpttBQNMPf2ovcgoFVsV4QNxTfNZMAxorXZXsk4fBA==}
peerDependencies:
typescript: ^5
peerDependenciesMeta:
typescript:
optional: true
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@@ -1078,6 +1114,15 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
node-releases@2.0.19:
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
@@ -1223,6 +1268,19 @@ packages:
peerDependencies:
react: ^19.0.0
react-i18next@15.4.0:
resolution: {integrity: sha512-Py6UkX3zV08RTvL6ZANRoBh9sL/ne6rQq79XlkHEdd82cZr2H9usbWpUNVadJntIZP2pu3M2rL1CN+5rQYfYFw==}
peerDependencies:
i18next: '>= 23.2.3'
react: '>= 16.8.0'
react-dom: '*'
react-native: '*'
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
react-refresh@0.14.2:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
engines: {node: '>=0.10.0'}
@@ -1238,6 +1296,9 @@ packages:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@@ -1344,6 +1405,9 @@ packages:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
trim-repeated@1.0.0:
resolution: {integrity: sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==}
engines: {node: '>=0.10.0'}
@@ -1429,6 +1493,16 @@ packages:
yaml:
optional: true
void-elements@3.1.0:
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
engines: {node: '>=0.10.0'}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
@@ -1554,6 +1628,10 @@ snapshots:
'@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.26.5
'@babel/runtime@7.26.0':
dependencies:
regenerator-runtime: 0.14.1
'@babel/template@7.25.9':
dependencies:
'@babel/code-frame': 7.26.2
@@ -2037,6 +2115,12 @@ snapshots:
convert-source-map@2.0.0: {}
cross-fetch@4.0.0:
dependencies:
node-fetch: 2.7.0
transitivePeerDependencies:
- encoding
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@@ -2310,6 +2394,26 @@ snapshots:
dependencies:
function-bind: 1.1.2
html-parse-stringify@3.0.1:
dependencies:
void-elements: 3.1.0
i18next-browser-languagedetector@8.0.2:
dependencies:
'@babel/runtime': 7.26.0
i18next-http-backend@3.0.1:
dependencies:
cross-fetch: 4.0.0
transitivePeerDependencies:
- encoding
i18next@24.2.1(typescript@5.7.3):
dependencies:
'@babel/runtime': 7.26.0
optionalDependencies:
typescript: 5.7.3
ignore@5.3.2: {}
import-fresh@3.3.0:
@@ -2435,6 +2539,10 @@ snapshots:
natural-compare@1.4.0: {}
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
node-releases@2.0.19: {}
normalize-path@3.0.0: {}
@@ -2551,6 +2659,15 @@ snapshots:
react: 19.0.0
scheduler: 0.25.0
react-i18next@15.4.0(i18next@24.2.1(typescript@5.7.3))(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
'@babel/runtime': 7.26.0
html-parse-stringify: 3.0.1
i18next: 24.2.1(typescript@5.7.3)
react: 19.0.0
optionalDependencies:
react-dom: 19.0.0(react@19.0.0)
react-refresh@0.14.2: {}
react@19.0.0: {}
@@ -2563,6 +2680,8 @@ snapshots:
dependencies:
picomatch: 2.3.1
regenerator-runtime@0.14.1: {}
resolve-from@4.0.0: {}
resolve@1.22.10:
@@ -2701,6 +2820,8 @@ snapshots:
dependencies:
is-number: 7.0.0
tr46@0.0.3: {}
trim-repeated@1.0.0:
dependencies:
escape-string-regexp: 1.0.5
@@ -2751,6 +2872,15 @@ snapshots:
jiti: 1.21.7
yaml: 2.7.0
void-elements@3.1.0: {}
webidl-conversions@3.0.1: {}
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
which@2.0.2:
dependencies:
isexe: 2.0.0

View File

@@ -13,6 +13,10 @@ import {
Repository
} from './types';
import {
translate
} from './i18n';
export default function App(): JSX.Element {
const { packages, loading, error } = usePackages();
const [search, setSearch] = useState('');
@@ -47,6 +51,8 @@ export default function App(): JSX.Element {
setSelectedRepository(repo);
};
const count = filteredPackages.length;
return (
<div className="min-h-screen bg-nord-6 dark:bg-nord-0 transition-colors" role="main">
<Header onRepositoryChange={handleRepositoryFilterChange} />
@@ -59,26 +65,30 @@ export default function App(): JSX.Element {
{/* Package Counter */}
<div className="mb-4 flex items-center justify-between">
<p className="text-sm text-nord-3 dark:text-nord-4" aria-live="polite">
Showing {filteredPackages.length} package{filteredPackages.length !== 1 ? 's' : ''}
</p>
<p className="text-sm text-nord-3 dark:text-nord-4" aria-live="polite">{(count === 1) ? translate("App.package_count.single", {
count
}) : (count >= 2) && (count <= 4) ? translate("App.package_count.multiple", {
count
}) : translate("App.package_count.plural", {
count
})}</p>
</div>
{/* Error State */}
{error ? (
<div 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>
<button
onClick={() => window.location.reload()}
className="mt-2 inline-block text-sm text-nord-10 hover:underline"
>
Retry
</button>
<p>{translate("App.error.fetching-packages", {
error
})}</p>
<button onClick={
() => window.location.reload()
} className="mt-2 inline-block text-sm text-nord-10 hover:underline">{translate("App.error.retry-fetching-packages")}</button>
</div>
) : filteredPackages.length === 0 ? (
) : (count === 0) ? (
// Empty State
<div className="text-center text-nord-3 dark:text-nord-4 mt-12">
<p>No packages found matching your search.</p>
<p>{translate("App.no-packages-found")}</p>
</div>
) : (
// Package List
@@ -89,7 +99,7 @@ export default function App(): JSX.Element {
<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 Source:{' '}
{translate("App.footer")}{' '}
<a
href="https://github.com/Snigdha-OS/snigdhaos-core"
className="text-nord-10 hover:text-nord-9 dark:text-nord-8 dark:hover:text-nord-7"

View File

@@ -1,18 +1,31 @@
import {
JSX
JSX,
useEffect
} from 'react';
import { ThemeToggle } from './ThemeToggle';
import { Logo } from './Logo';
import {
ThemeToggle
} from './ThemeToggle';
import {
Repository
Logo
} from './Logo';
import {
Repository,
Languages
} from '../types';
import {
MIRRORS
} from '../services/api';
import {
translate
} from '../i18n';
import i18next from 'i18next';
interface HeaderProps {
onRepositoryChange: (repo: Repository) => void;
}
@@ -23,12 +36,24 @@ export function Header({
const usedRepositories = new Set(Object.values(MIRRORS).map(mirror => mirror.repository));
const filteredRepository = Object.keys(Repository).reduce((acc, key) => {
// This code is flawed because it explicitly checks for 'ALL', reducing flexibility.
// A more generic approach should be used to filter unused repositories while preserving 'ALL'.
if ((key === 'ALL') || usedRepositories.has(Repository[key as keyof typeof Repository])) acc[key] = Repository[key as keyof typeof Repository];
return acc;
}, {} as Record<string, string>);
const handleLanguageChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const language = event.target.value;
i18next.changeLanguage(language).then(() => {
localStorage.setItem('selectedLanguage', language);
window.location.reload();
});
};
useEffect(() => {
const savedLanguage = localStorage.getItem('selectedLanguage');
if (savedLanguage && (savedLanguage !== i18next.language)) i18next.changeLanguage(savedLanguage);
}, []);
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">
@@ -46,7 +71,18 @@ export function Header({
(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(filteredRepository).map((repository) => (
<option key={repository} value={repository}>{((repository === Repository.ALL) ? 'All Repositories' : repository.charAt(0).toUpperCase() + repository.slice(1))}</option>
<option key={repository} value={repository}>{((repository === Repository.ALL) ? translate("Header.all-repositories") : repository.charAt(0).toUpperCase() + repository.slice(1))}</option>
))}
</select>
</div>
{/* Language Dropdown */}
<div>
<select onChange={handleLanguageChange} defaultValue={i18next.language} 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.entries(Languages).map(([key, label]) => (
<option key={key} value={key.toLowerCase()}>
{label}
</option>
))}
</select>
</div>

View File

@@ -5,6 +5,10 @@ import {
import { Terminal, Copy, Check } from 'lucide-react';
import {
translate
} from '../i18n';
interface InstallGuideProps {
packageName: string;
}
@@ -25,20 +29,13 @@ export function InstallGuide({ packageName }: InstallGuideProps): JSX.Element {
{/* Title Section */}
<div className="flex items-center gap-3 mb-4">
<Terminal className="h-5 w-5 text-nord-9 dark:text-nord-7" />
<span className="text-nord-2 dark:text-nord-5 text-sm font-semibold uppercase tracking-wide">
Installation Command
</span>
<span className="text-nord-2 dark:text-nord-5 text-sm font-semibold uppercase tracking-wide">{translate("InstallGuide.installation-command")}</span>
</div>
{/* Command Display Section */}
<div className="flex items-center justify-between bg-nord-4 dark:bg-nord-2 rounded-lg px-5 py-3">
<code className="text-nord-8 dark:text-nord-6 font-mono text-sm truncate">{command}</code>
<button
onClick={copyCommand}
className="flex items-center justify-center w-8 h-8 rounded-full bg-nord-3 hover:bg-nord-7 dark:bg-nord-5 dark:hover:bg-nord-8 transition-colors"
title={copied ? 'Copied!' : 'Copy to clipboard'}
aria-label={copied ? 'Copied' : 'Copy command'}
>
<button onClick={copyCommand} className="flex items-center justify-center w-8 h-8 rounded-full bg-nord-3 hover:bg-nord-7 dark:bg-nord-5 dark:hover:bg-nord-8 transition-colors" title={copied ? translate("InstallGuide.copy.success") : translate("InstallGuide.copy.prompt")}>
{copied ? (
<Check className="h-4 w-4 text-nord-6 dark:text-nord-0" />
) : (

View File

@@ -2,6 +2,10 @@ import {
JSX
} from 'react';
import {
translate
} from '../i18n';
export function Logo(): JSX.Element {
return (
<div className="flex items-center gap-3">
@@ -22,10 +26,8 @@ export function Logo(): JSX.Element {
</svg>
</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>
<h1 className="text-2xl font-bold tracking-tight">{translate("Logo.title")}</h1>
<p className="text-nord-4 text-sm mt-1">{translate("Logo.description")}</p>
</div>
</div>
);

View File

@@ -7,6 +7,10 @@ import { Box, ChevronDown, ChevronUp } from 'lucide-react';
import { Package } from '../types';
import { InstallGuide } from './InstallGuide';
import {
translate
} from '../i18n';
interface PackageCardProps {
package: Package;
}
@@ -49,12 +53,12 @@ export function PackageCard({ package: pkg }: PackageCardProps): JSX.Element {
{expanded ? (
<>
<ChevronUp className="h-5 w-5 animate-bounce" />
Hide Installation
{translate("PackageCard.hide-installation")}
</>
) : (
<>
<ChevronDown className="h-5 w-5 animate-bounce" />
Show Installation
{translate("PackageCard.show-installation")}
</>
)}
</button>

View File

@@ -1,38 +0,0 @@
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
}
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 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>
);
}

View File

@@ -1,28 +0,0 @@
import {
JSX
} from 'react';
import { ChevronDown, ChevronUp } from 'lucide-react';
interface ExpandButtonProps {
expanded: boolean;
onClick: () => void;
}
export function ExpandButton({ expanded, onClick }: ExpandButtonProps): JSX.Element {
return (
<button
onClick={onClick}
className="flex items-center gap-2 px-3 py-1 text-sm font-medium text-nord-9 dark:text-nord-8 hover:text-nord-10 dark:hover:text-nord-7 focus:outline-none focus:ring-2 focus:ring-nord-8 dark:focus:ring-nord-9 rounded-lg transition-all duration-300"
>
<span
className={`transform transition-transform duration-300 ${
expanded ? 'rotate-180' : ''
}`}
>
{expanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</span>
{expanded ? 'Hide installation' : 'Show installation'}
</button>
);
}

View File

@@ -1,59 +0,0 @@
import {
useState,
JSX
} 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): JSX.Element {
const [expanded, setExpanded] = useState(false);
return (
<div className="group bg-nord-5 dark:bg-nord-1 rounded-xl shadow-lg border border-nord-4 dark:border-nord-2 hover:shadow-xl transition-all duration-300 overflow-hidden">
{/* Header Section */}
<div className="p-5">
<div className="flex items-start gap-4">
{/* Icon */}
<div className="p-3 bg-nord-7/10 dark:bg-nord-7/20 rounded-full shadow-md group-hover:scale-105 transition-transform">
<Boxes className="h-6 w-6 text-nord-7" />
</div>
{/* Content */}
<div className="flex-1">
<div className="flex items-center justify-between">
<h3 className="font-semibold text-lg text-nord-0 dark:text-nord-6 group-hover:text-nord-8 transition-colors">
{pkg.name}
</h3>
<span className="text-sm text-nord-3 dark:text-nord-4">{pkg.version}</span>
</div>
<p className="mt-2 text-sm text-nord-2 dark:text-nord-4 line-clamp-2">
{pkg.description}
</p>
<div className="mt-3 flex items-center justify-between">
<Badge>{pkg.repository}</Badge>
<ExpandButton
expanded={expanded}
onClick={() => setExpanded(!expanded)}
aria-label={expanded ? 'Collapse package details' : 'Expand package details'}
/>
</div>
</div>
</div>
</div>
{/* Expandable Section */}
{expanded && (
<div className="border-t border-nord-4 dark:border-nord-2 bg-nord-6 dark:bg-nord-0 p-5 transition-opacity duration-300">
<InstallGuide packageName={pkg.name} />
</div>
)}
</div>
);
}

View File

@@ -1,29 +1,42 @@
import {
JSX
JSX,
useMemo
} from 'react';
import { Package } from '../types';
import { PackageCard } from './PackageCard';
import {
Package
} from '../types';
import {
PackageCard
} from './PackageCard';
interface PackageListProps {
packages: Package[];
loading: boolean;
packages: Package[];
loading: boolean;
}
export function PackageList({ packages, loading }: PackageListProps): JSX.Element {
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>
export function PackageList({
packages,
loading
}: PackageListProps): JSX.Element {
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>
);
}
const sortedPackages = useMemo(
() => [...packages].sort(
(a, b) => a.name.localeCompare(b.name)
), [packages]
);
return (
<div className="grid gap-4">
{sortedPackages.map((pkg) => (
<PackageCard key={pkg.name} package={pkg} />
))}
</div>
);
}

View File

@@ -4,6 +4,10 @@ import {
import { Search } from 'lucide-react';
import {
translate
} from '../i18n';
interface SearchBarProps {
value: string;
onChange: (value: string) => void;
@@ -26,7 +30,7 @@ export function SearchBar({
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder="Search packages..."
placeholder={translate("SearchBar.placeholder")}
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"
/>

42
src/i18n.tsx Normal file
View File

@@ -0,0 +1,42 @@
import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import {
initReactI18next
} from "react-i18next";
import {
LOCALES
} from './locales';
const resources = Object.fromEntries(
Object.entries(LOCALES).map(([key, value]) => [
key.toLowerCase(),
{
translation: value
}
])
);
i18n.use(initReactI18next).use(LanguageDetector).init({
resources,
fallbackLng: (localStorage.getItem('selectedLanguage') || "en"),
interpolation: {
escapeValue: false
},
detection: {
order: ['localStorage', 'navigator'],
caches: ['localStorage']
}
});
export const translate = (key: string, options?: {
[key: string]: any
}): string => {
return i18n.t(key, options);
};
export default i18n;

50
src/locales/en.tsx Normal file
View File

@@ -0,0 +1,50 @@
export const EN = {
// src/components/Header.tsx
Header: {
"all-repositories": "All Repositories"
},
// src/components/InstallGuide.tsx
InstallGuide: {
"installation-command": "Installation Command",
"copy": {
"prompt": "Copy to clipboard",
"success": "Copied!"
}
},
// src/components/Logo.tsx
Logo: {
"title": "Snigdha OS Package List",
"description": "Browse and search through the official Snigdha OS package repository",
},
// src/components/PackageCard.tsx
PackageCard: {
"show-installation": "Show Installation",
"hide-installation": "Hide Installation"
},
// src/components/SearchBar.tsx
SearchBar: {
"placeholder": "Search packages..."
},
// src/App.tsx
App: {
"package_count": {
"single": "Showing {{count}} package",
"multiple": "Showing {{count}} packages",
"plural": "Showing {{count}} packages",
},
error: {
"fetching-packages": "An error occurred while fetching packages: {{error}}",
"retry-fetching-packages": "Retry"
},
"no-packages-found": "No packages found matching your search.",
"footer": "Data Source:"
}
};

17
src/locales/index.tsx Normal file
View File

@@ -0,0 +1,17 @@
import {
EN
} from './en'; // English
import {
UK
} from './uk'; // Ukrainian
import {
PL
} from './pl'; // Polish
export const LOCALES = {
EN,
UK,
PL
};

50
src/locales/pl.tsx Normal file
View File

@@ -0,0 +1,50 @@
export const PL = {
// src/components/Header.tsx
Header: {
"all-repositories": "Wszystkie repozytoria"
},
// src/components/InstallGuide.tsx
InstallGuide: {
"installation-command": "Polecenie instalacji",
"copy": {
"prompt": "Skopiuj do schowka",
"success": "Skopiowano!"
}
},
// src/components/Logo.tsx
Logo: {
"title": "Lista pakietów Snigdha OS",
"description": "Przeglądaj i wyszukuj w oficjalnym repozytorium pakietów Snigdha OS",
},
// src/components/PackageCard.tsx
PackageCard: {
"show-installation": "Pokaż instalację",
"hide-installation": "Ukryj instalację"
},
// src/components/SearchBar.tsx
SearchBar: {
"placeholder": "Szukaj pakietów..."
},
// src/App.tsx
App: {
"package_count": {
"single": "Wyświetlanie {{count}} pakietu",
"multiple": "Wyświetlanie {{count}} pakietów",
"plural": "Wyświetlanie {{count}} pakietów",
},
error: {
"fetching-packages": "Wystąpił błąd podczas pobierania pakietów: {{error}}",
"retry-fetching-packages": "Spróbuj ponownie"
},
"no-packages-found": "Nie znaleziono pakietów odpowiadających Twojemu wyszukiwaniu.",
"footer": "Źródło danych:"
}
};

50
src/locales/uk.tsx Normal file
View File

@@ -0,0 +1,50 @@
export const UK = {
// src/components/Header.tsx
Header: {
"all-repositories": "Усі репозиторії"
},
// src/components/InstallGuide.tsx
InstallGuide: {
"installation-command": "Команда для встановлення",
"copy": {
"prompt": "Копіювати в буфер обміну",
"success": "Скопійовано!"
}
},
// src/components/Logo.tsx
Logo: {
"title": "Список пакетів Snigdha OS",
"description": "Переглядайте та шукайте в офіційному репозиторії пакетів Snigdha OS",
},
// src/components/PackageCard.tsx
PackageCard: {
"show-installation": "Показати інструкцію з встановлення",
"hide-installation": "Сховати інструкцію з встановлення"
},
// src/components/SearchBar.tsx
SearchBar: {
"placeholder": "Шукати пакети..."
},
// src/App.tsx
App: {
"package_count": {
"single": "Показано {{count}} пакет",
"multiple": "Показано {{count}} пакети",
"plural": "Показано {{count}} пакетів"
},
error: {
"fetching-packages": "Сталася помилка при отриманні пакетів: {{error}}",
"retry-fetching-packages": "Спробувати ще раз"
},
"no-packages-found": "Не знайдено жодного пакета, що відповідає вашому запиту.",
"footer": "Джерело даних:"
}
};

View File

@@ -1,10 +1,17 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import {
StrictMode
} from 'react';
import {
createRoot
} from 'react-dom/client';
import App from './App.tsx';
import './index.css';
import './i18n';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
<StrictMode>
<App />
</StrictMode>
);

View File

@@ -9,33 +9,27 @@ export const MIRRORS: Record<string, {
repository: Repository;
}> = {
'core': {
url: 'https://raw.githubusercontent.com/Snigdha-OS/snigdhaos-core/refs/heads/master/packages.txt',
url: 'https://raw.githubusercontent.com/Snigdha-OS/snigdhaos-pkgbuilds/refs/heads/master/snigdhaos-core/packages.json',
repository: ('core' as Repository)
},
'extra': {
url: 'https://raw.githubusercontent.com/Snigdha-OS/snigdhaos-extra/refs/heads/master/packages.txt',
url: 'https://raw.githubusercontent.com/Snigdha-OS/snigdhaos-pkgbuilds/refs/heads/master/snigdhaos-extra/packages.json',
repository: ('extra' as Repository)
}
}
// Fetch data from a single mirror (Core or Extra repository)
// Fetch data from a single mirror
async function fetchFromMirror(url: string, repository: Repository): Promise<Package[]> {
const response = await fetch(url);
const text = await response.text();
// Parse the text file content and return a list of packages
return text.split('\n').filter(Boolean).map((line) => {
const [
name,
version,
...description
] = line.split(' ');
const data = await response.json();
// Parse the json file content and return a list of packages
return data.map((item: any) => {
return {
name,
version,
description: description.join(' '),
name: item.name,
version: item.version,
description: item.description,
repository
};
});

View File

@@ -10,6 +10,13 @@ export enum Repository {
// Type alias for UI themes
export type Theme = ('light' | 'dark');
// Enumeration of supported languages
export enum Languages {
EN = "English",
UK = "Українська",
PL = "Polski"
}
// Interface representing a single package
export interface Package {
/** The name of the package */