feat (v1.0.0): initial refactor and redesign

This commit is contained in:
Aidan 2025-10-09 04:12:05 -04:00
parent 3058aa1ab4
commit fe9b50b30e
134 changed files with 17792 additions and 3670 deletions

View file

@ -0,0 +1,98 @@
'use client'
import {
getExpirationDate,
getDaysUntilExpiration,
getOwnershipDuration,
getOwnershipMonths,
isExpiringSoon,
formatDate,
getNextRenewalDate
} from '@/lib/domains/utils'
import Link from 'next/link'
import {
Calendar,
Clock,
ChevronRight,
RefreshCw
} from 'lucide-react'
import type { DomainCardProps } from '@/lib/types'
import { domainVisualConfig } from '@/lib/domains/config'
export default function DomainCard({ domain }: DomainCardProps) {
const expirationDate = getExpirationDate(domain)
const nextRenewalDate = getNextRenewalDate(domain)
const daysUntilExpiration = getDaysUntilExpiration(domain)
const ownershipYears = getOwnershipDuration(domain)
const ownershipMonths = getOwnershipMonths(domain)
const expiringSoon = isExpiringSoon(domain)
const statusVisual = domainVisualConfig.status[domain.status]
const categoryVisual = domainVisualConfig.category[domain.category]
const StatusIcon = statusVisual.icon
return (
<Link href={`/domains/${domain.domain}`}>
<div className="group relative h-full bg-gray-900/50 backdrop-blur-sm border border-gray-800 rounded-xl hover:border-gray-700 transition-all hover:shadow-xl hover:shadow-black/20 cursor-pointer overflow-hidden flex flex-col">
{expiringSoon && (
<div className="absolute top-0 left-0 right-0 h-1 bg-gray-500"></div>
)}
<div className="p-6 flex flex-col flex-1">
<div className="flex justify-between items-start mb-3">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className={`${statusVisual.color}`}>
<StatusIcon className="w-4 h-4" />
</span>
<h3 className="text-lg font-semibold text-gray-100 group-hover:text-white transition-colors">
{domain.domain}
</h3>
</div>
<p className="text-sm text-gray-500 line-clamp-2 min-h-[2.5rem]">{domain.usage}</p>
</div>
<ChevronRight className="w-5 h-5 text-gray-600 group-hover:text-gray-400 transition-all group-hover:translate-x-1" />
</div>
<div className="flex items-center gap-4 text-xs text-gray-400 mb-3">
<span className={`${categoryVisual.color} font-medium uppercase tracking-wide`}>
{categoryVisual.label}
</span>
<span className="text-gray-600"></span>
<span>{domain.registrar}</span>
{domain.autoRenew && (
<>
<span className="text-gray-600"></span>
<span className="text-slate-500/80">Auto-renew</span>
</>
)}
</div>
<div className="flex flex-col gap-2 pt-3 border-t border-gray-800/50 mt-auto">
<div className="flex items-center justify-between text-xs">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1.5">
<Clock className="w-3.5 h-3.5 text-gray-500" />
<span className="text-gray-400">
{ownershipYears < 1 ? `${ownershipMonths}mo owned` : `${ownershipYears}y owned`}
</span>
</div>
<div className="flex items-center gap-1.5">
<Calendar className="w-3.5 h-3.5 text-gray-500" />
<span className={expiringSoon ? 'text-gray-300 font-medium' : 'text-gray-400'}>
{expiringSoon ? `${daysUntilExpiration}d left` : formatDate(expirationDate)}
</span>
</div>
</div>
</div>
<div className="flex items-center gap-1.5 text-xs">
<RefreshCw className="w-3.5 h-3.5 text-gray-500" />
<span className="text-gray-400">
Next renewal: {formatDate(nextRenewalDate)}
</span>
</div>
</div>
</div>
</div>
</Link>
)
}

View file

@ -0,0 +1,180 @@
import {
getRegistrationDate,
getExpirationDate,
getDaysUntilExpiration,
getOwnershipDuration,
getOwnershipMonths,
formatDate,
isExpiringSoon,
getRenewalProgress,
getOwnershipDays
} from '@/lib/domains/utils'
import { registrars } from '@/lib/domains/data'
import { domainVisualConfig } from '@/lib/domains/config'
import {
Shield,
Tag,
AlertCircle,
ToggleLeft,
ToggleRight,
Activity
} from 'lucide-react'
import type { DomainDetailsProps } from '@/lib/types'
export default function DomainDetails({ domain }: DomainDetailsProps) {
const registrationDate = getRegistrationDate(domain)
const expirationDate = getExpirationDate(domain)
const daysUntilExpiration = getDaysUntilExpiration(domain)
const ownershipYears = getOwnershipDuration(domain)
const ownershipMonths = getOwnershipMonths(domain)
const ownershipDays = getOwnershipDays(domain)
const expiringSoon = isExpiringSoon(domain)
const renewalProgress = getRenewalProgress(domain)
const registrarConfig = registrars[domain.registrar]
const statusVisual = domainVisualConfig.status[domain.status]
const categoryVisual = domainVisualConfig.category[domain.category]
const StatusIcon = statusVisual.icon
const CategoryIcon = categoryVisual.icon
return (
<div className="space-y-4">
<div className="bg-gray-900/50 backdrop-blur-sm border border-gray-800 rounded-xl p-6">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-xs text-gray-500 uppercase tracking-wide mb-2">Status</p>
<div className={`inline-flex items-center gap-2 px-3 py-2 rounded-lg ${statusVisual.bg} ${statusVisual.border} border`}>
<span className={statusVisual.color}>
<StatusIcon className="w-5 h-5" />
</span>
<span className={`font-medium ${statusVisual.color}`}>
{statusVisual.label}
</span>
</div>
</div>
<div>
<p className="text-xs text-gray-500 uppercase tracking-wide mb-2">Category</p>
<div className={`inline-flex items-center gap-2 px-3 py-2 rounded-lg ${categoryVisual.bg} ${categoryVisual.border} border`}>
<span className={categoryVisual.color}>
<CategoryIcon className="w-5 h-5" />
</span>
<span className={`font-medium ${categoryVisual.color}`}>
{categoryVisual.label}
</span>
</div>
</div>
</div>
</div>
<div className="bg-gray-900/50 backdrop-blur-sm border border-gray-800 rounded-xl p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-medium text-gray-300">Domain Lifecycle</h3>
<div className="flex items-center gap-3 text-xs text-gray-500">
Owned for {ownershipDays} days ({ownershipYears < 1 ? `${ownershipMonths} months` : `${ownershipYears} years`})
</div>
</div>
<div className="relative mb-4">
<div className="flex justify-between text-xs text-gray-500 mb-2">
<span>Registered</span>
<span>Expires</span>
</div>
<div className="relative h-4 bg-gray-800 rounded-full overflow-hidden">
<div
className="absolute left-0 top-0 h-full bg-slate-500 rounded-full transition-all duration-500"
style={{ width: `${renewalProgress}%` }}
/>
{expiringSoon && (
<div className="absolute right-0 top-0 h-full w-24 bg-gray-600/30" />
)}
</div>
<div className="flex justify-between text-xs mt-2">
<span className="text-gray-400">{formatDate(registrationDate)}</span>
<span className={`font-medium ${
expiringSoon ? 'text-gray-300' : renewalProgress > 75 ? 'text-slate-400' : 'text-gray-400'
}`}>
{formatDate(expirationDate)}
</span>
</div>
</div>
<div className="grid grid-cols-2 gap-2 text-center">
<div className="p-2 bg-gray-800/50 rounded-lg">
<div className="text-lg font-bold text-slate-400">{Math.floor(renewalProgress)}%</div>
<div className="text-xs text-gray-500">Period Used</div>
</div>
<div className={`p-2 rounded-lg ${expiringSoon ? 'bg-gray-800/70' : 'bg-gray-800/50'}`}>
<div className={`text-lg font-bold ${expiringSoon ? 'text-gray-300' : 'text-slate-400'}`}>
{daysUntilExpiration}
</div>
<div className="text-xs text-gray-500">Days Left</div>
</div>
</div>
{expiringSoon && (
<div className="flex items-center gap-2 p-3 mt-3 bg-gray-800/50 border border-gray-700 rounded-lg">
<AlertCircle className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-400">
Domain expires soon
</span>
</div>
)}
</div>
<div className="bg-gray-900/50 backdrop-blur-sm border border-gray-800 rounded-xl p-6">
<div className="grid grid-cols-2 gap-6">
<div>
<div className="flex items-center gap-2 mb-3">
<Shield className="w-4 h-4 text-gray-500" />
<p className="text-xs text-gray-500 uppercase tracking-wide">Registrar</p>
</div>
<div className="flex items-center gap-2">
{registrarConfig && (
<div className={`w-8 h-8 bg-gray-800 rounded-lg flex items-center justify-center ${registrarConfig.color}`}>
<registrarConfig.icon className="w-4 h-4" />
</div>
)}
<span className="text-gray-200 font-medium">{domain.registrar}</span>
</div>
</div>
<div>
<div className="flex items-center gap-2 mb-3">
<Activity className="w-4 h-4 text-gray-500" />
<p className="text-xs text-gray-500 uppercase tracking-wide">Auto-Renewal</p>
</div>
<button className="flex items-center gap-3 px-3 py-2 rounded-lg bg-gray-800 border border-gray-700 cursor-default">
{domain.autoRenew ? (
<>
<ToggleRight className="w-5 h-5 text-slate-400" />
<span className="text-sm text-slate-400 font-medium">Enabled</span>
</>
) : (
<>
<ToggleLeft className="w-5 h-5 text-gray-500" />
<span className="text-sm text-gray-500 font-medium">Disabled</span>
</>
)}
</button>
</div>
</div>
</div>
<div className="bg-gray-900/50 backdrop-blur-sm border border-gray-800 rounded-xl p-6">
<div className="flex items-center gap-2 mb-4">
<Tag className="w-4 h-4 text-gray-500" />
<h3 className="text-sm font-medium text-gray-300">Tags</h3>
</div>
<div className="flex flex-wrap gap-2">
{domain.tags.map(tag => (
<span
key={tag}
className="px-3 py-1.5 bg-gray-800/50 text-gray-300 rounded-full text-sm hover:bg-gray-800 transition-colors border border-gray-700/50"
>
#{tag}
</span>
))}
</div>
</div>
</div>
)
}

View file

@ -0,0 +1,198 @@
'use client'
import { useState } from 'react'
import { Search, Filter, X } from 'lucide-react'
import type {
DomainFiltersProps,
DomainCategory,
DomainStatus,
DomainRegistrarId,
DomainSortOption
} from '@/lib/types'
import { sortOptions } from '@/lib/domains/config'
export default function DomainFilters({
onSearchChange,
onCategoryChange,
onStatusChange,
onRegistrarChange,
onSortChange,
registrars
}: DomainFiltersProps) {
const [search, setSearch] = useState('')
const [selectedCategories, setSelectedCategories] = useState<DomainCategory[]>([])
const [selectedStatuses, setSelectedStatuses] = useState<DomainStatus[]>([])
const [selectedRegistrars, setSelectedRegistrars] = useState<DomainRegistrarId[]>([])
const [sortBy, setSortBy] = useState<DomainSortOption>('name')
const [showFilters, setShowFilters] = useState(false)
const categories: DomainCategory[] = ['personal', 'service', 'project', 'fun', 'legacy']
const statuses: DomainStatus[] = ['active', 'parked', 'reserved']
const handleSearchChange = (value: string) => {
setSearch(value)
onSearchChange(value)
}
const toggleCategory = (category: DomainCategory) => {
const updated = selectedCategories.includes(category)
? selectedCategories.filter(c => c !== category)
: [...selectedCategories, category]
setSelectedCategories(updated)
onCategoryChange(updated)
}
const toggleStatus = (status: DomainStatus) => {
const updated = selectedStatuses.includes(status)
? selectedStatuses.filter(s => s !== status)
: [...selectedStatuses, status]
setSelectedStatuses(updated)
onStatusChange(updated)
}
const toggleRegistrar = (registrar: DomainRegistrarId) => {
const updated = selectedRegistrars.includes(registrar)
? selectedRegistrars.filter(r => r !== registrar)
: [...selectedRegistrars, registrar]
setSelectedRegistrars(updated)
onRegistrarChange(updated)
}
const handleSortChange = (value: DomainSortOption) => {
setSortBy(value)
onSortChange(value)
}
const clearFilters = () => {
setSearch('')
setSelectedCategories([])
setSelectedStatuses([])
setSelectedRegistrars([])
setSortBy('name')
onSearchChange('')
onCategoryChange([])
onStatusChange([])
onRegistrarChange([])
onSortChange('name')
}
const hasActiveFilters = search || selectedCategories.length > 0 || selectedStatuses.length > 0 || selectedRegistrars.length > 0
return (
<div className="mb-8 space-y-4">
<div className="flex gap-4 items-center">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 w-5 h-5" />
<input
type="text"
value={search}
onChange={(e) => handleSearchChange(e.target.value)}
placeholder="Search domains..."
className="w-full pl-10 pr-4 py-2 bg-gray-900/50 border border-gray-800 rounded-lg text-gray-200 placeholder-gray-500 focus:outline-none focus:border-gray-700"
/>
</div>
<button
onClick={() => setShowFilters(!showFilters)}
className={`flex items-center gap-2 px-4 py-2 rounded-lg border transition-colors ${
showFilters || hasActiveFilters
? 'bg-gray-800 border-gray-700 text-white'
: 'bg-gray-900/50 border-gray-800 text-gray-400 hover:border-gray-700 hover:text-gray-300'
}`}
>
<Filter className="w-5 h-5" />
Filters
{hasActiveFilters && (
<span className="ml-1 px-2 py-0.5 text-xs bg-slate-500/20 text-slate-400 rounded-full">
Active
</span>
)}
</button>
<select
value={sortBy}
onChange={(e) => handleSortChange(e.target.value as DomainSortOption)}
className="px-4 py-2 bg-gray-900/50 border border-gray-800 rounded-lg text-gray-200 focus:outline-none focus:border-gray-700"
>
{sortOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
{showFilters && (
<div className="p-4 bg-gray-900/30 border border-gray-800 rounded-lg space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-sm font-medium text-gray-300">Filter Options</h3>
{hasActiveFilters && (
<button
onClick={clearFilters}
className="text-xs text-gray-500 hover:text-gray-400 flex items-center gap-1"
>
<X className="w-3 h-3" />
Clear all
</button>
)}
</div>
<div>
<h4 className="text-xs text-gray-500 mb-2">Category</h4>
<div className="flex flex-wrap gap-2">
{categories.map(category => (
<button
key={category}
onClick={() => toggleCategory(category)}
className={`px-3 py-1 text-sm rounded-full border transition-colors ${
selectedCategories.includes(category)
? 'bg-slate-500/20 text-slate-400 border-slate-500/40'
: 'bg-gray-800/50 text-gray-400 border-gray-700 hover:border-gray-600'
}`}
>
{category}
</button>
))}
</div>
</div>
<div>
<h4 className="text-xs text-gray-500 mb-2">Status</h4>
<div className="flex flex-wrap gap-2">
{statuses.map(status => (
<button
key={status}
onClick={() => toggleStatus(status)}
className={`px-3 py-1 text-sm rounded-full border transition-colors ${
selectedStatuses.includes(status)
? 'bg-slate-500/20 text-slate-400 border-slate-500/40'
: 'bg-gray-800/50 text-gray-400 border-gray-700 hover:border-gray-600'
}`}
>
{status}
</button>
))}
</div>
</div>
<div>
<h4 className="text-xs text-gray-500 mb-2">Registrar</h4>
<div className="flex flex-wrap gap-2">
{registrars.map(registrar => (
<button
key={registrar}
onClick={() => toggleRegistrar(registrar)}
className={`px-3 py-1 text-sm rounded-full border transition-colors ${
selectedRegistrars.includes(registrar)
? 'bg-slate-500/20 text-slate-400 border-slate-500/40'
: 'bg-gray-800/50 text-gray-400 border-gray-700 hover:border-gray-600'
}`}
>
{registrar}
</button>
))}
</div>
</div>
</div>
)}
</div>
)
}

View file

@ -0,0 +1,88 @@
import { getRenewalTimeline, formatDate, getNextRenewalDate } from '@/lib/domains/utils'
import { Calendar, RefreshCw, Star } from 'lucide-react'
import type { DomainTimelineProps } from '@/lib/types'
export default function DomainTimeline({ domain }: DomainTimelineProps) {
const timeline = getRenewalTimeline(domain)
const nextRenewalDate = getNextRenewalDate(domain)
return (
<div className="bg-gray-900/50 backdrop-blur-sm border border-gray-800 rounded-xl p-6">
<div className="relative">
<div className="absolute left-6 top-8 bottom-0 w-0.5 bg-gray-700"></div>
<div className="space-y-8">
{timeline.map((event, index) => {
const isLatest = index === timeline.length - 1
const isRegistration = event.type === 'registration'
return (
<div key={index} className="relative flex items-start gap-4">
<div className={`relative z-10 flex items-center justify-center w-12 h-12 rounded-full ${
isRegistration || isLatest
? 'bg-gray-800 border-2 border-slate-400/50'
: 'bg-gray-800 border-2 border-gray-700'
}`}>
{isRegistration ? (
<Star className="w-6 h-6 text-slate-300" />
) : (
<RefreshCw className={`w-5 h-5 ${isLatest ? 'text-slate-300' : 'text-gray-500'}`} />
)}
</div>
<div className="flex-1 pb-8">
<div className={`rounded-lg p-4 border transition-colors ${
isRegistration || isLatest
? 'bg-gray-800/50 border-gray-700/50 hover:border-gray-600/50'
: 'bg-slate-400/5 border-slate-400/20 hover:border-slate-400/30'
}`}>
<div className="flex items-center justify-between mb-2">
<span className={`text-sm font-medium ${
isRegistration || isLatest ? 'text-slate-300' : 'text-gray-400'
}`}>
{isRegistration ? 'Domain Registered' : 'Domain Renewed'}
</span>
<div className="flex items-center gap-2 text-xs text-gray-500">
<Calendar className="w-3 h-3" />
{formatDate(event.date)}
</div>
</div>
<div className="text-sm text-gray-300">
{isRegistration ? (
<span>Initial registration for {event.years} {event.years === 1 ? 'year' : 'years'}</span>
) : (
<span>Renewed for {event.years} {event.years === 1 ? 'year' : 'years'}</span>
)}
</div>
</div>
</div>
</div>
)
})}
<div className="relative flex items-start gap-4">
<div className="relative z-10 flex items-center justify-center w-12 h-12 rounded-full bg-gray-900 border-2 border-dashed border-gray-700">
<Calendar className="w-5 h-5 text-gray-600" />
</div>
<div className="flex-1">
<div className="bg-gray-900/30 rounded-lg p-4 border border-dashed border-gray-700/50">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-500">
Next Renewal
</span>
<div className="flex items-center gap-2 text-xs text-gray-500">
<Calendar className="w-3 h-3" />
{formatDate(nextRenewalDate)}
</div>
</div>
<div className="text-xs text-gray-600">
{domain.autoRenew ? 'Auto-renewal enabled' : 'Manual renewal required'}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)
}