mirror of
https://github.com/yusufipk/RepoHub.git
synced 2026-09-11 10:36:07 +00:00
Initalize the project with front-end
This commit is contained in:
+162
@@ -0,0 +1,162 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Production builds
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage/
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
public
|
||||||
|
|
||||||
|
# Vite build output
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Rollup.js default build output
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Storybook build outputs
|
||||||
|
.out
|
||||||
|
.storybook-out
|
||||||
|
|
||||||
|
# Temporary folders
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
memory-bank/
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
*.db
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Kubernetes
|
||||||
|
*.kubeconfig
|
||||||
|
|
||||||
|
# Terraform
|
||||||
|
*.tfstate
|
||||||
|
*.tfstate.*
|
||||||
|
.terraform/
|
||||||
|
|
||||||
|
# Backup files
|
||||||
|
*.bak
|
||||||
|
*.backup
|
||||||
|
*.old
|
||||||
|
|
||||||
|
# Test files
|
||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
|
test-results.xml
|
||||||
|
|
||||||
|
# Sentry
|
||||||
|
.sentryclirc
|
||||||
|
|
||||||
|
# Local development
|
||||||
|
.local/
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
module.exports = {
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: 'en',
|
||||||
|
locales: ['en', 'tr'],
|
||||||
|
localeDetection: true,
|
||||||
|
},
|
||||||
|
fallbackLng: {
|
||||||
|
default: ['en'],
|
||||||
|
},
|
||||||
|
debug: process.env.NODE_ENV === 'development',
|
||||||
|
reloadOnPrerender: process.env.NODE_ENV === 'development',
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
experimental: {
|
||||||
|
appDir: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nextConfig;
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "repohub",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint",
|
||||||
|
"type-check": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"next": "14.0.4",
|
||||||
|
"react": "^18",
|
||||||
|
"react-dom": "^18",
|
||||||
|
"lucide-react": "^0.294.0",
|
||||||
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"tailwind-merge": "^2.1.0",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"react-query": "^3.39.3",
|
||||||
|
"zustand": "^4.4.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
|
"autoprefixer": "^10.0.1",
|
||||||
|
"postcss": "^8",
|
||||||
|
"tailwindcss": "^3.3.0",
|
||||||
|
"eslint": "^8",
|
||||||
|
"eslint-config-next": "14.0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+4729
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
--primary: 221.2 83.2% 53.3%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
--secondary: 210 40% 96%;
|
||||||
|
--secondary-foreground: 222.2 84% 4.9%;
|
||||||
|
--muted: 210 40% 96%;
|
||||||
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
--accent: 210 40% 96%;
|
||||||
|
--accent-foreground: 222.2 84% 4.9%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--input: 214.3 31.8% 91.4%;
|
||||||
|
--ring: 221.2 83.2% 53.3%;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
--card: 222.2 84% 4.9%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
--primary: 217.2 91.2% 59.8%;
|
||||||
|
--primary-foreground: 222.2 84% 4.9%;
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
--accent: 217.2 32.6% 17.5%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
--ring: 224.3 76.3% 94.1%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import './globals.css'
|
||||||
|
import type { Metadata } from 'next'
|
||||||
|
import { Inter } from 'next/font/google'
|
||||||
|
|
||||||
|
const inter = Inter({ subsets: ['latin'] })
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'RepoHub - Cross-Platform Package Manager',
|
||||||
|
description: 'Simplify software installation across Linux, Windows, and macOS with official repositories',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body className={inter.className}>{children}</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { RepoHubApp } from '@/components/RepoHubApp'
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
return <RepoHubApp />
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useMemo } from 'react'
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
|
import { mockPackages, categories, licenses } from '@/data/mockData'
|
||||||
|
import { Package, FilterOptions, Platform } from '@/types'
|
||||||
|
import { Search, Package as PackageIcon, Terminal, Monitor } from 'lucide-react'
|
||||||
|
|
||||||
|
interface PackageBrowserProps {
|
||||||
|
selectedPlatform: Platform | null
|
||||||
|
selectedPackages: Package[]
|
||||||
|
onPackageToggle: (pkg: Package) => void
|
||||||
|
onFiltersChange: (filters: FilterOptions) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PackageBrowser({
|
||||||
|
selectedPlatform,
|
||||||
|
selectedPackages,
|
||||||
|
onPackageToggle,
|
||||||
|
onFiltersChange
|
||||||
|
}: PackageBrowserProps) {
|
||||||
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
|
const [filters, setFilters] = useState<FilterOptions>({
|
||||||
|
platforms: [],
|
||||||
|
categories: [],
|
||||||
|
licenses: [],
|
||||||
|
types: [],
|
||||||
|
repositories: [],
|
||||||
|
searchQuery: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredPackages = useMemo(() => {
|
||||||
|
return mockPackages.filter(pkg => {
|
||||||
|
// Platform filter
|
||||||
|
if (selectedPlatform && pkg.platform !== selectedPlatform.id) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search filter
|
||||||
|
if (searchQuery && !pkg.name.toLowerCase().includes(searchQuery.toLowerCase()) &&
|
||||||
|
!pkg.description.toLowerCase().includes(searchQuery.toLowerCase())) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category filter
|
||||||
|
if (filters.categories.length > 0 && !filters.categories.includes(pkg.category)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// License filter
|
||||||
|
if (filters.licenses.length > 0 && !filters.licenses.includes(pkg.license)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type filter
|
||||||
|
if (filters.types.length > 0 && !filters.types.includes(pkg.type)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repository filter
|
||||||
|
if (filters.repositories.length > 0 && !filters.repositories.includes(pkg.repository)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}, [selectedPlatform, searchQuery, filters])
|
||||||
|
|
||||||
|
const handleFilterChange = (key: keyof FilterOptions, value: any) => {
|
||||||
|
const newFilters = { ...filters, [key]: value }
|
||||||
|
setFilters(newFilters)
|
||||||
|
onFiltersChange(newFilters)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPackageSelected = (pkg: Package) => {
|
||||||
|
return selectedPackages.some(selected => selected.id === pkg.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedPlatform) {
|
||||||
|
return (
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardContent className="flex items-center justify-center h-64">
|
||||||
|
<p className="text-muted-foreground">Please select a platform first</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Filters */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Filters</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
{/* Search */}
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search packages..."
|
||||||
|
className="w-full pl-10 pr-4 py-2 border border-input rounded-md bg-background"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Category Filter */}
|
||||||
|
<Select onValueChange={(value) =>
|
||||||
|
handleFilterChange('categories', value ? [value] : [])
|
||||||
|
}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Category" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{categories.map(category => (
|
||||||
|
<SelectItem key={category} value={category}>
|
||||||
|
{category}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{/* Type Filter */}
|
||||||
|
<Select onValueChange={(value) =>
|
||||||
|
handleFilterChange('types', value ? [value as 'gui' | 'cli'] : [])
|
||||||
|
}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="gui">GUI Applications</SelectItem>
|
||||||
|
<SelectItem value="cli">CLI Tools</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{/* Repository Filter */}
|
||||||
|
<Select onValueChange={(value) =>
|
||||||
|
handleFilterChange('repositories', value ? [value as 'official' | 'third-party'] : [])
|
||||||
|
}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Repository" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="official">Official Only</SelectItem>
|
||||||
|
<SelectItem value="third-party">Third Party</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Package List */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Available Packages ({filteredPackages.length})</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Select packages to include in your installation script
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{filteredPackages.map((pkg) => (
|
||||||
|
<div
|
||||||
|
key={pkg.id}
|
||||||
|
className={`p-4 border rounded-lg transition-colors ${
|
||||||
|
isPackageSelected(pkg)
|
||||||
|
? 'border-primary bg-primary/5'
|
||||||
|
: 'border-border hover:bg-secondary/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<Checkbox
|
||||||
|
checked={isPackageSelected(pkg)}
|
||||||
|
onCheckedChange={() => onPackageToggle(pkg)}
|
||||||
|
/>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center space-x-2 mb-1">
|
||||||
|
<h3 className="font-semibold truncate">{pkg.name}</h3>
|
||||||
|
{pkg.type === 'gui' ? (
|
||||||
|
<Monitor className="h-4 w-4 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<Terminal className="h-4 w-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
{pkg.repository === 'official' && (
|
||||||
|
<span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded">
|
||||||
|
Official
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground mb-2">{pkg.description}</p>
|
||||||
|
<div className="flex items-center space-x-4 text-xs text-muted-foreground">
|
||||||
|
<span>Version: {pkg.version}</span>
|
||||||
|
<span>License: {pkg.license}</span>
|
||||||
|
<span>Category: {pkg.category}</span>
|
||||||
|
{pkg.popularity && (
|
||||||
|
<span>Popularity: {pkg.popularity}%</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1 mt-2">
|
||||||
|
{pkg.tags.map(tag => (
|
||||||
|
<span
|
||||||
|
key={tag}
|
||||||
|
className="text-xs bg-secondary text-secondary-foreground px-2 py-1 rounded"
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{filteredPackages.length === 0 && (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<PackageIcon className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
||||||
|
<p className="text-muted-foreground">No packages found matching your criteria</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { platforms } from '@/data/mockData'
|
||||||
|
import { Platform } from '@/types'
|
||||||
|
|
||||||
|
interface PlatformSelectorProps {
|
||||||
|
selectedPlatform: Platform | null
|
||||||
|
onPlatformSelect: (platform: Platform) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PlatformSelector({ selectedPlatform, onPlatformSelect }: PlatformSelectorProps) {
|
||||||
|
return (
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Select Your Platform</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Choose your operating system and package manager to browse available packages
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{platforms.map((platform) => (
|
||||||
|
<Button
|
||||||
|
key={platform.id}
|
||||||
|
variant={selectedPlatform?.id === platform.id ? "default" : "outline"}
|
||||||
|
className="h-auto p-4 flex flex-col items-center space-y-2"
|
||||||
|
onClick={() => onPlatformSelect(platform)}
|
||||||
|
>
|
||||||
|
<div className="text-2xl">{platform.icon}</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="font-semibold">{platform.name}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">{platform.packageManager}</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{selectedPlatform && (
|
||||||
|
<div className="mt-4 p-4 bg-secondary rounded-md">
|
||||||
|
<p className="text-sm">
|
||||||
|
<strong>Selected:</strong> {selectedPlatform.name} ({selectedPlatform.packageManager})
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { PlatformSelector } from '@/components/PlatformSelector'
|
||||||
|
import { PackageBrowser } from '@/components/PackageBrowser'
|
||||||
|
import { SelectionManager } from '@/components/SelectionManager'
|
||||||
|
import { ScriptPreview } from '@/components/ScriptPreview'
|
||||||
|
import { generateScript } from '@/lib/scriptGenerator'
|
||||||
|
import { Platform, Package, SelectedPackage, FilterOptions, GeneratedScript } from '@/types'
|
||||||
|
|
||||||
|
export function RepoHubApp() {
|
||||||
|
const [selectedPlatform, setSelectedPlatform] = useState<Platform | null>(null)
|
||||||
|
const [selectedPackages, setSelectedPackages] = useState<SelectedPackage[]>([])
|
||||||
|
const [generatedScript, setGeneratedScript] = useState<GeneratedScript | null>(null)
|
||||||
|
|
||||||
|
const handlePlatformSelect = (platform: Platform) => {
|
||||||
|
setSelectedPlatform(platform)
|
||||||
|
// Clear selections when platform changes
|
||||||
|
setSelectedPackages([])
|
||||||
|
setGeneratedScript(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePackageToggle = (pkg: Package) => {
|
||||||
|
setSelectedPackages(prev => {
|
||||||
|
const exists = prev.some(p => p.id === pkg.id)
|
||||||
|
if (exists) {
|
||||||
|
return prev.filter(p => p.id !== pkg.id)
|
||||||
|
} else {
|
||||||
|
return [...prev, { ...pkg, selectedAt: new Date().toISOString() }]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePackageRemove = (pkgId: string) => {
|
||||||
|
setSelectedPackages(prev => prev.filter(p => p.id !== pkgId))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClearAll = () => {
|
||||||
|
setSelectedPackages([])
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleGenerateScript = () => {
|
||||||
|
if (selectedPlatform && selectedPackages.length > 0) {
|
||||||
|
const script = generateScript(selectedPackages, selectedPlatform)
|
||||||
|
setGeneratedScript(script)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFiltersChange = (filters: FilterOptions) => {
|
||||||
|
// Filters are handled internally by PackageBrowser
|
||||||
|
// This could be expanded to handle URL params or state management
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCloseScriptPreview = () => {
|
||||||
|
setGeneratedScript(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900 mb-4">
|
||||||
|
RepoHub
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-gray-600 mb-2">
|
||||||
|
Cross-Platform Package Manager
|
||||||
|
</p>
|
||||||
|
<p className="text-lg text-gray-500 max-w-2xl mx-auto">
|
||||||
|
Simplify software installation across Linux, Windows, and macOS with official repositories
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Platform Selector */}
|
||||||
|
<PlatformSelector
|
||||||
|
selectedPlatform={selectedPlatform}
|
||||||
|
onPlatformSelect={handlePlatformSelect}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Package Browser */}
|
||||||
|
<PackageBrowser
|
||||||
|
selectedPlatform={selectedPlatform}
|
||||||
|
selectedPackages={selectedPackages}
|
||||||
|
onPackageToggle={handlePackageToggle}
|
||||||
|
onFiltersChange={handleFiltersChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Selection Manager */}
|
||||||
|
<SelectionManager
|
||||||
|
selectedPackages={selectedPackages}
|
||||||
|
onPackageRemove={handlePackageRemove}
|
||||||
|
onClearAll={handleClearAll}
|
||||||
|
onGenerateScript={handleGenerateScript}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Script Preview Modal */}
|
||||||
|
{generatedScript && (
|
||||||
|
<ScriptPreview
|
||||||
|
generatedScript={generatedScript}
|
||||||
|
selectedPackages={selectedPackages}
|
||||||
|
selectedPlatform={selectedPlatform}
|
||||||
|
onClose={handleCloseScriptPreview}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { GeneratedScript, SelectedPackage, Platform } from '@/types'
|
||||||
|
import { Download, Copy, Check, Terminal, Shield } from 'lucide-react'
|
||||||
|
|
||||||
|
interface ScriptPreviewProps {
|
||||||
|
generatedScript: GeneratedScript | null
|
||||||
|
selectedPackages: SelectedPackage[]
|
||||||
|
selectedPlatform: Platform | null
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScriptPreview({
|
||||||
|
generatedScript,
|
||||||
|
selectedPackages,
|
||||||
|
selectedPlatform,
|
||||||
|
onClose
|
||||||
|
}: ScriptPreviewProps) {
|
||||||
|
const [copied, setCopied] = useState(false)
|
||||||
|
|
||||||
|
if (!generatedScript || !selectedPlatform) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(generatedScript.script)
|
||||||
|
setCopied(true)
|
||||||
|
setTimeout(() => setCopied(false), 2000)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy script:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDownload = () => {
|
||||||
|
const blob = new Blob([generatedScript.script], { type: 'text/plain' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = `install-packages-${selectedPlatform.id}.sh`
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getScriptLanguage = () => {
|
||||||
|
switch (selectedPlatform.id) {
|
||||||
|
case 'windows':
|
||||||
|
return 'powershell'
|
||||||
|
default:
|
||||||
|
return 'bash'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getScriptExtension = () => {
|
||||||
|
switch (selectedPlatform.id) {
|
||||||
|
case 'windows':
|
||||||
|
return '.ps1'
|
||||||
|
default:
|
||||||
|
return '.sh'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
|
||||||
|
<Card className="w-full max-w-4xl max-h-[90vh] overflow-hidden">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle className="flex items-center space-x-2">
|
||||||
|
<Terminal className="h-5 w-5" />
|
||||||
|
<span>Installation Script</span>
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Idempotent script for {selectedPlatform.name} using {selectedPlatform.packageManager}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" onClick={onClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{/* Script Info */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="p-3 bg-secondary rounded-lg">
|
||||||
|
<div className="flex items-center space-x-2 mb-1">
|
||||||
|
<Shield className="h-4 w-4 text-green-600" />
|
||||||
|
<span className="font-medium text-sm">Official Repositories</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
All packages from trusted sources
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-secondary rounded-lg">
|
||||||
|
<div className="flex items-center space-x-2 mb-1">
|
||||||
|
<Terminal className="h-4 w-4 text-blue-600" />
|
||||||
|
<span className="font-medium text-sm">Idempotent</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Safe to run multiple times
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-secondary rounded-lg">
|
||||||
|
<div className="flex items-center space-x-2 mb-1">
|
||||||
|
<Download className="h-4 w-4 text-purple-600" />
|
||||||
|
<span className="font-medium text-sm">{selectedPackages.length} Packages</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Ready for installation
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Package List */}
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-2">Included Packages:</h4>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{selectedPackages.map((pkg) => (
|
||||||
|
<span
|
||||||
|
key={pkg.id}
|
||||||
|
className="text-xs bg-primary/10 text-primary px-2 py-1 rounded"
|
||||||
|
>
|
||||||
|
{pkg.name} ({pkg.version})
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Script Content */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h4 className="font-medium">Script Content:</h4>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<Button variant="outline" size="sm" onClick={handleCopy}>
|
||||||
|
{copied ? (
|
||||||
|
<Check className="h-4 w-4 mr-2" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-4 w-4 mr-2" />
|
||||||
|
)}
|
||||||
|
{copied ? 'Copied!' : 'Copy'}
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" onClick={handleDownload}>
|
||||||
|
<Download className="h-4 w-4 mr-2" />
|
||||||
|
Download {getScriptExtension()}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<pre className="bg-muted p-4 rounded-lg text-sm overflow-x-auto max-h-64 overflow-y-auto">
|
||||||
|
<code>{generatedScript.script}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Usage Instructions */}
|
||||||
|
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||||
|
<h4 className="font-medium text-blue-900 mb-2">How to use:</h4>
|
||||||
|
<ol className="text-sm text-blue-800 space-y-1 list-decimal list-inside">
|
||||||
|
<li>Download the script file to your target machine</li>
|
||||||
|
<li>Make it executable (for Linux/macOS): <code className="bg-blue-100 px-1 rounded">chmod +x install-packages-{selectedPlatform.id}{getScriptExtension()}</code></li>
|
||||||
|
<li>Run the script with appropriate permissions</li>
|
||||||
|
<li>The script will automatically handle repository setup and package installation</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Package, SelectedPackage } from '@/types'
|
||||||
|
import { X, PackageOpen, Download } from 'lucide-react'
|
||||||
|
|
||||||
|
interface SelectionManagerProps {
|
||||||
|
selectedPackages: SelectedPackage[]
|
||||||
|
onPackageRemove: (pkgId: string) => void
|
||||||
|
onClearAll: () => void
|
||||||
|
onGenerateScript: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SelectionManager({
|
||||||
|
selectedPackages,
|
||||||
|
onPackageRemove,
|
||||||
|
onClearAll,
|
||||||
|
onGenerateScript
|
||||||
|
}: SelectionManagerProps) {
|
||||||
|
if (selectedPackages.length === 0) {
|
||||||
|
return (
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardContent className="flex items-center justify-center h-32">
|
||||||
|
<div className="text-center">
|
||||||
|
<PackageOpen className="h-8 w-8 text-muted-foreground mx-auto mb-2" />
|
||||||
|
<p className="text-muted-foreground">No packages selected</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle>Selected Packages ({selectedPackages.length})</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
These packages will be included in your installation script
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<Button variant="outline" size="sm" onClick={onClearAll}>
|
||||||
|
Clear All
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onGenerateScript}>
|
||||||
|
<Download className="h-4 w-4 mr-2" />
|
||||||
|
Generate Script
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2 max-h-64 overflow-y-auto">
|
||||||
|
{selectedPackages.map((pkg) => (
|
||||||
|
<div
|
||||||
|
key={pkg.id}
|
||||||
|
className="flex items-center justify-between p-3 bg-secondary rounded-lg"
|
||||||
|
>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<h4 className="font-medium truncate">{pkg.name}</h4>
|
||||||
|
<span className="text-xs bg-primary/10 text-primary px-2 py-1 rounded">
|
||||||
|
{pkg.version}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground truncate">
|
||||||
|
{pkg.description}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center space-x-2 text-xs text-muted-foreground mt-1">
|
||||||
|
<span>{pkg.platform}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{pkg.category}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{pkg.license}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPackageRemove(pkg.id)}
|
||||||
|
className="ml-2"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 p-3 bg-muted rounded-lg">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
<strong>Note:</strong> The generated script will use official repositories only
|
||||||
|
and will be idempotent (safe to run multiple times).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-4 py-2",
|
||||||
|
sm: "h-9 rounded-md px-3",
|
||||||
|
lg: "h-11 rounded-md px-8",
|
||||||
|
icon: "h-10 w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Button.displayName = "Button"
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Card = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Card.displayName = "Card"
|
||||||
|
|
||||||
|
const CardHeader = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardHeader.displayName = "CardHeader"
|
||||||
|
|
||||||
|
const CardTitle = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<h3
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-2xl font-semibold leading-none tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardTitle.displayName = "CardTitle"
|
||||||
|
|
||||||
|
const CardDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardDescription.displayName = "CardDescription"
|
||||||
|
|
||||||
|
const CardContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
|
))
|
||||||
|
CardContent.displayName = "CardContent"
|
||||||
|
|
||||||
|
const CardFooter = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex items-center p-6 pt-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardFooter.displayName = "CardFooter"
|
||||||
|
|
||||||
|
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||||
|
import { Check } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Checkbox = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator
|
||||||
|
className={cn("flex items-center justify-center text-current")}
|
||||||
|
>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
))
|
||||||
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Checkbox }
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||||
|
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Select = SelectPrimitive.Root
|
||||||
|
|
||||||
|
const SelectGroup = SelectPrimitive.Group
|
||||||
|
|
||||||
|
const SelectValue = SelectPrimitive.Value
|
||||||
|
|
||||||
|
const SelectTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
))
|
||||||
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const SelectScrollUpButton = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
))
|
||||||
|
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||||
|
|
||||||
|
const SelectScrollDownButton = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
))
|
||||||
|
SelectScrollDownButton.displayName =
|
||||||
|
SelectPrimitive.ScrollDownButton.displayName
|
||||||
|
|
||||||
|
const SelectContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||||
|
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
position === "popper" &&
|
||||||
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"p-1",
|
||||||
|
position === "popper" &&
|
||||||
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
))
|
||||||
|
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const SelectLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||||
|
|
||||||
|
const SelectItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
))
|
||||||
|
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const SelectSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectGroup,
|
||||||
|
SelectValue,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectContent,
|
||||||
|
SelectLabel,
|
||||||
|
SelectItem,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectScrollDownButton,
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import { Platform, Package } from '@/types'
|
||||||
|
|
||||||
|
export const platforms: Platform[] = [
|
||||||
|
{
|
||||||
|
id: 'ubuntu',
|
||||||
|
name: 'Ubuntu/Debian',
|
||||||
|
packageManager: 'apt',
|
||||||
|
icon: '🐧'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fedora',
|
||||||
|
name: 'Fedora',
|
||||||
|
packageManager: 'dnf',
|
||||||
|
icon: '🎩'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'arch',
|
||||||
|
name: 'Arch Linux',
|
||||||
|
packageManager: 'pacman',
|
||||||
|
icon: '🏛️'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'windows',
|
||||||
|
name: 'Windows',
|
||||||
|
packageManager: 'winget',
|
||||||
|
icon: '🪟'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'macos',
|
||||||
|
name: 'macOS',
|
||||||
|
packageManager: 'homebrew',
|
||||||
|
icon: '🍎'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export const mockPackages: Package[] = [
|
||||||
|
// Ubuntu/Debian packages
|
||||||
|
{
|
||||||
|
id: 'firefox',
|
||||||
|
name: 'Firefox',
|
||||||
|
description: 'Fast, private and safe web browser',
|
||||||
|
version: '119.0.1',
|
||||||
|
category: 'Internet',
|
||||||
|
license: 'MPL-2.0',
|
||||||
|
type: 'gui',
|
||||||
|
platform: 'ubuntu',
|
||||||
|
repository: 'official',
|
||||||
|
lastUpdated: '2024-01-15',
|
||||||
|
downloads: 1500000,
|
||||||
|
popularity: 95,
|
||||||
|
tags: ['browser', 'web', 'internet']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'vlc',
|
||||||
|
name: 'VLC Media Player',
|
||||||
|
description: 'Multi-platform multimedia player',
|
||||||
|
version: '3.0.20',
|
||||||
|
category: 'Multimedia',
|
||||||
|
license: 'GPL-2.0',
|
||||||
|
type: 'gui',
|
||||||
|
platform: 'ubuntu',
|
||||||
|
repository: 'official',
|
||||||
|
lastUpdated: '2024-01-10',
|
||||||
|
downloads: 800000,
|
||||||
|
popularity: 88,
|
||||||
|
tags: ['media', 'video', 'audio']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'git',
|
||||||
|
name: 'Git',
|
||||||
|
description: 'Fast, scalable, distributed revision control system',
|
||||||
|
version: '2.43.0',
|
||||||
|
category: 'Development',
|
||||||
|
license: 'GPL-2.0',
|
||||||
|
type: 'cli',
|
||||||
|
platform: 'ubuntu',
|
||||||
|
repository: 'official',
|
||||||
|
lastUpdated: '2024-01-12',
|
||||||
|
downloads: 2000000,
|
||||||
|
popularity: 98,
|
||||||
|
tags: ['version-control', 'development', 'cli']
|
||||||
|
},
|
||||||
|
// Windows packages
|
||||||
|
{
|
||||||
|
id: 'vscode',
|
||||||
|
name: 'Visual Studio Code',
|
||||||
|
description: 'Lightweight but powerful source code editor',
|
||||||
|
version: '1.85.1',
|
||||||
|
category: 'Development',
|
||||||
|
license: 'MIT',
|
||||||
|
type: 'gui',
|
||||||
|
platform: 'windows',
|
||||||
|
repository: 'official',
|
||||||
|
lastUpdated: '2024-01-14',
|
||||||
|
downloads: 5000000,
|
||||||
|
popularity: 99,
|
||||||
|
tags: ['editor', 'development', 'ide']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'discord',
|
||||||
|
name: 'Discord',
|
||||||
|
description: 'Voice, video and text communication',
|
||||||
|
version: '1.0.9013',
|
||||||
|
category: 'Communication',
|
||||||
|
license: 'Proprietary',
|
||||||
|
type: 'gui',
|
||||||
|
platform: 'windows',
|
||||||
|
repository: 'official',
|
||||||
|
lastUpdated: '2024-01-13',
|
||||||
|
downloads: 3000000,
|
||||||
|
popularity: 92,
|
||||||
|
tags: ['chat', 'voice', 'gaming']
|
||||||
|
},
|
||||||
|
// macOS packages
|
||||||
|
{
|
||||||
|
id: 'homebrew',
|
||||||
|
name: 'Homebrew',
|
||||||
|
description: 'The Missing Package Manager for macOS',
|
||||||
|
version: '4.1.11',
|
||||||
|
category: 'System',
|
||||||
|
license: 'BSD-2-Clause',
|
||||||
|
type: 'cli',
|
||||||
|
platform: 'macos',
|
||||||
|
repository: 'official',
|
||||||
|
lastUpdated: '2024-01-11',
|
||||||
|
downloads: 1000000,
|
||||||
|
popularity: 90,
|
||||||
|
tags: ['package-manager', 'system', 'cli']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export const categories = [
|
||||||
|
'Development',
|
||||||
|
'Internet',
|
||||||
|
'Multimedia',
|
||||||
|
'System',
|
||||||
|
'Communication',
|
||||||
|
'Office',
|
||||||
|
'Graphics',
|
||||||
|
'Games',
|
||||||
|
'Science',
|
||||||
|
'Utilities'
|
||||||
|
]
|
||||||
|
|
||||||
|
export const licenses = [
|
||||||
|
'MIT',
|
||||||
|
'GPL-2.0',
|
||||||
|
'GPL-3.0',
|
||||||
|
'Apache-2.0',
|
||||||
|
'BSD-2-Clause',
|
||||||
|
'BSD-3-Clause',
|
||||||
|
'MPL-2.0',
|
||||||
|
'Proprietary',
|
||||||
|
'LGPL-2.1',
|
||||||
|
'LGPL-3.0'
|
||||||
|
]
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// Internationalization temporarily disabled
|
||||||
|
// Will be implemented in later phase
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// Routing temporarily disabled
|
||||||
|
// Will be implemented in later phase
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
import { SelectedPackage, Platform, GeneratedScript } from '@/types'
|
||||||
|
|
||||||
|
export function generateScript(packages: SelectedPackage[], platform: Platform): GeneratedScript {
|
||||||
|
const script = createScriptContent(packages, platform)
|
||||||
|
|
||||||
|
return {
|
||||||
|
platform: platform.id,
|
||||||
|
script,
|
||||||
|
packages,
|
||||||
|
generatedAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createScriptContent(packages: SelectedPackage[], platform: Platform): string {
|
||||||
|
switch (platform.id) {
|
||||||
|
case 'ubuntu':
|
||||||
|
return generateUbuntuScript(packages)
|
||||||
|
case 'fedora':
|
||||||
|
return generateFedoraScript(packages)
|
||||||
|
case 'arch':
|
||||||
|
return generateArchScript(packages)
|
||||||
|
case 'windows':
|
||||||
|
return generateWindowsScript(packages)
|
||||||
|
case 'macos':
|
||||||
|
return generateMacOSScript(packages)
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported platform: ${platform.id}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateUbuntuScript(packages: SelectedPackage[]): string {
|
||||||
|
const packageNames = packages.map(p => p.name).join(' ')
|
||||||
|
|
||||||
|
return `#!/bin/bash
|
||||||
|
|
||||||
|
# RepoHub Installation Script for Ubuntu/Debian
|
||||||
|
# Generated on ${new Date().toISOString()}
|
||||||
|
# This script is idempotent and safe to run multiple times
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Starting package installation for Ubuntu/Debian..."
|
||||||
|
|
||||||
|
# Update package lists
|
||||||
|
echo "Updating package lists..."
|
||||||
|
sudo apt update
|
||||||
|
|
||||||
|
# Install packages
|
||||||
|
echo "Installing packages: ${packageNames}"
|
||||||
|
sudo apt install -y ${packageNames}
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
echo "Verifying installation..."
|
||||||
|
for package in ${packageNames}; do
|
||||||
|
if dpkg -l | grep -q "^ii $package "; then
|
||||||
|
echo "✓ $package installed successfully"
|
||||||
|
else
|
||||||
|
echo "✗ $package installation failed"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Installation completed!"`
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateFedoraScript(packages: SelectedPackage[]): string {
|
||||||
|
const packageNames = packages.map(p => p.name).join(' ')
|
||||||
|
|
||||||
|
return `#!/bin/bash
|
||||||
|
|
||||||
|
# RepoHub Installation Script for Fedora
|
||||||
|
# Generated on ${new Date().toISOString()}
|
||||||
|
# This script is idempotent and safe to run multiple times
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Starting package installation for Fedora..."
|
||||||
|
|
||||||
|
# Update package lists
|
||||||
|
echo "Updating package lists..."
|
||||||
|
sudo dnf update -y
|
||||||
|
|
||||||
|
# Install packages
|
||||||
|
echo "Installing packages: ${packageNames}"
|
||||||
|
sudo dnf install -y ${packageNames}
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
echo "Verifying installation..."
|
||||||
|
for package in ${packageNames}; do
|
||||||
|
if rpm -q $package >/dev/null 2>&1; then
|
||||||
|
echo "✓ $package installed successfully"
|
||||||
|
else
|
||||||
|
echo "✗ $package installation failed"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Installation completed!"`
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateArchScript(packages: SelectedPackage[]): string {
|
||||||
|
const packageNames = packages.map(p => p.name).join(' ')
|
||||||
|
|
||||||
|
return `#!/bin/bash
|
||||||
|
|
||||||
|
# RepoHub Installation Script for Arch Linux
|
||||||
|
# Generated on ${new Date().toISOString()}
|
||||||
|
# This script is idempotent and safe to run multiple times
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Starting package installation for Arch Linux..."
|
||||||
|
|
||||||
|
# Update package lists
|
||||||
|
echo "Updating package lists..."
|
||||||
|
sudo pacman -Sy --noconfirm
|
||||||
|
|
||||||
|
# Install packages
|
||||||
|
echo "Installing packages: ${packageNames}"
|
||||||
|
sudo pacman -S --noconfirm ${packageNames}
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
echo "Verifying installation..."
|
||||||
|
for package in ${packageNames}; do
|
||||||
|
if pacman -Qi $package >/dev/null 2>&1; then
|
||||||
|
echo "✓ $package installed successfully"
|
||||||
|
else
|
||||||
|
echo "✗ $package installation failed"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Installation completed!"`
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateWindowsScript(packages: SelectedPackage[]): string {
|
||||||
|
const packageNames = packages.map(p => p.id).join(' ')
|
||||||
|
|
||||||
|
return `# RepoHub Installation Script for Windows
|
||||||
|
# Generated on ${new Date().toISOString()}
|
||||||
|
# This script is idempotent and safe to run multiple times
|
||||||
|
|
||||||
|
Write-Host "Starting package installation for Windows..."
|
||||||
|
|
||||||
|
# Install packages using winget
|
||||||
|
Write-Host "Installing packages: ${packageNames}"
|
||||||
|
|
||||||
|
$packages = @(${packages.map(p => `'${p.id}'`).join(', ')})
|
||||||
|
|
||||||
|
foreach ($package in $packages) {
|
||||||
|
Write-Host "Installing $package..."
|
||||||
|
try {
|
||||||
|
winget install --id $package --accept-package-agreements --accept-source-agreements -e
|
||||||
|
Write-Host "✓ $package installed successfully"
|
||||||
|
} catch {
|
||||||
|
Write-Host "✗ $package installation failed: $_"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Installation completed!"`
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateMacOSScript(packages: SelectedPackage[]): string {
|
||||||
|
const packageNames = packages.map(p => p.name).join(' ')
|
||||||
|
|
||||||
|
return `#!/bin/bash
|
||||||
|
|
||||||
|
# RepoHub Installation Script for macOS
|
||||||
|
# Generated on ${new Date().toISOString()}
|
||||||
|
# This script is idempotent and safe to run multiple times
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Starting package installation for macOS..."
|
||||||
|
|
||||||
|
# Check if Homebrew is installed
|
||||||
|
if ! command -v brew &> /dev/null; then
|
||||||
|
echo "Homebrew is not installed. Installing Homebrew..."
|
||||||
|
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||||
|
else
|
||||||
|
echo "Homebrew is already installed. Updating..."
|
||||||
|
brew update
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install packages
|
||||||
|
echo "Installing packages: ${packageNames}"
|
||||||
|
brew install ${packageNames}
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
echo "Verifying installation..."
|
||||||
|
for package in ${packageNames}; do
|
||||||
|
if brew list --formula | grep -q "^$package$"; then
|
||||||
|
echo "✓ $package installed successfully"
|
||||||
|
else
|
||||||
|
echo "✗ $package installation failed"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Installation completed!"
|
||||||
|
}`
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
export function middleware(request: NextRequest) {
|
||||||
|
// Middleware temporarily disabled - just pass through
|
||||||
|
return NextResponse.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
/*
|
||||||
|
* Match all request paths except for the ones starting with:
|
||||||
|
* - api (API routes)
|
||||||
|
* - _next/static (static files)
|
||||||
|
* - _next/image (image optimization files)
|
||||||
|
* - favicon.ico (favicon file)
|
||||||
|
*/
|
||||||
|
'/((?!api|_next/static|_next/image|favicon.ico).*)',
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
export interface Platform {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
packageManager: string
|
||||||
|
icon: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Package {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
version: string
|
||||||
|
category: string
|
||||||
|
license: string
|
||||||
|
type: 'gui' | 'cli'
|
||||||
|
platform: string
|
||||||
|
repository: 'official' | 'third-party'
|
||||||
|
lastUpdated: string
|
||||||
|
downloads?: number
|
||||||
|
popularity?: number
|
||||||
|
tags: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilterOptions {
|
||||||
|
platforms: string[]
|
||||||
|
categories: string[]
|
||||||
|
licenses: string[]
|
||||||
|
types: ('gui' | 'cli')[]
|
||||||
|
repositories: ('official' | 'third-party')[]
|
||||||
|
searchQuery: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectedPackage extends Package {
|
||||||
|
selectedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GeneratedScript {
|
||||||
|
platform: string
|
||||||
|
script: string
|
||||||
|
packages: SelectedPackage[]
|
||||||
|
generatedAt: string
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
darkMode: ["class"],
|
||||||
|
content: [
|
||||||
|
'./pages/**/*.{ts,tsx}',
|
||||||
|
'./components/**/*.{ts,tsx}',
|
||||||
|
'./app/**/*.{ts,tsx}',
|
||||||
|
'./src/**/*.{ts,tsx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: "2rem",
|
||||||
|
screens: {
|
||||||
|
"2xl": "1400px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: "hsl(var(--border))",
|
||||||
|
input: "hsl(var(--input))",
|
||||||
|
ring: "hsl(var(--ring))",
|
||||||
|
background: "hsl(var(--background))",
|
||||||
|
foreground: "hsl(var(--foreground))",
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary))",
|
||||||
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted))",
|
||||||
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent))",
|
||||||
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: "hsl(var(--popover))",
|
||||||
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card))",
|
||||||
|
foreground: "hsl(var(--card-foreground))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
"accordion-down": {
|
||||||
|
from: { height: 0 },
|
||||||
|
to: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
},
|
||||||
|
"accordion-up": {
|
||||||
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
to: { height: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require("tailwindcss-animate")],
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "es6"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user