mirror of
https://github.com/yusufipk/RepoHub.git
synced 2026-09-11 18:46:07 +00:00
feat: add cryptocurrency donation support with Cryptomus integration
- Added support modal component with payment creation flow and multi-language translations - Integrated heart icon button in header to open donation modal - Refactored PackageBrowser to accept packages as props and simplified filtering logic
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useTheme } from '@/hooks/useTheme'
|
||||
import { useLocale } from '@/contexts/LocaleContext'
|
||||
import { Sun, Moon, Monitor, Globe } from 'lucide-react'
|
||||
import { SupportModal } from '@/components/SupportModal'
|
||||
import { Sun, Moon, Monitor, Globe, Heart } from 'lucide-react'
|
||||
|
||||
export function Header() {
|
||||
const { theme, isDark, toggleTheme } = useTheme()
|
||||
const { locale, toggleLocale } = useLocale()
|
||||
const { locale, toggleLocale, t } = useLocale()
|
||||
const [isSupportModalOpen, setIsSupportModalOpen] = useState(false)
|
||||
|
||||
const getThemeIcon = () => {
|
||||
switch (theme) {
|
||||
@@ -32,7 +35,8 @@ export function Header() {
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<>
|
||||
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container flex h-14 items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
||||
@@ -44,6 +48,19 @@ export function Header() {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
{/* Support Button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setIsSupportModalOpen(true)}
|
||||
className="w-full justify-start text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950/20"
|
||||
>
|
||||
<Heart className="h-4 w-4" />
|
||||
<span className="ml-2 hidden sm:inline">
|
||||
{t('support.title')}
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
{/* Theme Toggle */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -72,5 +89,12 @@ export function Header() {
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Support Modal */}
|
||||
<SupportModal
|
||||
isOpen={isSupportModalOpen}
|
||||
onClose={() => setIsSupportModalOpen(false)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
+112
-130
@@ -5,77 +5,52 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { apiClient } from '@/lib/api/client'
|
||||
import { useLocale } from '@/contexts/LocaleContext'
|
||||
import { Package, FilterOptions, Platform } from '@/types'
|
||||
import { Search, Package as PackageIcon, Terminal, Monitor } from 'lucide-react'
|
||||
import { PackageIcon, Search, Monitor, Terminal } from 'lucide-react'
|
||||
|
||||
interface PackageBrowserProps {
|
||||
selectedPlatform: Platform | null
|
||||
selectedPackages: Package[]
|
||||
onPackageToggle: (pkg: Package) => void
|
||||
onFiltersChange: (filters: FilterOptions) => void
|
||||
interface Package {
|
||||
id: string
|
||||
name: string
|
||||
version: string
|
||||
description: string
|
||||
category: string
|
||||
type: 'gui' | 'cli'
|
||||
repository: 'official' | 'third-party'
|
||||
license: string
|
||||
tags: string[]
|
||||
popularity?: number
|
||||
}
|
||||
|
||||
export function PackageBrowser({
|
||||
selectedPlatform,
|
||||
selectedPackages,
|
||||
interface PackageBrowserProps {
|
||||
selectedPlatform: { id: string; name: string } | null
|
||||
packages: Package[]
|
||||
selectedPackages: Package[]
|
||||
onPackageToggle: (pkg: Package) => void
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
export function PackageBrowser({
|
||||
selectedPlatform,
|
||||
packages,
|
||||
selectedPackages,
|
||||
onPackageToggle,
|
||||
onFiltersChange
|
||||
loading = false
|
||||
}: PackageBrowserProps) {
|
||||
const { t } = useLocale()
|
||||
const [packages, setPackages] = useState<Package[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [filters, setFilters] = useState<FilterOptions>({
|
||||
platform_id: selectedPlatform?.id || '',
|
||||
type: '',
|
||||
repository: '',
|
||||
search: '',
|
||||
limit: 50,
|
||||
offset: 0
|
||||
const [filters, setFilters] = useState({
|
||||
categories: [] as string[],
|
||||
types: [] as ('gui' | 'cli')[],
|
||||
repositories: [] as ('official' | 'third-party')[]
|
||||
})
|
||||
|
||||
// Load packages when platform changes
|
||||
useEffect(() => {
|
||||
const loadPackages = async () => {
|
||||
if (!selectedPlatform) {
|
||||
setPackages([])
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await apiClient.getPackages({
|
||||
platform_id: selectedPlatform.id,
|
||||
search: searchQuery,
|
||||
type: filters.type || undefined,
|
||||
repository: filters.repository || undefined,
|
||||
limit: filters.limit,
|
||||
offset: filters.offset
|
||||
})
|
||||
setPackages(result.packages)
|
||||
} catch (error) {
|
||||
console.error('Failed to load packages:', error)
|
||||
// Fallback to mock data if API fails
|
||||
const { mockPackages } = await import('@/data/mockData')
|
||||
setPackages(mockPackages.filter(pkg => pkg.platform === selectedPlatform.id))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
loadPackages()
|
||||
}, [selectedPlatform, searchQuery, filters.type, filters.repository])
|
||||
const categories = useMemo(() => {
|
||||
const cats = new Set(packages.map(pkg => pkg.category))
|
||||
return Array.from(cats).sort()
|
||||
}, [packages])
|
||||
|
||||
const filteredPackages = useMemo(() => {
|
||||
return packages.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())) {
|
||||
@@ -87,11 +62,6 @@ export function PackageBrowser({
|
||||
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
|
||||
@@ -104,12 +74,13 @@ export function PackageBrowser({
|
||||
|
||||
return true
|
||||
})
|
||||
}, [selectedPlatform, searchQuery, filters])
|
||||
}, [packages, searchQuery, filters])
|
||||
|
||||
const handleFilterChange = (key: keyof FilterOptions, value: any) => {
|
||||
const newFilters = { ...filters, [key]: value }
|
||||
setFilters(newFilters)
|
||||
onFiltersChange(newFilters)
|
||||
const handleFilterChange = (filterType: keyof typeof filters, values: string[]) => {
|
||||
setFilters(prev => ({
|
||||
...prev,
|
||||
[filterType]: values
|
||||
}))
|
||||
}
|
||||
|
||||
const isPackageSelected = (pkg: Package) => {
|
||||
@@ -126,64 +97,20 @@ export function PackageBrowser({
|
||||
)
|
||||
}
|
||||
|
||||
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={t('packages.search')}
|
||||
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={t('packages.filters.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={t('packages.filters.type')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="gui">{t('packages.filters.gui')}</SelectItem>
|
||||
<SelectItem value="cli">{t('packages.filters.cli')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Repository Filter */}
|
||||
<Select onValueChange={(value) =>
|
||||
handleFilterChange('repositories', value ? [value as 'official' | 'third-party'] : [])
|
||||
}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t('packages.filters.repository')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
if (loading) {
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardHeader className="pb-4">
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<PackageIcon className="h-5 w-5" />
|
||||
{t('packages.title')}
|
||||
{selectedPlatform && (
|
||||
<span className="text-sm font-normal text-muted-foreground">
|
||||
({packages.length} packages for {selectedPlatform.name})
|
||||
</span>
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-sm">
|
||||
{t('packages.description')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
@@ -230,10 +157,65 @@ export function PackageBrowser({
|
||||
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={t('packages.filters.category')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map((category: string) => (
|
||||
<SelectItem key={category} value={category}>
|
||||
{category}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Type Filter */}
|
||||
<Select onValueChange={(value) =>
|
||||
handleFilterChange('types', value ? [value as 'gui' | 'cli'] : [])
|
||||
}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t('packages.filters.type')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="gui">{t('packages.filters.gui')}</SelectItem>
|
||||
<SelectItem value="cli">{t('packages.filters.cli')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Repository Filter */}
|
||||
<Select onValueChange={(value) =>
|
||||
handleFilterChange('repositories', value ? [value as 'official' | 'third-party'] : [])
|
||||
}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t('packages.filters.repository')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="official">{t('packages.filters.official')}</SelectItem>
|
||||
<SelectItem value="third-party">{t('packages.filters.third_party')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Package List */}
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||
{filteredPackages.map(pkg => (
|
||||
<div
|
||||
key={pkg.id}
|
||||
className={`p-3 border rounded-lg cursor-pointer transition-colors ${
|
||||
isPackageSelected(pkg)
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-border hover:bg-secondary/50'
|
||||
}`}
|
||||
onClick={() => onPackageToggle(pkg)}
|
||||
>
|
||||
<div className="flex items-start space-x-3">
|
||||
<Checkbox
|
||||
@@ -264,7 +246,7 @@ export function PackageBrowser({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{pkg.tags.map(tag => (
|
||||
{(pkg.tags || []).map(tag => (
|
||||
<span
|
||||
key={tag}
|
||||
className="text-xs bg-secondary text-secondary-foreground px-2 py-1 rounded"
|
||||
@@ -284,8 +266,8 @@ export function PackageBrowser({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { useLocale } from '@/contexts/LocaleContext'
|
||||
import { Loader2, Heart, AlertCircle, CheckCircle } from 'lucide-react'
|
||||
|
||||
interface SupportModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
interface PaymentData {
|
||||
payment_url: string
|
||||
payment_id: string
|
||||
}
|
||||
|
||||
export function SupportModal({ isOpen, onClose }: SupportModalProps) {
|
||||
const [amount, setAmount] = useState('')
|
||||
const [currency, setCurrency] = useState('USDT')
|
||||
const [email, setEmail] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [payment, setPayment] = useState<PaymentData | null>(null)
|
||||
const [error, setError] = useState('')
|
||||
const { t } = useLocale()
|
||||
|
||||
const cryptoOptions = [
|
||||
{ value: 'USDT', label: 'USDT (TRC20)', network: 'TRC20' },
|
||||
{ value: 'USDC', label: 'USDC (TRC20)', network: 'TRC20' },
|
||||
{ value: 'BTC', label: 'Bitcoin', network: 'BTC' },
|
||||
{ value: 'ETH', label: 'Ethereum', network: 'ETH' },
|
||||
{ value: 'LTC', label: 'Litecoin', network: 'LTC' },
|
||||
{ value: 'TRX', label: 'TRON', network: 'TRC20' }
|
||||
]
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError('')
|
||||
setPayment(null)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/support/create-payment', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
amount: parseFloat(amount),
|
||||
currency,
|
||||
order_id: `support_${Date.now()}`,
|
||||
description: t('support.description'),
|
||||
email: email || undefined
|
||||
})
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Payment creation failed')
|
||||
}
|
||||
|
||||
setPayment(data)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
setAmount('')
|
||||
setCurrency('USDT')
|
||||
setEmail('')
|
||||
setPayment(null)
|
||||
setError('')
|
||||
}
|
||||
|
||||
const handleClose = (e?: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e?.preventDefault()
|
||||
resetForm()
|
||||
onClose()
|
||||
}
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Heart className="h-5 w-5 text-red-500" />
|
||||
<span>{t('support.title')}</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t('support.subtitle')}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={handleClose}>
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{!payment ? (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="amount">{t('support.amount')}</Label>
|
||||
<Input
|
||||
id="amount"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="1"
|
||||
placeholder="10.00"
|
||||
value={amount}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setAmount(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="currency">{t('support.currency')}</Label>
|
||||
<Select value={currency} onValueChange={setCurrency}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{cryptoOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">{t('support.email_optional')}</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="[email protected]"
|
||||
value={email}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="flex items-center space-x-2 text-red-600 bg-red-50 p-3 rounded-lg">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<span className="text-sm">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={loading || !amount}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{t('support.creating')}
|
||||
</>
|
||||
) : (
|
||||
t('support.create_payment')
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2 text-green-600 bg-green-50 p-3 rounded-lg">
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
<span className="text-sm">{t('support.payment_created')}</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{t('support.payment_id')}</Label>
|
||||
<div className="p-2 bg-muted rounded font-mono text-sm">
|
||||
{payment.payment_id}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{t('support.amount_to_pay')}</Label>
|
||||
<div className="p-2 bg-muted rounded text-sm">
|
||||
{amount} {currency}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => window.open(payment.payment_url, '_blank')}
|
||||
className="w-full"
|
||||
>
|
||||
{t('support.pay_now')}
|
||||
</Button>
|
||||
|
||||
<div className="text-xs text-muted-foreground text-center">
|
||||
{t('support.payment_note')}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={resetForm}
|
||||
className="w-full"
|
||||
>
|
||||
{t('support.create_another')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-xs text-muted-foreground space-y-1">
|
||||
<p>{t('support.secure_payment')}</p>
|
||||
<p>{t('support.thank_you')}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
Reference in New Issue
Block a user