← back to Wine Finder Next
app/membership/page-improved.tsx
642 lines
'use client';
import { useState, useEffect, useCallback, useMemo } from 'react';
import type { Bottle, GovernanceVote, MarketplaceListing } from '@/src/types/membership';
import JsonLd from './JsonLd';
import LoadingSkeleton from '@/components/membership/LoadingSkeleton';
export default function MembershipPage() {
const [bottles, setBottles] = useState<Bottle[]>([]);
const [activeVotes, setActiveVotes] = useState<GovernanceVote[]>([]);
const [marketplaceListings, setMarketplaceListings] = useState<MarketplaceListing[]>([]);
const [selectedTab, setSelectedTab] = useState<'bottles' | 'voting' | 'marketplace' | 'my-holdings'>('bottles');
const [loading, setLoading] = useState(true);
const loadData = useCallback(async () => {
try {
const [bottlesRes, votesRes, marketRes] = await Promise.all([
fetch('/api/membership/bottles'),
fetch('/api/membership/votes?active=true'),
fetch('/api/membership/marketplace?active=true')
]);
const [bottlesData, votesData, marketData] = await Promise.all([
bottlesRes.json(),
votesRes.json(),
marketRes.json()
]);
if (bottlesData.success) setBottles(bottlesData.data);
if (votesData.success) setActiveVotes(votesData.data);
if (marketData.success) setMarketplaceListings(marketData.data);
} catch (error) {
console.error('Error loading data:', error);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
loadData();
}, [loadData]);
// Memoize tabs configuration
const tabs = useMemo(() => [
{ id: 'bottles' as const, label: 'Investment Bottles', icon: '🍷', ariaLabel: 'View available wine bottles for investment' },
{ id: 'voting' as const, label: 'Governance Voting', icon: '🗳️', ariaLabel: 'Participate in governance votes' },
{ id: 'marketplace' as const, label: 'Trading Marketplace', icon: '🏪', ariaLabel: 'Browse marketplace listings' },
{ id: 'my-holdings' as const, label: 'My Portfolio', icon: '💼', ariaLabel: 'View your wine portfolio' }
], []);
if (loading) {
return <LoadingSkeleton />;
}
return (
<>
<JsonLd />
<div className="min-h-screen bg-gradient-to-br from-purple-900 via-indigo-900 to-blue-900 transition-all duration-300">
{/* Optimized Header with better mobile responsiveness */}
<header className="bg-black/30 backdrop-blur-md border-b border-white/10 sticky top-0 z-50 transition-all duration-300">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<h1 className="text-3xl sm:text-4xl font-bold text-white mb-2 animate-slide-down">
Tokenized Wine Investment Platform | Wine Membership DAO
</h1>
<p className="text-gray-300 text-base sm:text-lg animate-fade-in">
Own fractional shares in rare wine bottles like Château Margaux 1996 and DRC La Tâche 2005.
Participate in governance voting, receive sample distributions, and trade shares on our marketplace.
</p>
{/* Info Grid with improved mobile layout */}
<div className="mt-4 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4">
<div className="text-white bg-white/5 rounded-lg p-3 backdrop-blur-sm transition-all duration-200 hover:bg-white/10">
<span className="font-semibold block sm:inline">10,000 Shares</span>
<span className="text-sm sm:text-base text-gray-300 block sm:inline sm:ml-1">per bottle</span>
</div>
<div className="text-white bg-white/5 rounded-lg p-3 backdrop-blur-sm transition-all duration-200 hover:bg-white/10">
<span className="font-semibold block sm:inline">Democratic Governance</span>
<span className="text-sm sm:text-base text-gray-300 block sm:inline sm:ml-1">with voting rights</span>
</div>
<div className="text-white bg-white/5 rounded-lg p-3 backdrop-blur-sm transition-all duration-200 hover:bg-white/10">
<span className="font-semibold block sm:inline">Licensed Distribution</span>
<span className="text-sm sm:text-base text-gray-300 block sm:inline sm:ml-1">via compliance partners</span>
</div>
</div>
</div>
</header>
{/* Improved Navigation with keyboard support and animations */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<nav
className="flex flex-col sm:flex-row gap-2 mb-8 bg-black/20 p-2 rounded-xl backdrop-blur-sm animate-slide-up"
role="navigation"
aria-label="Membership platform sections"
>
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setSelectedTab(tab.id)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setSelectedTab(tab.id);
}
}}
aria-label={tab.ariaLabel}
aria-current={selectedTab === tab.id ? 'page' : undefined}
className={`flex-1 py-3 px-4 sm:px-6 rounded-lg font-semibold transition-all duration-300 transform focus:outline-none focus:ring-2 focus:ring-purple-400 focus:ring-offset-2 focus:ring-offset-purple-900 ${
selectedTab === tab.id
? 'bg-white text-purple-900 shadow-lg scale-105'
: 'text-white hover:bg-white/10 hover:scale-102'
}`}
>
<span className="hidden sm:inline">{tab.icon} {tab.label}</span>
<span className="sm:hidden text-center">{tab.icon}<br /><span className="text-xs">{tab.label.split(' ')[0]}</span></span>
</button>
))}
</nav>
{/* Content with fade transitions */}
<main className="animate-fade-in">
{selectedTab === 'bottles' && <BottlesTab bottles={bottles} />}
{selectedTab === 'voting' && <VotingTab votes={activeVotes} bottles={bottles} onVoteComplete={loadData} />}
{selectedTab === 'marketplace' && <MarketplaceTab listings={marketplaceListings} bottles={bottles} onPurchase={loadData} />}
{selectedTab === 'my-holdings' && <MyHoldingsTab />}
</main>
</div>
{/* Enhanced Footer */}
<footer className="bg-black/40 border-t border-white/10 mt-12 py-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-white mb-4">About Tokenized Wine Investment</h2>
<div className="grid md:grid-cols-2 gap-6 text-gray-300">
<div>
<h3 className="font-semibold text-white mb-2">What is Wine Tokenization?</h3>
<p className="text-sm leading-relaxed">
Wine tokenization allows investors to purchase fractional shares in ultra-premium wine bottles.
Each bottle is divided into 10,000 membership units, making rare wines like Château Margaux
and Domaine de la Romanée-Conti accessible to wine enthusiasts and investors at any budget level.
</p>
</div>
<div>
<h3 className="font-semibold text-white mb-2">Investment Benefits</h3>
<ul className="text-sm space-y-1">
<li>• Fractional ownership of investment-grade wines</li>
<li>• Democratic governance through voting rights</li>
<li>• Professional sample distribution when unlocked</li>
<li>• Secondary market trading via our marketplace</li>
<li>• Authenticated provenance and secure storage</li>
</ul>
</div>
</div>
<div className="mt-6 text-center text-gray-400 text-sm">
<p>Red Thunder Wine Membership Platform | Tokenized Rare Wine Investment</p>
<p className="mt-2">Featuring Bordeaux First Growths, Burgundy Grand Crus, and other collectible wines</p>
</div>
</div>
</footer>
</div>
</>
);
}
function BottlesTab({ bottles }: { bottles: Bottle[] }) {
return (
<section aria-label="Available tokenized wine bottles for investment">
<h2 className="text-2xl sm:text-3xl font-bold text-white mb-6">Investment-Grade Wine Bottles</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
{bottles.map((bottle, index) => {
const sharesSold = bottle.totalMembershipUnits - bottle.availableMembershipUnits;
const sharesNeeded = bottle.totalMembershipUnits;
const pricePerShare = bottle.estimatedValue / bottle.totalMembershipUnits;
const fundingProgress = (sharesSold / sharesNeeded) * 100;
return (
<article
key={bottle.id}
className="bg-white/10 backdrop-blur-sm rounded-xl p-4 sm:p-6 border border-white/20 hover:border-white/40 transition-all duration-300 transform hover:scale-105 hover:shadow-2xl animate-scale-in focus-within:ring-2 focus-within:ring-purple-400"
style={{ animationDelay: `${index * 100}ms` }}
itemScope
itemType="https://schema.org/Product"
>
<div className="flex justify-between items-start mb-4">
<div className="flex-1">
<h3 className="text-lg sm:text-xl font-bold text-white mb-1" itemProp="name">
{bottle.name}
</h3>
<p className="text-gray-300 text-sm sm:text-base">{bottle.vintage}</p>
</div>
<span
className={`px-3 py-1 rounded-full text-xs font-semibold transition-all duration-200 ${
bottle.status === 'active' ? 'bg-green-500/20 text-green-300 border border-green-500/50' :
bottle.status === 'unlocked' ? 'bg-yellow-500/20 text-yellow-300 border border-yellow-500/50' :
'bg-gray-500/20 text-gray-300 border border-gray-500/50'
}`}
role="status"
aria-label={`Status: ${bottle.status}`}
>
{bottle.status}
</span>
</div>
<div className="space-y-2 mb-4" itemProp="description">
<p className="text-gray-300 text-sm">
<span className="font-semibold">Producer:</span> <span itemProp="brand">{bottle.producer}</span>
</p>
<p className="text-gray-300 text-sm">
<span className="font-semibold">Region:</span> {bottle.region}
</p>
<p className="text-gray-300 text-sm">
<span className="font-semibold">Size:</span> {bottle.size}
</p>
</div>
<div className="bg-black/20 rounded-lg p-3 sm:p-4 mb-4">
<div className="grid grid-cols-2 gap-3 sm:gap-4">
<div>
<p className="text-lg sm:text-xl font-bold text-white mb-1">
${(bottle.estimatedValue / 4.5).toLocaleString()}
</p>
<p className="text-gray-400 text-xs">Market Price</p>
</div>
<div itemProp="offers" itemScope itemType="https://schema.org/Offer">
<meta itemProp="priceCurrency" content="USD" />
<meta itemProp="price" content={bottle.estimatedValue.toString()} />
<p className="text-lg sm:text-xl font-bold text-green-400 mb-1">
${bottle.estimatedValue.toLocaleString()}
</p>
<p className="text-gray-400 text-xs">Total Value</p>
</div>
</div>
<p className="text-xs text-gray-500 mt-2">Includes acquisition, authentication, insurance & storage</p>
</div>
{/* Enhanced Progress Bar with animation */}
<div className="mb-4">
<div className="flex justify-between text-sm mb-2">
<span className="text-white font-semibold text-xs sm:text-sm">
{sharesSold.toLocaleString()} / {sharesNeeded.toLocaleString()} Sold
</span>
<span className="text-purple-300 font-bold">{fundingProgress.toFixed(1)}%</span>
</div>
<div className="h-3 bg-black/30 rounded-full overflow-hidden border border-purple-500/30">
<div
className="h-full bg-gradient-to-r from-purple-500 to-pink-500 transition-all duration-1000 ease-out"
style={{ width: `${fundingProgress}%` }}
role="progressbar"
aria-valuenow={fundingProgress}
aria-valuemin={0}
aria-valuemax={100}
aria-label={`${fundingProgress.toFixed(1)}% funded`}
/>
</div>
<p className="text-xs text-gray-400 mt-1">
{bottle.availableMembershipUnits.toLocaleString()} shares available
</p>
</div>
<div className="grid grid-cols-2 gap-3 mb-4">
<div className="bg-black/30 rounded-lg p-3 border border-purple-500/20 transition-colors hover:border-purple-500/40">
<p className="text-base sm:text-lg font-bold text-white">${pricePerShare.toFixed(2)}</p>
<p className="text-xs text-gray-400">Per Share</p>
</div>
<div className="bg-black/30 rounded-lg p-3 border border-green-500/20 transition-colors hover:border-green-500/40">
<p className="text-base sm:text-lg font-bold text-green-300">{bottle.availableMembershipUnits.toLocaleString()}</p>
<p className="text-xs text-gray-400">Available</p>
</div>
</div>
<button
className="w-full bg-gradient-to-r from-purple-600 to-pink-600 text-white py-3 rounded-lg font-semibold hover:from-purple-700 hover:to-pink-700 transition-all duration-300 shadow-lg hover:shadow-xl transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-purple-400 focus:ring-offset-2 focus:ring-offset-purple-900"
aria-label={`Buy shares of ${bottle.name} ${bottle.vintage}`}
>
Buy Wine Shares
</button>
</article>
);
})}
</div>
</section>
);
}
function VotingTab({ votes, bottles, onVoteComplete }: {
votes: GovernanceVote[];
bottles: Bottle[];
onVoteComplete: () => void;
}) {
const [selectedVote, setSelectedVote] = useState<string | null>(null);
const [voteChoice, setVoteChoice] = useState<'for' | 'against' | 'abstain'>('for');
const [userId] = useState('user_1');
const [membershipUnitId, setMembershipUnitId] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const handleCastVote = async (voteId: string) => {
if (!membershipUnitId) {
alert('Please enter your Membership Unit ID');
return;
}
setIsSubmitting(true);
try {
const res = await fetch(`/api/membership/votes/${voteId}/cast`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, membershipUnitId, choice: voteChoice })
});
const data = await res.json();
if (data.success) {
alert('Vote cast successfully!');
setSelectedVote(null);
setMembershipUnitId('');
onVoteComplete();
} else {
alert(data.error || 'Failed to cast vote');
}
} catch (error) {
alert('Error casting vote');
} finally {
setIsSubmitting(false);
}
};
return (
<section aria-label="Active governance votes">
<h2 className="text-2xl sm:text-3xl font-bold text-white mb-6">Governance & Voting</h2>
<div className="space-y-6">
{votes.map((vote, index) => {
const bottle = bottles.find(b => b.id === vote.bottleId);
const totalVotes = vote.votesFor + vote.votesAgainst + vote.votesAbstain;
const forPercentage = totalVotes > 0 ? (vote.votesFor / totalVotes) * 100 : 0;
const againstPercentage = totalVotes > 0 ? (vote.votesAgainst / totalVotes) * 100 : 0;
const quorumProgress = bottle ? (totalVotes / bottle.totalMembershipUnits) * 100 : 0;
const totalShares = bottle?.totalMembershipUnits || 0;
const votesNeededForQuorum = Math.ceil(totalShares * (vote.quorumRequired / 100));
const votesNeededToPass = Math.ceil(votesNeededForQuorum * (vote.approvalThreshold / 100));
const votesRemainingForQuorum = Math.max(0, votesNeededForQuorum - totalVotes);
const votesRemainingToPass = Math.max(0, votesNeededToPass - vote.votesFor);
return (
<article
key={vote.id}
className="bg-white/10 backdrop-blur-sm rounded-xl p-4 sm:p-6 border border-white/20 transition-all duration-300 hover:border-white/30 animate-slide-up"
style={{ animationDelay: `${index * 100}ms` }}
>
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-4 mb-4">
<div className="flex-1">
<h3 className="text-xl sm:text-2xl font-bold text-white mb-2">{vote.proposal}</h3>
<p className="text-gray-300 mb-2 text-sm sm:text-base">{vote.description}</p>
{bottle && (
<p className="text-purple-300 font-semibold text-sm sm:text-base">
📦 {bottle.name} ({bottle.vintage})
</p>
)}
</div>
<span
className="px-4 py-2 rounded-full text-sm font-semibold bg-green-500/20 text-green-300 border border-green-500/50 self-start"
role="status"
aria-label={`Vote status: ${vote.status}`}
>
{vote.status}
</span>
</div>
{/* Responsive Stats Grid */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 mb-6">
<div className="bg-black/30 rounded-lg p-3 sm:p-4 border border-purple-500/30">
<div className="text-lg sm:text-2xl font-bold text-white">{totalShares.toLocaleString()}</div>
<div className="text-xs text-gray-400">Total Shares</div>
</div>
<div className="bg-black/30 rounded-lg p-3 sm:p-4 border border-blue-500/30">
<div className="text-lg sm:text-2xl font-bold text-blue-300">{(totalShares - (bottle?.availableMembershipUnits || 0)).toLocaleString()}</div>
<div className="text-xs text-gray-400">Shares Sold</div>
</div>
<div className="bg-black/30 rounded-lg p-3 sm:p-4 border border-green-500/30">
<div className="text-lg sm:text-2xl font-bold text-green-300">{votesNeededForQuorum.toLocaleString()}</div>
<div className="text-xs text-gray-400">Quorum Needed</div>
</div>
<div className="bg-black/30 rounded-lg p-3 sm:p-4 border border-yellow-500/30">
<div className="text-lg sm:text-2xl font-bold text-yellow-300">{votesRemainingToPass.toLocaleString()}</div>
<div className="text-xs text-gray-400">To Unlock</div>
</div>
</div>
{/* Enhanced Vote Progress with better contrast */}
<div className="space-y-4 mb-6">
<div>
<div className="flex justify-between text-sm mb-2">
<span className="text-white font-semibold text-xs sm:text-sm">
✅ For: {vote.votesFor} ({votesRemainingToPass} more needed)
</span>
<span className="text-green-300 font-semibold">{forPercentage.toFixed(1)}%</span>
</div>
<div className="h-4 bg-black/40 rounded-full overflow-hidden border-2 border-green-500/30">
<div
className="h-full bg-gradient-to-r from-green-500 to-green-400 transition-all duration-1000"
style={{ width: `${forPercentage}%` }}
role="progressbar"
aria-valuenow={forPercentage}
aria-valuemin={0}
aria-valuemax={100}
aria-label={`${forPercentage.toFixed(1)}% votes for`}
/>
</div>
</div>
<div>
<div className="flex justify-between text-sm mb-2">
<span className="text-white font-semibold text-xs sm:text-sm">
❌ Against: {vote.votesAgainst}
</span>
<span className="text-red-300 font-semibold">{againstPercentage.toFixed(1)}%</span>
</div>
<div className="h-4 bg-black/40 rounded-full overflow-hidden border-2 border-red-500/30">
<div
className="h-full bg-gradient-to-r from-red-500 to-red-400 transition-all duration-1000"
style={{ width: `${againstPercentage}%` }}
role="progressbar"
aria-valuenow={againstPercentage}
aria-valuemin={0}
aria-valuemax={100}
aria-label={`${againstPercentage.toFixed(1)}% votes against`}
/>
</div>
</div>
<div>
<div className="flex justify-between text-sm mb-2">
<span className="text-white font-semibold text-xs sm:text-sm">
📊 Quorum: {totalVotes}/{votesNeededForQuorum} ({votesRemainingForQuorum} needed)
</span>
<span className="text-blue-300 font-semibold">{quorumProgress.toFixed(1)}%</span>
</div>
<div className="h-4 bg-black/40 rounded-full overflow-hidden border-2 border-blue-500/30">
<div
className="h-full bg-gradient-to-r from-blue-500 to-blue-400 transition-all duration-1000"
style={{ width: `${Math.min(quorumProgress, 100)}%` }}
role="progressbar"
aria-valuenow={Math.min(quorumProgress, 100)}
aria-valuemin={0}
aria-valuemax={100}
aria-label={`${quorumProgress.toFixed(1)}% quorum reached`}
/>
</div>
</div>
</div>
<p className="text-gray-400 text-sm mb-4">
Voting ends: {new Date(vote.endDate).toLocaleDateString()} at {new Date(vote.endDate).toLocaleTimeString()}
</p>
{/* Cast Vote Interface */}
{selectedVote === vote.id ? (
<div className="bg-black/20 p-4 rounded-lg space-y-3 border border-white/10">
<label htmlFor={`membership-id-${vote.id}`} className="sr-only">
Membership Unit ID
</label>
<input
id={`membership-id-${vote.id}`}
type="text"
placeholder="Enter your Membership Unit ID"
value={membershipUnitId}
onChange={(e) => setMembershipUnitId(e.target.value)}
className="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-400 focus:border-transparent text-base"
style={{ fontSize: '16px' }}
disabled={isSubmitting}
aria-required="true"
/>
<fieldset className="flex flex-col sm:flex-row gap-2">
<legend className="sr-only">Choose your vote</legend>
{(['for', 'against', 'abstain'] as const).map((choice) => (
<button
key={choice}
type="button"
onClick={() => setVoteChoice(choice)}
disabled={isSubmitting}
className={`flex-1 py-3 rounded-lg font-semibold transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-400 ${
voteChoice === choice
? 'bg-purple-600 text-white shadow-lg scale-105'
: 'bg-white/10 text-gray-300 hover:bg-white/20'
} disabled:opacity-50 disabled:cursor-not-allowed`}
aria-pressed={voteChoice === choice}
>
{choice === 'for' && '✅ For'}
{choice === 'against' && '❌ Against'}
{choice === 'abstain' && '⚪ Abstain'}
</button>
))}
</fieldset>
<div className="flex flex-col sm:flex-row gap-2">
<button
onClick={() => handleCastVote(vote.id)}
disabled={isSubmitting}
className="flex-1 bg-gradient-to-r from-green-600 to-green-500 text-white py-3 rounded-lg font-semibold hover:from-green-700 hover:to-green-600 transition-all duration-300 shadow-lg disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-green-400"
>
{isSubmitting ? 'Submitting...' : 'Confirm Vote'}
</button>
<button
onClick={() => setSelectedVote(null)}
disabled={isSubmitting}
className="px-6 bg-white/10 text-white py-3 rounded-lg font-semibold hover:bg-white/20 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-gray-400"
>
Cancel
</button>
</div>
</div>
) : (
<button
onClick={() => setSelectedVote(vote.id)}
className="w-full bg-gradient-to-r from-purple-600 to-pink-600 text-white py-3 rounded-lg font-semibold hover:from-purple-700 hover:to-pink-700 transition-all duration-300 shadow-lg transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-purple-400"
>
Cast Your Vote
</button>
)}
</article>
);
})}
{votes.length === 0 && (
<div className="text-center py-12 bg-white/5 rounded-xl border border-white/10">
<p className="text-gray-300 text-lg">No active governance votes at this time</p>
</div>
)}
</div>
</section>
);
}
function MarketplaceTab({ listings, bottles, onPurchase }: {
listings: MarketplaceListing[];
bottles: Bottle[];
onPurchase: () => void;
}) {
const [isPurchasing, setIsPurchasing] = useState<string | null>(null);
const handlePurchase = async (listingId: string) => {
const buyerId = 'user_1';
const buyerEmail = 'buyer@example.com';
const buyerName = 'Demo Buyer';
if (!confirm('Are you sure you want to purchase this item?')) return;
setIsPurchasing(listingId);
try {
const res = await fetch(`/api/membership/marketplace/${listingId}/purchase`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ buyerId, buyerEmail, buyerName })
});
const data = await res.json();
if (data.success) {
alert('Purchase successful!');
onPurchase();
} else {
alert(data.error || 'Purchase failed');
}
} catch (error) {
alert('Error completing purchase');
} finally {
setIsPurchasing(null);
}
};
return (
<section aria-label="Marketplace for trading wine shares">
<h2 className="text-2xl sm:text-3xl font-bold text-white mb-6">Secondary Market Trading</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6">
{listings.map((listing, index) => {
const bottle = bottles.find(b => b.id === listing.bottleId);
const isPurchasingThis = isPurchasing === listing.id;
return (
<article
key={listing.id}
className="bg-white/10 backdrop-blur-sm rounded-xl p-4 sm:p-6 border border-white/20 transition-all duration-300 hover:border-white/30 hover:shadow-xl animate-scale-in"
style={{ animationDelay: `${index * 100}ms` }}
>
<div className="flex justify-between items-start mb-4">
<div className="flex-1">
<span className="inline-block px-3 py-1 rounded-full text-xs font-semibold bg-purple-500/20 text-purple-300 border border-purple-500/50 mb-2">
{listing.listingType === 'membership' ? '🎫 Membership Share' : '🧪 Sample Right'}
</span>
<h3 className="text-lg sm:text-xl font-bold text-white mt-2">
{bottle?.name || 'Unknown Bottle'}
</h3>
<p className="text-gray-300 text-sm sm:text-base">{bottle?.vintage}</p>
</div>
</div>
<div className="bg-black/20 rounded-lg p-4 mb-4">
<p className="text-2xl sm:text-3xl font-bold text-white mb-1">
${listing.price.toLocaleString()}
</p>
<p className="text-gray-400 text-sm">Listed by: {listing.sellerName}</p>
</div>
<p className="text-gray-400 text-sm mb-4">
Listed: {new Date(listing.listedAt).toLocaleDateString()}
</p>
<button
onClick={() => handlePurchase(listing.id)}
disabled={isPurchasingThis}
className="w-full bg-gradient-to-r from-green-600 to-emerald-600 text-white py-3 rounded-lg font-semibold hover:from-green-700 hover:to-emerald-700 transition-all duration-300 shadow-lg transform hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-green-400"
>
{isPurchasingThis ? 'Processing...' : 'Purchase Wine Share'}
</button>
</article>
);
})}
{listings.length === 0 && (
<div className="col-span-2 text-center py-12 bg-white/5 rounded-xl border border-white/10">
<p className="text-gray-300 text-lg">No items currently listed in marketplace</p>
</div>
)}
</div>
</section>
);
}
function MyHoldingsTab() {
return (
<section
aria-label="Your wine investment portfolio"
className="bg-white/10 backdrop-blur-sm rounded-xl p-6 sm:p-8 border border-white/20 text-center animate-fade-in"
>
<h2 className="text-2xl font-bold text-white mb-4">My Wine Investment Portfolio</h2>
<p className="text-gray-300 mb-6 text-sm sm:text-base">
Connect your wallet to view your wine membership shares, sample rights, and voting history
</p>
<button className="bg-gradient-to-r from-blue-600 to-cyan-600 text-white px-6 sm:px-8 py-3 rounded-lg font-semibold hover:from-blue-700 hover:to-cyan-700 transition-all duration-300 shadow-lg transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-400">
Connect Wallet
</button>
</section>
);
}