feat (v1.0.0): initial refactor and redesign
This commit is contained in:
parent
3058aa1ab4
commit
fe9b50b30e
134 changed files with 17792 additions and 3670 deletions
49
app/domains/[domain]/page.tsx
Normal file
49
app/domains/[domain]/page.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { notFound } from 'next/navigation'
|
||||
import DomainTimeline from '@/components/domains/DomainTimeline'
|
||||
import DomainDetails from '@/components/domains/DomainDetails'
|
||||
import { ArrowLeft, Globe } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { domains } from '@/lib/domains/data'
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return domains.map((domain) => ({
|
||||
domain: domain.domain,
|
||||
}))
|
||||
}
|
||||
|
||||
export default async function DomainPage({ params }: { params: Promise<{ domain: string }> }) {
|
||||
const { domain: domainParam } = await params
|
||||
const domain = domains.find(d => d.domain === domainParam)
|
||||
|
||||
if (!domain) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grow container mx-auto px-4 py-12">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<Link href="/domains" className="inline-flex items-center gap-2 text-gray-400 hover:text-gray-300 mb-8 transition-colors">
|
||||
<ArrowLeft />
|
||||
Back to Domains
|
||||
</Link>
|
||||
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<Globe className="w-10 h-10 text-gray-400" />
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold text-gray-200 glow">
|
||||
{domain.domain}
|
||||
</h1>
|
||||
<p className="text-gray-400 mt-1">{domain.usage}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<DomainDetails domain={domain} />
|
||||
<DomainTimeline domain={domain} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,45 +1,144 @@
|
|||
import Header from '@/components/Header'
|
||||
import Footer from '@/components/Footer'
|
||||
import { Link } from "lucide-react"
|
||||
import { TbCurrencyDollarOff } from "react-icons/tb";
|
||||
import domains from "@/public/data/domains.json"
|
||||
'use client'
|
||||
|
||||
import { useState, useMemo } from 'react'
|
||||
import DomainCard from '@/components/domains/DomainCard'
|
||||
import DomainFilters from '@/components/domains/DomainFilters'
|
||||
import PageHeader from '@/components/objects/PageHeader'
|
||||
import { Link, AlertCircle } from "lucide-react"
|
||||
import { TbCurrencyDollarOff } from "react-icons/tb"
|
||||
import { domains } from "@/lib/domains/data"
|
||||
import { getDaysUntilExpiration, getOwnershipDuration, getOwnershipMonths } from '@/lib/domains/utils'
|
||||
import type {
|
||||
DomainCategory,
|
||||
DomainStatus,
|
||||
DomainRegistrarId,
|
||||
DomainSortOption
|
||||
} from '@/lib/types'
|
||||
|
||||
export default function Domains() {
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [selectedCategories, setSelectedCategories] = useState<DomainCategory[]>([])
|
||||
const [selectedStatuses, setSelectedStatuses] = useState<DomainStatus[]>([])
|
||||
const [selectedRegistrars, setSelectedRegistrars] = useState<DomainRegistrarId[]>([])
|
||||
const [sortBy, setSortBy] = useState<DomainSortOption>('name')
|
||||
|
||||
const uniqueRegistrars = useMemo<DomainRegistrarId[]>(() => {
|
||||
return Array.from(new Set(domains.map(d => d.registrar))).sort()
|
||||
}, [])
|
||||
|
||||
const filteredAndSortedDomains = useMemo(() => {
|
||||
const filtered = domains.filter(domain => {
|
||||
const matchesSearch = searchQuery === '' ||
|
||||
domain.domain.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
domain.usage.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
domain.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
|
||||
const matchesCategory = selectedCategories.length === 0 ||
|
||||
selectedCategories.includes(domain.category)
|
||||
|
||||
const matchesStatus = selectedStatuses.length === 0 ||
|
||||
selectedStatuses.includes(domain.status)
|
||||
|
||||
const matchesRegistrar = selectedRegistrars.length === 0 ||
|
||||
selectedRegistrars.includes(domain.registrar)
|
||||
|
||||
return matchesSearch && matchesCategory && matchesStatus && matchesRegistrar
|
||||
})
|
||||
|
||||
filtered.sort((a, b) => {
|
||||
switch (sortBy) {
|
||||
case 'name':
|
||||
return a.domain.localeCompare(b.domain)
|
||||
case 'expiration':
|
||||
return getDaysUntilExpiration(a) - getDaysUntilExpiration(b)
|
||||
case 'ownership':
|
||||
return getOwnershipDuration(b) - getOwnershipDuration(a)
|
||||
case 'registrar':
|
||||
return a.registrar.localeCompare(b.registrar)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
})
|
||||
|
||||
return filtered
|
||||
}, [searchQuery, selectedCategories, selectedStatuses, selectedRegistrars, sortBy])
|
||||
|
||||
const stats = useMemo(() => {
|
||||
const expiringSoon = domains.filter(d => getDaysUntilExpiration(d) <= 90).length
|
||||
const totalDomains = domains.length
|
||||
const activeDomains = domains.filter(d => d.status === 'active').length
|
||||
const avgOwnershipYears = domains.reduce((acc, d) => acc + getOwnershipDuration(d), 0) / domains.length
|
||||
const avgOwnershipMonths = domains.reduce((acc, d) => acc + getOwnershipMonths(d), 0) / domains.length
|
||||
|
||||
return { expiringSoon, totalDomains, activeDomains, avgOwnershipYears, avgOwnershipMonths }
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
<main className="grow container mx-auto px-4 py-12">
|
||||
<div className="max-w-2xl mx-auto flex flex-col items-center text-center">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex justify-center">
|
||||
<Link size={60} />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mt-2 text-center text-gray-200" style={{ textShadow: '0 0 10px rgba(255, 255, 255, 0.5)' }}>
|
||||
My Domains
|
||||
</h1>
|
||||
</div>
|
||||
<div className="grow container mx-auto px-4 py-12">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex flex-col items-center text-center mb-8">
|
||||
<PageHeader
|
||||
icon={<Link size={60} />}
|
||||
title="My Domain Portfolio"
|
||||
/>
|
||||
<div className="mb-4 p-4 pt-8 flex flex-col items-center space-y-2">
|
||||
<TbCurrencyDollarOff size={26} className="text-red-500" />
|
||||
<span className="text-red-500 font-medium text-center mt-1 mb-0">
|
||||
<TbCurrencyDollarOff size={26} className="text-gray-500" />
|
||||
<span className="text-gray-400 font-medium text-center mt-1 mb-0">
|
||||
These domains are not for sale.
|
||||
</span>
|
||||
<span className="text-red-500 font-medium text-center">
|
||||
<span className="text-gray-400 font-medium text-center">
|
||||
All requests to buy them will be declined.
|
||||
</span>
|
||||
</div>
|
||||
<div className="p-6 pt-0 w-full">
|
||||
{domains.map(domain => (
|
||||
<div key={domain.id} className="mb-4">
|
||||
<h2 className="text-2xl font-semibold text-gray-200">
|
||||
{domain.domain}
|
||||
</h2>
|
||||
<p className="text-gray-300">{domain.usage}</p>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 w-full max-w-3xl mb-8">
|
||||
<div className="bg-gray-900/50 backdrop-blur-sm border border-gray-800 rounded-lg p-4">
|
||||
<div className="text-2xl font-bold text-gray-300">{stats.totalDomains}</div>
|
||||
<div className="text-sm text-gray-500">Total Domains</div>
|
||||
</div>
|
||||
<div className="bg-gray-900/50 backdrop-blur-sm border border-gray-800 rounded-lg p-4">
|
||||
<div className="text-2xl font-bold text-gray-300">{stats.activeDomains}</div>
|
||||
<div className="text-sm text-gray-500">Active</div>
|
||||
</div>
|
||||
<div className="bg-gray-900/50 backdrop-blur-sm border border-gray-800 rounded-lg p-4">
|
||||
<div className="text-2xl font-bold text-gray-300 flex items-center justify-center gap-1">
|
||||
{stats.expiringSoon > 0 && <AlertCircle className="text-orange-500" />}
|
||||
{stats.expiringSoon}
|
||||
</div>
|
||||
))}
|
||||
<div className="text-sm text-gray-500">Expiring Soon</div>
|
||||
</div>
|
||||
<div className="bg-gray-900/50 backdrop-blur-sm border border-gray-800 rounded-lg p-4">
|
||||
<div className="text-2xl font-bold text-gray-300 flex items-center justify-center gap-1">
|
||||
{stats.avgOwnershipYears < 1
|
||||
? `${Math.round(stats.avgOwnershipMonths)} mo`
|
||||
: `${stats.avgOwnershipYears.toFixed(1)} yr`}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">Avg Time Owned</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
<DomainFilters
|
||||
onSearchChange={setSearchQuery}
|
||||
onCategoryChange={setSelectedCategories}
|
||||
onStatusChange={setSelectedStatuses}
|
||||
onRegistrarChange={setSelectedRegistrars}
|
||||
onSortChange={setSortBy}
|
||||
registrars={uniqueRegistrars}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{filteredAndSortedDomains.map(domain => (
|
||||
<DomainCard key={domain.domain} domain={domain} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredAndSortedDomains.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-500">No domains match your filters</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue