Enhance Indexer page with sorting functionality and size/age parsing; update News and Settings pages for improved layout and responsiveness; implement subscription tiers and donation options on Upgrade page.
This commit is contained in:
parent
1eb9ca54f9
commit
3cd85be0dd
@ -62,9 +62,9 @@ export default function NewsPage() {
|
||||
// };
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4 py-8 md:py-12 space-y-8">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-3xl font-bold tracking-tight">Site News & Announcements</h1>
|
||||
<div className="container mx-auto p-4 py-8 md:py-12 lg:py-16 xl:max-w-7xl space-y-8 lg:space-y-10">
|
||||
<div className="flex justify-between items-center mb-6 lg:mb-8">
|
||||
<h1 className="text-3xl lg:text-4xl font-bold tracking-tight">Site News & Announcements</h1>
|
||||
<div className="space-x-2">
|
||||
<Link href="/upgrade" passHref>
|
||||
<Button variant="ghost">Upgrade</Button>
|
||||
@ -78,11 +78,11 @@ export default function NewsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-6 lg:space-y-8">
|
||||
{mockNewsData.map((article) => (
|
||||
<Card key={article.id} className="shadow-md hover:shadow-lg transition-shadow duration-200">
|
||||
<CardHeader>
|
||||
<CardTitle>{article.title}</CardTitle>
|
||||
<CardTitle className="text-xl lg:text-2xl">{article.title}</CardTitle>
|
||||
<CardDescription>
|
||||
Posted by {article.author} on {article.date}
|
||||
</CardDescription>
|
||||
|
||||
167
app/page.tsx
167
app/page.tsx
@ -29,7 +29,8 @@ import {
|
||||
} from "@/components/ui/carousel";
|
||||
import Autoplay from "embla-carousel-autoplay";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import React, { useState, useRef } from "react";
|
||||
import React, { useState, useRef, useMemo } from "react";
|
||||
import { ArrowUpDown, ArrowUp, ArrowDown } from "lucide-react";
|
||||
|
||||
// Mock data structure
|
||||
interface NzbItem {
|
||||
@ -63,9 +64,45 @@ const PlaceholderCarouselImage = ({ text }: { text: string }) => (
|
||||
</div>
|
||||
);
|
||||
|
||||
// Helper function to parse size string (e.g., "1.2 GB") to bytes
|
||||
const parseSizeToBytes = (sizeStr: string): number => {
|
||||
const parts = sizeStr.toLowerCase().split(" ");
|
||||
if (parts.length !== 2) return 0;
|
||||
const value = parseFloat(parts[0]);
|
||||
const unit = parts[1];
|
||||
if (isNaN(value)) return 0;
|
||||
|
||||
if (unit.startsWith("kb")) return value * 1024;
|
||||
if (unit.startsWith("mb")) return value * 1024 * 1024;
|
||||
if (unit.startsWith("gb")) return value * 1024 * 1024 * 1024;
|
||||
if (unit.startsWith("tb")) return value * 1024 * 1024 * 1024 * 1024;
|
||||
return value; // Assuming bytes if no unit or unknown unit
|
||||
};
|
||||
|
||||
// Helper function to parse age string (e.g., "3 days", "5 hours") to a consistent unit (e.g., minutes)
|
||||
const parseAgeToMinutes = (ageStr: string): number => {
|
||||
const parts = ageStr.toLowerCase().split(" ");
|
||||
if (parts.length !== 2) return 0;
|
||||
const value = parseInt(parts[0]);
|
||||
const unit = parts[1];
|
||||
if (isNaN(value)) return 0;
|
||||
|
||||
if (unit.startsWith("minute")) return value;
|
||||
if (unit.startsWith("hour")) return value * 60;
|
||||
if (unit.startsWith("day")) return value * 60 * 24;
|
||||
if (unit.startsWith("week")) return value * 60 * 24 * 7;
|
||||
if (unit.startsWith("month")) return value * 60 * 24 * 30; // Approximate
|
||||
if (unit.startsWith("year")) return value * 60 * 24 * 365; // Approximate
|
||||
return 0;
|
||||
};
|
||||
|
||||
export default function IndexerPage() {
|
||||
const categories = ["All", "Movies", "Series", "Anime", "Music"];
|
||||
const [selectedItem, setSelectedItem] = useState<NzbItem | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<string>("All");
|
||||
|
||||
const [sortColumn, setSortColumn] = useState<keyof NzbItem | null>(null);
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||
|
||||
// Autoplay plugin ref
|
||||
const autoplayPlugin = useRef(
|
||||
@ -151,10 +188,45 @@ export default function IndexerPage() {
|
||||
},
|
||||
];
|
||||
|
||||
// Filter data based on category (simple filter for now)
|
||||
const getFilteredData = (category: string) => {
|
||||
if (category === "All") return mockNzbData;
|
||||
return mockNzbData.filter((item) => item.category === category);
|
||||
const handleSort = (column: keyof NzbItem) => {
|
||||
if (sortColumn === column) {
|
||||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
setSortColumn(column);
|
||||
setSortDirection('asc');
|
||||
}
|
||||
};
|
||||
|
||||
const sortedData = useMemo(() => {
|
||||
let dataToFilter = activeTab === "All" ? mockNzbData : mockNzbData.filter(item => item.category === activeTab);
|
||||
if (!sortColumn) return dataToFilter;
|
||||
|
||||
const currentSortColumn = sortColumn as keyof NzbItem;
|
||||
|
||||
return [...dataToFilter].sort((a, b) => {
|
||||
const aVal = a[currentSortColumn];
|
||||
const bVal = b[currentSortColumn];
|
||||
|
||||
let comparison = 0;
|
||||
|
||||
if (currentSortColumn === 'size') {
|
||||
comparison = parseSizeToBytes(aVal as string) - parseSizeToBytes(bVal as string);
|
||||
} else if (currentSortColumn === 'age') {
|
||||
comparison = parseAgeToMinutes(aVal as string) - parseAgeToMinutes(bVal as string);
|
||||
} else if (typeof aVal === 'number' && typeof bVal === 'number') {
|
||||
comparison = aVal - bVal;
|
||||
} else if (typeof aVal === 'string' && typeof bVal === 'string') {
|
||||
comparison = aVal.localeCompare(bVal);
|
||||
}
|
||||
|
||||
return sortDirection === 'asc' ? comparison : -comparison;
|
||||
});
|
||||
}, [mockNzbData, activeTab, sortColumn, sortDirection]);
|
||||
|
||||
const getSortIcon = (column: keyof NzbItem) => {
|
||||
if (sortColumn !== column) return <ArrowUpDown className="ml-2 h-4 w-4" />;
|
||||
if (sortDirection === 'asc') return <ArrowUp className="ml-2 h-4 w-4 text-primary" />;
|
||||
return <ArrowDown className="ml-2 h-4 w-4 text-primary" />;
|
||||
};
|
||||
|
||||
return (
|
||||
@ -212,7 +284,7 @@ export default function IndexerPage() {
|
||||
</Carousel>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="All" className="w-full">
|
||||
<Tabs defaultValue="All" className="w-full" onValueChange={setActiveTab}>
|
||||
<TabsList className="mb-4">
|
||||
{categories.map((category) => (
|
||||
<TabsTrigger key={category} value={category}>
|
||||
@ -221,48 +293,49 @@ export default function IndexerPage() {
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{categories.map((category) => (
|
||||
<TabsContent key={category} value={category}>
|
||||
<div className="flex w-full max-w-sm items-center space-x-2 mb-4">
|
||||
<Input type="text" placeholder={`Search in ${category}...`} />
|
||||
<Button type="submit">Search</Button>
|
||||
</div>
|
||||
<Table>
|
||||
<TableCaption>
|
||||
{category === "All"
|
||||
? "A list of NZBs."
|
||||
: `Results for ${category}.`}
|
||||
</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[35%]">Name</TableHead>
|
||||
<TableHead>Size</TableHead>
|
||||
<TableHead>Age</TableHead>
|
||||
<TableHead>Downloads</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
<Table>
|
||||
<TableCaption>
|
||||
{activeTab === "All"
|
||||
? "A list of all NZBs."
|
||||
: `Results for ${activeTab}.`}
|
||||
{sortColumn && ` Sorted by ${sortColumn} (${sortDirection === 'asc' ? 'ascending' : 'descending'}).`}
|
||||
</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[35%] cursor-pointer hover:bg-muted/50 transition-colors" onClick={() => handleSort('name')}>
|
||||
<div className="flex items-center">Name {getSortIcon('name')}</div>
|
||||
</TableHead>
|
||||
<TableHead className="cursor-pointer hover:bg-muted/50 transition-colors" onClick={() => handleSort('size')}>
|
||||
<div className="flex items-center">Size {getSortIcon('size')}</div>
|
||||
</TableHead>
|
||||
<TableHead className="cursor-pointer hover:bg-muted/50 transition-colors" onClick={() => handleSort('age')}>
|
||||
<div className="flex items-center">Age {getSortIcon('age')}</div>
|
||||
</TableHead>
|
||||
<TableHead className="cursor-pointer hover:bg-muted/50 transition-colors" onClick={() => handleSort('downloads')}>
|
||||
<div className="flex items-center">Downloads {getSortIcon('downloads')}</div>
|
||||
</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{sortedData.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell className="font-medium">{item.name}</TableCell>
|
||||
<TableCell>{item.size}</TableCell>
|
||||
<TableCell>{item.age}</TableCell>
|
||||
<TableCell>{item.downloads.toLocaleString()}</TableCell>
|
||||
<TableCell className="text-right space-x-2">
|
||||
<Button variant="ghost" size="sm" onClick={() => setSelectedItem(item)}>
|
||||
Info
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Download
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{getFilteredData(category).map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell className="font-medium">{item.name}</TableCell>
|
||||
<TableCell>{item.size}</TableCell>
|
||||
<TableCell>{item.age}</TableCell>
|
||||
<TableCell>{item.downloads.toLocaleString()}</TableCell>
|
||||
<TableCell className="text-right space-x-2">
|
||||
<Button variant="ghost" size="sm" onClick={() => setSelectedItem(item)}>
|
||||
Info
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Download
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TabsContent>
|
||||
))}
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Tabs>
|
||||
|
||||
{/* Dialog for NZB Info - controlled by selectedItem state */}
|
||||
|
||||
@ -26,9 +26,9 @@ export default function SettingsPage() {
|
||||
const manualDownloadsPercentage = (userStats.manualDownloadsToday / userStats.manualDownloadLimitDaily) * 100;
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4 py-8 md:py-12 space-y-8">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-3xl font-bold">User Settings</h1>
|
||||
<div className="container mx-auto p-4 py-8 md:py-12 lg:py-16 xl:max-w-7xl space-y-8 lg:space-y-10">
|
||||
<div className="flex justify-between items-center mb-6 lg:mb-8">
|
||||
<h1 className="text-3xl lg:text-4xl font-bold">User Settings</h1>
|
||||
<div className="space-x-2">
|
||||
<Link href="/news" passHref>
|
||||
<Button variant="ghost">News</Button>
|
||||
@ -42,111 +42,117 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* User Statistics Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Your Statistics</CardTitle>
|
||||
<CardDescription>Overview of your account activity and limits.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">Current Rank:</span>
|
||||
<span className="text-sm text-primary font-semibold">{userStats.rank}</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-sm font-medium">Daily API Usage:</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{userStats.apiHitsToday.toLocaleString()} / {userStats.apiLimitDaily.toLocaleString()} hits
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={apiUsagePercentage} className="w-full" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-sm font-medium">Daily Manual Downloads:</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{userStats.manualDownloadsToday.toLocaleString()} / {userStats.manualDownloadLimitDaily.toLocaleString()} files
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={manualDownloadsPercentage} className="w-full" />
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">Total Releases Grabbed (All Time):</span>
|
||||
<span className="text-sm font-semibold">{userStats.filesDownloaded.toLocaleString()}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Link href="/upgrade" passHref className="w-full">
|
||||
<Button className="w-full">Upgrade Your Tier</Button>
|
||||
</Link>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 lg:gap-10">
|
||||
<div className="lg:col-span-2 space-y-8 lg:space-y-10">
|
||||
{/* User Statistics Card */}
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl lg:text-2xl">Your Statistics</CardTitle>
|
||||
<CardDescription>Overview of your account activity and limits.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">Current Rank:</span>
|
||||
<span className="text-sm text-primary font-semibold">{userStats.rank}</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-sm font-medium">Daily API Usage:</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{userStats.apiHitsToday.toLocaleString()} / {userStats.apiLimitDaily.toLocaleString()} hits
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={apiUsagePercentage} className="w-full" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-sm font-medium">Daily Manual Downloads:</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{userStats.manualDownloadsToday.toLocaleString()} / {userStats.manualDownloadLimitDaily.toLocaleString()} files
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={manualDownloadsPercentage} className="w-full" />
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">Total Releases Grabbed (All Time):</span>
|
||||
<span className="text-sm font-semibold">{userStats.filesDownloaded.toLocaleString()}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Link href="/upgrade" passHref className="w-full">
|
||||
<Button className="w-full">Upgrade Your Tier</Button>
|
||||
</Link>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Account Information</CardTitle>
|
||||
<CardDescription>Manage your account details.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="username" className="text-sm font-medium">
|
||||
Username
|
||||
</label>
|
||||
<Input id="username" defaultValue="CurrentUser" disabled />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="email" className="text-sm font-medium">
|
||||
Email
|
||||
</label>
|
||||
<Input id="email" type="email" defaultValue="user@example.com" disabled />
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button disabled>Update Account (Not Implemented)</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl lg:text-2xl">Account Information</CardTitle>
|
||||
<CardDescription>Manage your account details.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="username" className="text-sm font-medium">
|
||||
Username
|
||||
</label>
|
||||
<Input id="username" defaultValue="CurrentUser" disabled />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="email" className="text-sm font-medium">
|
||||
Email
|
||||
</label>
|
||||
<Input id="email" type="email" defaultValue="user@example.com" disabled />
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button disabled>Update Account (Not Implemented)</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>API Key</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your API key for external applications.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="apiKey" className="text-sm font-medium">
|
||||
Your API Key
|
||||
</label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input
|
||||
id="apiKey"
|
||||
defaultValue="dummy-api-key-12345abcdef"
|
||||
readOnly
|
||||
/>
|
||||
<Button variant="outline" disabled>Copy (Not Implemented)</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button variant="secondary" disabled>Regenerate API Key (Not Implemented)</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl lg:text-2xl">API Key</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your API key for external applications.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="apiKey" className="text-sm font-medium">
|
||||
Your API Key
|
||||
</label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input
|
||||
id="apiKey"
|
||||
defaultValue="dummy-api-key-12345abcdef"
|
||||
readOnly
|
||||
/>
|
||||
<Button variant="outline" disabled>Copy (Not Implemented)</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button variant="secondary" disabled>Regenerate API Key (Not Implemented)</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="border-destructive">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-destructive">Danger Zone</CardTitle>
|
||||
<CardDescription>
|
||||
Permanent actions that cannot be undone.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button variant="destructive" disabled>
|
||||
Delete Account (Not Implemented)
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="lg:col-span-1 space-y-8 lg:space-y-10">
|
||||
<Card className="border-destructive shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl lg:text-2xl text-destructive">Danger Zone</CardTitle>
|
||||
<CardDescription>
|
||||
Permanent actions that cannot be undone.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button variant="destructive" disabled>
|
||||
Delete Account (Not Implemented)
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
'use client'; // Make it a client component
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@ -5,9 +7,12 @@ import {
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardFooter,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import Link from "next/link";
|
||||
import { CheckCircle2, RadioTower } from "lucide-react"; // Added RadioTower for selection indication
|
||||
import React, { useState } from "react"; // Import useState
|
||||
|
||||
// Placeholder SVG for QR Code
|
||||
const PlaceholderQrCode = () => (
|
||||
@ -25,84 +30,305 @@ const PlaceholderQrCode = () => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
interface SubscriptionTier {
|
||||
id: string;
|
||||
name: string;
|
||||
priceDisplay: string; // e.g., "Free", "$5"
|
||||
priceUSD: number; // Numeric price for calculations
|
||||
priceDescription?: string;
|
||||
features: string[];
|
||||
ctaText: string;
|
||||
isCurrentPlan?: boolean;
|
||||
highlight?: boolean;
|
||||
buttonVariant?: "default" | "outline" | "secondary" | "ghost" | "link";
|
||||
}
|
||||
|
||||
const subscriptionTiersData: SubscriptionTier[] = [
|
||||
{
|
||||
id: "free",
|
||||
name: "Basic Access",
|
||||
priceDisplay: "Free",
|
||||
priceUSD: 0,
|
||||
features: [
|
||||
"5 API hits/day",
|
||||
"10 Manual downloads/day",
|
||||
"Access to common categories",
|
||||
"30-day access",
|
||||
"Community support",
|
||||
],
|
||||
ctaText: "Current Plan",
|
||||
isCurrentPlan: true,
|
||||
buttonVariant: "outline",
|
||||
},
|
||||
{
|
||||
id: "pro",
|
||||
name: "Pro User",
|
||||
priceDisplay: "$15",
|
||||
priceUSD: 5,
|
||||
priceDescription: "/ year",
|
||||
features: [
|
||||
"500 API hits/day",
|
||||
"100 Manual downloads/day",
|
||||
"Access to all categories",
|
||||
"Priority support",
|
||||
"Early access to new features",
|
||||
],
|
||||
ctaText: "Select Pro Plan",
|
||||
highlight: true,
|
||||
buttonVariant: "default",
|
||||
},
|
||||
{
|
||||
id: "elite",
|
||||
name: "Elite Contributor",
|
||||
priceDisplay: "$25",
|
||||
priceUSD: 10,
|
||||
priceDescription: "/ year",
|
||||
features: [
|
||||
"Unlimited API hits/day",
|
||||
"Unlimited Manual downloads/day",
|
||||
"Access to all categories & archives",
|
||||
"Dedicated VIP support",
|
||||
"Direct influence on feature roadmap",
|
||||
"Special badge in community",
|
||||
],
|
||||
ctaText: "Select Elite Plan",
|
||||
buttonVariant: "default",
|
||||
},
|
||||
];
|
||||
|
||||
// Placeholder conversion rates - replace with actual or dynamic rates if needed
|
||||
const XMR_PER_USD = 0.005;
|
||||
const LTC_PER_USD = 0.01;
|
||||
|
||||
export default function UpgradePage() {
|
||||
const [selectedTierId, setSelectedTierId] = useState<string | null>("pro");
|
||||
|
||||
const xmrAddress = "4YOURXMRADDRESSXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
|
||||
const ltcAddress = "LYOURLTCADDRESSXXXXXXXXXXXXXXXXXXXXXXX";
|
||||
|
||||
const selectedTier = subscriptionTiersData.find(tier => tier.id === selectedTierId);
|
||||
|
||||
const getCryptoAmount = (usdAmount: number, rate: number, applyDiscount: boolean = false) => {
|
||||
if (usdAmount <= 0) return "N/A";
|
||||
let finalUsdAmount = usdAmount;
|
||||
if (applyDiscount) {
|
||||
finalUsdAmount *= 0.9; // Apply 10% discount
|
||||
}
|
||||
return (finalUsdAmount * rate).toFixed(5); // Show 5 decimal places for crypto
|
||||
};
|
||||
|
||||
const isEliteXMRDiscountActive = selectedTier && selectedTier.id === 'elite';
|
||||
|
||||
const currentXMRAmount = selectedTier ? getCryptoAmount(selectedTier.priceUSD, XMR_PER_USD, isEliteXMRDiscountActive) : "Select a plan";
|
||||
const originalXMRAmount = selectedTier ? getCryptoAmount(selectedTier.priceUSD, XMR_PER_USD) : "N/A";
|
||||
const currentLTCAmount = selectedTier ? getCryptoAmount(selectedTier.priceUSD, LTC_PER_USD) : "Select a plan";
|
||||
|
||||
const handleTierSelect = (tierId: string) => {
|
||||
const tier = subscriptionTiersData.find(t => t.id === tierId);
|
||||
if (tier && !tier.isCurrentPlan) {
|
||||
setSelectedTierId(tierId);
|
||||
} else if (tier && tier.isCurrentPlan) {
|
||||
setSelectedTierId(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4 py-8 md:py-12 space-y-8">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-3xl font-bold tracking-tight">Support & Upgrade</h1>
|
||||
<div className="container mx-auto p-4 py-8 md:py-12 lg:py-16 xl:max-w-7xl space-y-12 lg:space-y-16">
|
||||
<div className="flex justify-between items-center mb-6 lg:mb-8">
|
||||
<h1 className="text-3xl lg:text-4xl font-bold tracking-tight">Support & Upgrade</h1>
|
||||
<div className="space-x-2">
|
||||
<Link href="/news" passHref>
|
||||
<Button variant="ghost">News</Button>
|
||||
</Link>
|
||||
<Link href="/settings" passHref>
|
||||
<Button variant="ghost">Settings</Button>
|
||||
</Link>
|
||||
<Link href="/" passHref>
|
||||
<Button variant="outline">Back to Indexer</Button>
|
||||
</Link>
|
||||
<Link href="/news" passHref><Button variant="ghost">News</Button></Link>
|
||||
<Link href="/settings" passHref><Button variant="ghost">Settings</Button></Link>
|
||||
<Link href="/" passHref><Button variant="outline">Back to Indexer</Button></Link>
|
||||
</div>
|
||||
</div>
|
||||
<CardDescription className="text-center text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
If you find this NZB Indexer useful, please consider supporting its development and maintenance.
|
||||
Your contributions help keep the servers running and allow us to introduce new features!
|
||||
</CardDescription>
|
||||
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-10">
|
||||
{/* Monero (XMR) Card */}
|
||||
<Card className="shadow-[0_8px_30px_rgb(160,32,240,0.3)] hover:shadow-[0_12px_40px_rgb(160,32,240,0.4)] transition-all duration-300 ease-out">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-2xl font-semibold">Monero (XMR)</CardTitle>
|
||||
{/* Optional: XMR Icon can go here */}
|
||||
</div>
|
||||
<CardDescription className="pt-1">
|
||||
Donate Monero for private, untraceable support.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6 text-center pt-4">
|
||||
<div className="flex justify-center my-6">
|
||||
<PlaceholderQrCode />
|
||||
</div>
|
||||
<p className="text-sm font-medium text-foreground/90">XMR Address:</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input type="text" value={xmrAddress} readOnly className="text-xs bg-background/30 border-foreground/20" />
|
||||
<Button variant="outline" size="sm" disabled>Copy</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground pt-2">
|
||||
Ensure you are sending XMR to this address. Transactions are irreversible.
|
||||
<section className="space-y-8 lg:space-y-10 p-6 lg:p-8 rounded-xl bg-card/20 shadow-sm">
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl lg:text-3xl font-semibold tracking-tight">Choose Your Plan</h2>
|
||||
<p className="text-muted-foreground mt-2 lg:mt-3">
|
||||
Select a plan to proceed with the donation.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 lg:gap-8">
|
||||
{subscriptionTiersData.map((tier) => (
|
||||
<Card
|
||||
key={tier.id}
|
||||
className={`flex flex-col cursor-pointer transition-all duration-200
|
||||
${selectedTierId === tier.id ? 'border-primary ring-2 ring-primary shadow-[0_8px_30px_rgb(var(--primary-hsl)/0.4)]' :
|
||||
(tier.highlight && !tier.isCurrentPlan && selectedTierId !== 'elite') ? 'border-primary/70 shadow-[0_8px_30px_rgb(var(--primary-hsl)/0.2)]' : 'shadow-md'}
|
||||
${tier.isCurrentPlan ? 'border-dashed' : ''}
|
||||
`}
|
||||
onClick={() => handleTierSelect(tier.id)}
|
||||
>
|
||||
<CardHeader className="pb-4 relative">
|
||||
{selectedTierId === tier.id && (
|
||||
<RadioTower className="absolute top-3 right-3 h-5 w-5 text-primary" />
|
||||
)}
|
||||
<CardTitle className={`text-xl font-semibold ${ (tier.highlight && !tier.isCurrentPlan && selectedTierId !== 'elite') || selectedTierId === tier.id ? 'text-primary' : ''}`}>{tier.name}</CardTitle>
|
||||
<div className="flex items-baseline">
|
||||
<span className="text-3xl font-extrabold tracking-tight">{tier.priceDisplay}</span>
|
||||
{tier.priceDescription && <span className="ml-1 text-sm text-muted-foreground">{tier.priceDescription}</span>}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-grow space-y-3 pt-2">
|
||||
<ul className="space-y-2">
|
||||
{tier.features.map((feature, index) => (
|
||||
<li key={index} className="flex items-center text-sm">
|
||||
<CheckCircle2 className={`mr-2 h-4 w-4 flex-shrink-0 ${selectedTierId === tier.id || (tier.highlight && !tier.isCurrentPlan && selectedTierId !== 'elite') ? 'text-primary' : 'text-green-500'}`} />
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
<CardFooter className="mt-auto pt-6">
|
||||
<Button
|
||||
className="w-full"
|
||||
variant={tier.isCurrentPlan ? "outline" : selectedTierId === tier.id ? "default" : (tier.highlight && selectedTierId !== 'elite') ? "default" : "outline"}
|
||||
disabled={tier.isCurrentPlan}
|
||||
onClick={(e) => {
|
||||
if (!tier.isCurrentPlan) {
|
||||
e.stopPropagation();
|
||||
handleTierSelect(tier.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{tier.isCurrentPlan ? "Current Plan" : selectedTierId === tier.id ? "Selected" : tier.ctaText}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-muted-foreground mt-2">
|
||||
{selectedTier
|
||||
? `To support the ${selectedTier.name} tier (${selectedTier.priceDisplay}${selectedTier.priceDescription || ''}), please donate the equivalent amount shown below.`
|
||||
: ""}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 gap-10">
|
||||
<Card className={`transition-all duration-300 ease-in-out rounded-lg border hover:border-primary/70
|
||||
${selectedTier && selectedTier.priceUSD > 0 ? 'border-primary/50 ring-2 ring-primary shadow-[0_10px_35px_rgb(var(--primary-hsl)/0.4)] hover:shadow-[0_12px_40px_rgb(var(--primary-hsl)/0.5)]' : 'border-border/30 opacity-70 hover:opacity-90'}`}>
|
||||
<CardHeader className="pb-4">
|
||||
<CardTitle className="text-2xl font-semibold">Monero (XMR)</CardTitle>
|
||||
<CardDescription className="pt-1">
|
||||
{selectedTier && selectedTier.priceUSD > 0
|
||||
? (
|
||||
<>
|
||||
{isEliteXMRDiscountActive && (
|
||||
<span className="block text-xs font-semibold text-green-500 mb-1">
|
||||
10% DISCOUNT
|
||||
</span>
|
||||
)}
|
||||
Suggested for {selectedTier.name}: ~{currentXMRAmount} XMR
|
||||
</>
|
||||
)
|
||||
: "Donate Monero for private, untraceable support."}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6 text-center pt-4">
|
||||
<div className="flex justify-center my-6"><PlaceholderQrCode /></div>
|
||||
<p className="text-sm font-medium text-foreground/90">XMR Address:</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input type="text" value={xmrAddress} readOnly className="text-xs bg-background/30 border-foreground/20" />
|
||||
<Button variant="outline" size="sm" disabled>Copy</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground pt-2">
|
||||
Ensure you are sending XMR to this address. Transactions are irreversible.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className={`transition-all duration-300 ease-in-out rounded-lg border hover:border-primary/70
|
||||
${selectedTier && selectedTier.priceUSD > 0 ? 'border-primary/50 ring-2 ring-primary shadow-[0_10px_35px_rgb(var(--primary-hsl)/0.4)] hover:shadow-[0_12px_40px_rgb(var(--primary-hsl)/0.5)]' : 'border-border/30 opacity-70 hover:opacity-90'}`}>
|
||||
<CardHeader className="pb-4">
|
||||
<CardTitle className="text-2xl font-semibold">Litecoin (LTC)</CardTitle>
|
||||
<CardDescription className="pt-1">
|
||||
{selectedTier && selectedTier.priceUSD > 0
|
||||
? `Suggested for ${selectedTier.name}: ~${currentLTCAmount} LTC`
|
||||
: "Donate Litecoin for fast and low-fee transactions."}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6 text-center pt-4">
|
||||
<div className="flex justify-center my-6"><PlaceholderQrCode /></div>
|
||||
<p className="text-sm font-medium text-foreground/90">LTC Address:</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input type="text" value={ltcAddress} readOnly className="text-xs bg-background/30 border-foreground/20" />
|
||||
<Button variant="outline" size="sm" disabled>Copy</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground pt-2">
|
||||
Ensure you are sending LTC to this address. Transactions are irreversible.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Litecoin (LTC) Card */}
|
||||
<Card className="shadow-[0_8px_30px_rgb(56,189,248,0.3)] hover:shadow-[0_12px_40px_rgb(56,189,248,0.4)] transition-all duration-300 ease-out">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-2xl font-semibold">Litecoin (LTC)</CardTitle>
|
||||
{/* Optional: LTC Icon can go here */}
|
||||
</div>
|
||||
<CardDescription className="pt-1">
|
||||
Donate Litecoin for fast and low-fee transactions.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6 text-center pt-4">
|
||||
<div className="flex justify-center my-6">
|
||||
<PlaceholderQrCode />
|
||||
</div>
|
||||
<p className="text-sm font-medium text-foreground/90">LTC Address:</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input type="text" value={ltcAddress} readOnly className="text-xs bg-background/30 border-foreground/20" />
|
||||
<Button variant="outline" size="sm" disabled>Copy</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground pt-2">
|
||||
Ensure you are sending LTC to this address. Transactions are irreversible.
|
||||
<section className="space-y-8 lg:space-y-10 p-6 lg:p-8 rounded-xl bg-card/20 shadow-sm">
|
||||
<div className="text-center pb-4 border-b border-border/50 mb-6 lg:mb-8" >
|
||||
<h2 className="text-2xl lg:text-3xl font-semibold tracking-tight">Or, Make a One-Time Donation</h2>
|
||||
<p className="text-muted-foreground mt-2 lg:mt-3">
|
||||
{selectedTier && selectedTier.priceUSD > 0
|
||||
? `You've selected the ${selectedTier.name} plan. Suggested donation amounts are shown below.`
|
||||
: "Support our project with a custom crypto donation." }
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-2 gap-8 lg:gap-12 xl:gap-16">
|
||||
<Card className={`transition-all duration-300 ease-in-out rounded-lg border hover:border-primary/70
|
||||
${selectedTier && selectedTier.priceUSD > 0 ? 'border-primary/50 ring-2 ring-primary shadow-[0_10px_35px_rgb(var(--primary-hsl)/0.4)] hover:shadow-[0_12px_40px_rgb(var(--primary-hsl)/0.5)]' : 'border-border/30 opacity-70 hover:opacity-90'}`}>
|
||||
<CardHeader className="pb-4">
|
||||
<CardTitle className="text-2xl font-semibold">Monero (XMR)</CardTitle>
|
||||
<CardDescription className="pt-1">
|
||||
{selectedTier && selectedTier.priceUSD > 0
|
||||
? (
|
||||
<>
|
||||
{isEliteXMRDiscountActive && (
|
||||
<span className="block text-xs font-semibold text-green-500 mb-1">
|
||||
10% DISCOUNT
|
||||
</span>
|
||||
)}
|
||||
Suggested for {selectedTier.name}: ~{currentXMRAmount} XMR
|
||||
</>
|
||||
)
|
||||
: "Donate Monero for private, untraceable support."}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6 text-center pt-4">
|
||||
<div className="flex justify-center my-6"><PlaceholderQrCode /></div>
|
||||
<p className="text-sm font-medium text-foreground/90">XMR Address:</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input type="text" value={xmrAddress} readOnly className="text-xs bg-background/30 border-foreground/20" />
|
||||
<Button variant="outline" size="sm" disabled>Copy</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground pt-2">
|
||||
Ensure you are sending XMR to this address. Transactions are irreversible.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className={`transition-all duration-300 ease-in-out rounded-lg border hover:border-primary/70
|
||||
${selectedTier && selectedTier.priceUSD > 0 ? 'border-primary/50 ring-2 ring-primary shadow-[0_10px_35px_rgb(var(--primary-hsl)/0.4)] hover:shadow-[0_12px_40px_rgb(var(--primary-hsl)/0.5)]' : 'border-border/30 opacity-70 hover:opacity-90'}`}>
|
||||
<CardHeader className="pb-4">
|
||||
<CardTitle className="text-2xl font-semibold">Litecoin (LTC)</CardTitle>
|
||||
<CardDescription className="pt-1">
|
||||
{selectedTier && selectedTier.priceUSD > 0
|
||||
? `Suggested for ${selectedTier.name}: ~${currentLTCAmount} LTC`
|
||||
: "Donate Litecoin for fast and low-fee transactions."}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6 text-center pt-4">
|
||||
<div className="flex justify-center my-6"><PlaceholderQrCode /></div>
|
||||
<p className="text-sm font-medium text-foreground/90">LTC Address:</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input type="text" value={ltcAddress} readOnly className="text-xs bg-background/30 border-foreground/20" />
|
||||
<Button variant="outline" size="sm" disabled>Copy</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground pt-2">
|
||||
Ensure you are sending LTC to this address. Transactions are irreversible.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user