init commit
This commit is contained in:
parent
7ddffa1afe
commit
91b4b5802f
107
app/globals.css
107
app/globals.css
@ -2,20 +2,105 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
|
||||
--background: 0 0% 100%;
|
||||
|
||||
--foreground: 0 0% 3.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
|
||||
--primary: 0 0% 9%;
|
||||
|
||||
--primary-foreground: 0 0% 98%;
|
||||
|
||||
--secondary: 0 0% 96.1%;
|
||||
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
|
||||
--muted: 0 0% 96.1%;
|
||||
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
|
||||
--accent: 0 0% 96.1%;
|
||||
|
||||
--accent-foreground: 0 0% 9%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--border: 0 0% 89.8%;
|
||||
|
||||
--input: 0 0% 89.8%;
|
||||
|
||||
--ring: 0 0% 3.9%;
|
||||
|
||||
--chart-1: 12 76% 61%;
|
||||
|
||||
--chart-2: 173 58% 39%;
|
||||
|
||||
--chart-3: 197 37% 24%;
|
||||
|
||||
--chart-4: 43 74% 66%;
|
||||
|
||||
--chart-5: 27 87% 67%;
|
||||
|
||||
--radius: 0.5rem
|
||||
}
|
||||
.dark {
|
||||
--background: 240 10% 4%; /* Very dark blue-black */
|
||||
--foreground: 240 10% 95%; /* Light, slightly bluish white */
|
||||
|
||||
--card: 240 10% 7%; /* Slightly lighter dark blue-black */
|
||||
--card-foreground: 240 10% 95%;
|
||||
|
||||
--popover: 240 10% 7%;
|
||||
--popover-foreground: 240 10% 95%;
|
||||
|
||||
--primary: 270 70% 55%; /* Main purple */
|
||||
--primary-foreground: 270 70% 95%; /* Light purple for text on primary */
|
||||
|
||||
--secondary: 270 40% 25%; /* Darker, muted purple */
|
||||
--secondary-foreground: 270 40% 85%; /* Lighter muted purple for text */
|
||||
|
||||
--muted: 240 10% 15%; /* Dark gray */
|
||||
--muted-foreground: 240 5% 60%; /* Lighter gray for muted text */
|
||||
|
||||
--accent: 270 80% 65%; /* Brighter purple for accents */
|
||||
--accent-foreground: 270 80% 95%; /* Light purple for text on accent */
|
||||
|
||||
--destructive: 0 62.8% 30.6%; /* Keeping default red for destructive actions */
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--border: 240 10% 15%; /* Dark gray border */
|
||||
--input: 240 10% 15%; /* Dark gray input background */
|
||||
--ring: 270 70% 60%; /* Purple for focus rings */
|
||||
|
||||
--chart-1: 270 70% 50%;
|
||||
--chart-2: 270 60% 55%;
|
||||
--chart-3: 270 50% 60%;
|
||||
--chart-4: 270 40% 65%;
|
||||
--chart-5: 270 30% 70%;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" className="dark">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
|
||||
111
app/news/page.tsx
Normal file
111
app/news/page.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
'use client'; // Or remove if no client-side interactivity needed for display
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
interface NewsArticle {
|
||||
id: string;
|
||||
title: string;
|
||||
date: string;
|
||||
author: string;
|
||||
content: string; // Can be a snippet or full content
|
||||
isFullContent?: boolean; // Optional: to toggle between snippet and full
|
||||
}
|
||||
|
||||
const mockNewsData: NewsArticle[] = [
|
||||
{
|
||||
id: "news1",
|
||||
title: "Important: Upcoming Maintenance Window",
|
||||
date: "October 26, 2023",
|
||||
author: "Admin Team",
|
||||
content: "Please be advised that we will be performing scheduled maintenance on our servers this Saturday from 02:00 to 04:00 UTC. During this time, the indexer may be temporarily unavailable. We apologize for any inconvenience.",
|
||||
},
|
||||
{
|
||||
id: "news2",
|
||||
title: "New Feature Alert: Dark Mode Enhanced!",
|
||||
date: "October 24, 2023",
|
||||
author: "Dev Team",
|
||||
content: "We're excited to announce that our dark mode has been revamped with even better contrast and new thematic colors. Check it out in your settings and let us know what you think! More UI enhancements are on the way.",
|
||||
},
|
||||
{
|
||||
id: "news3",
|
||||
title: "Welcome to the New NZB Indexer!",
|
||||
date: "October 20, 2023",
|
||||
author: "Admin",
|
||||
content: "Welcome everyone to the newly launched NZB Indexer! We're thrilled to have you here. Explore the features, and feel free to reach out with any feedback or suggestions. Happy indexing!",
|
||||
},
|
||||
];
|
||||
|
||||
export default function NewsPage() {
|
||||
// For now, we just display all content. Could add a toggle later.
|
||||
// const [expandedArticles, setExpandedArticles] = useState<Set<string>>(new Set());
|
||||
|
||||
// const toggleReadMore = (id: string) => {
|
||||
// setExpandedArticles(prev => {
|
||||
// const newSet = new Set(prev);
|
||||
// if (newSet.has(id)) {
|
||||
// newSet.delete(id);
|
||||
// } else {
|
||||
// newSet.add(id);
|
||||
// }
|
||||
// return newSet;
|
||||
// });
|
||||
// };
|
||||
|
||||
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="space-x-2">
|
||||
<Link href="/upgrade" passHref>
|
||||
<Button variant="ghost">Upgrade</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>
|
||||
|
||||
<div className="space-y-6">
|
||||
{mockNewsData.map((article) => (
|
||||
<Card key={article.id} className="shadow-md hover:shadow-lg transition-shadow duration-200">
|
||||
<CardHeader>
|
||||
<CardTitle>{article.title}</CardTitle>
|
||||
<CardDescription>
|
||||
Posted by {article.author} on {article.date}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-foreground/90 whitespace-pre-line">
|
||||
{article.content}
|
||||
{/* {article.content.length > 200 && !expandedArticles.has(article.id)
|
||||
? `${article.content.substring(0, 200)}...`
|
||||
: article.content}
|
||||
*/}
|
||||
</p>
|
||||
</CardContent>
|
||||
{/* {article.content.length > 200 && (
|
||||
<CardFooter>
|
||||
<Button variant="link" onClick={() => toggleReadMore(article.id)} className="p-0 h-auto">
|
||||
{expandedArticles.has(article.id) ? "Read Less" : "Read More"}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
)} */}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
393
app/page.tsx
393
app/page.tsx
@ -1,101 +1,308 @@
|
||||
import Image from "next/image";
|
||||
'use client';
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogClose,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
} from "@/components/ui/carousel";
|
||||
import Autoplay from "embla-carousel-autoplay";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import React, { useState, useRef } from "react";
|
||||
|
||||
// Mock data structure
|
||||
interface NzbItem {
|
||||
id: string;
|
||||
name: string;
|
||||
size: string;
|
||||
age: string;
|
||||
category: string;
|
||||
downloads: number;
|
||||
metadata: {
|
||||
description: string;
|
||||
groups: string[];
|
||||
files: string[];
|
||||
};
|
||||
}
|
||||
|
||||
// Mock data structure for Carousel items
|
||||
interface FeaturedItem {
|
||||
id: string;
|
||||
title: string;
|
||||
category: string;
|
||||
imageUrl?: string; // Optional: direct image URL or use a placeholder
|
||||
description: string;
|
||||
rating: string; // e.g., "IMDb: 8.5/10"
|
||||
}
|
||||
|
||||
// Placeholder SVG for Carousel Item Image
|
||||
const PlaceholderCarouselImage = ({ text }: { text: string }) => (
|
||||
<div className="w-full h-64 bg-muted flex items-center justify-center rounded-md">
|
||||
<span className="text-xl text-muted-foreground">{text}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function IndexerPage() {
|
||||
const categories = ["All", "Movies", "Series", "Anime", "Music"];
|
||||
const [selectedItem, setSelectedItem] = useState<NzbItem | null>(null);
|
||||
|
||||
// Autoplay plugin ref
|
||||
const autoplayPlugin = useRef(
|
||||
Autoplay({ delay: 10000, stopOnInteraction: true })
|
||||
);
|
||||
|
||||
const mockFeaturedData: FeaturedItem[] = [
|
||||
{
|
||||
id: "feat1",
|
||||
title: "Blockbuster Movie Premiere",
|
||||
category: "Movies",
|
||||
description: "The latest action-packed thriller hits the screens! Don't miss out.",
|
||||
rating: "IMDb: 9.2/10",
|
||||
},
|
||||
{
|
||||
id: "feat2",
|
||||
title: "New Hit Series - Episode 1",
|
||||
category: "Series",
|
||||
description: "A gripping new drama series that will keep you on the edge of your seat.",
|
||||
rating: "Rotten Tomatoes: 95%",
|
||||
},
|
||||
{
|
||||
id: "feat3",
|
||||
title: "Must-Watch Anime Film",
|
||||
category: "Anime",
|
||||
description: "Critically acclaimed anime feature with stunning visuals.",
|
||||
rating: "MyAnimeList: 8.9/10",
|
||||
},
|
||||
];
|
||||
|
||||
const mockNzbData: NzbItem[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Example Movie Title",
|
||||
size: "2.5 GB",
|
||||
age: "1 day",
|
||||
category: "Movies",
|
||||
downloads: 1250,
|
||||
metadata: {
|
||||
description: "A fantastic movie about something exciting.",
|
||||
groups: ["alt.binaries.movies.hd"],
|
||||
files: ["movie_part1.rar", "movie_part2.rar"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Awesome Series S01E01",
|
||||
size: "500 MB",
|
||||
age: "5 hours",
|
||||
category: "Series",
|
||||
downloads: 340,
|
||||
metadata: {
|
||||
description: "First episode of an awesome new series.",
|
||||
groups: ["alt.binaries.tv"],
|
||||
files: ["series_s01e01.mkv"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "Cool Anime Movie",
|
||||
size: "1.2 GB",
|
||||
age: "3 days",
|
||||
category: "Anime",
|
||||
downloads: 780,
|
||||
metadata: {
|
||||
description: "A visually stunning anime film.",
|
||||
groups: ["alt.binaries.anime"],
|
||||
files: ["anime.movie.mkv"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "Great Music Album",
|
||||
size: "300 MB",
|
||||
age: "10 days",
|
||||
category: "Music",
|
||||
downloads: 50,
|
||||
metadata: {
|
||||
description: "An album by a popular artist.",
|
||||
groups: ["alt.binaries.music.mp3"],
|
||||
files: ["track01.mp3", "track02.mp3", "cover.jpg"],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
||||
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
priority
|
||||
/>
|
||||
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
|
||||
<li className="mb-2">
|
||||
Get started by editing{" "}
|
||||
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
|
||||
app/page.tsx
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li>Save and see your changes instantly.</li>
|
||||
</ol>
|
||||
|
||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||
<a
|
||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Deploy now
|
||||
</a>
|
||||
<a
|
||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read our docs
|
||||
</a>
|
||||
<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">NZB Indexer</h1>
|
||||
<div className="space-x-2">
|
||||
<Link href="/news" passHref>
|
||||
<Button variant="ghost">News</Button>
|
||||
</Link>
|
||||
<Link href="/upgrade" passHref>
|
||||
<Button variant="ghost">Upgrade</Button>
|
||||
</Link>
|
||||
<Link href="/settings" passHref>
|
||||
<Button variant="ghost">Settings</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
</div>
|
||||
|
||||
{/* Featured Carousel Section */}
|
||||
<div className="space-y-4">
|
||||
<h2
|
||||
className="text-2xl font-semibold tracking-tight text-primary drop-shadow-[0_2px_3px_hsl(var(--primary)/0.5)]"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/file.svg"
|
||||
alt="File icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Learn
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
Newest Uploads
|
||||
</h2>
|
||||
<Carousel
|
||||
className="w-full"
|
||||
opts={{ loop: true }}
|
||||
plugins={[autoplayPlugin.current]}
|
||||
onMouseEnter={autoplayPlugin.current.stop}
|
||||
onMouseLeave={autoplayPlugin.current.reset}
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/window.svg"
|
||||
alt="Window icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Examples
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/globe.svg"
|
||||
alt="Globe icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Go to nextjs.org →
|
||||
</a>
|
||||
</footer>
|
||||
<CarouselContent>
|
||||
{mockFeaturedData.map((featuredItem) => (
|
||||
<CarouselItem key={featuredItem.id}>
|
||||
<Card className="overflow-hidden">
|
||||
<CardContent className="flex flex-col md:flex-row items-center p-0">
|
||||
<div className="w-full md:w-1/3 h-64 md:h-auto">
|
||||
<PlaceholderCarouselImage text={featuredItem.title} />
|
||||
</div>
|
||||
<div className="p-6 space-y-3 flex-1">
|
||||
<h3 className="text-xl font-semibold">{featuredItem.title}</h3>
|
||||
<p className="text-sm text-muted-foreground">{featuredItem.description}</p>
|
||||
<div className="flex justify-between items-center pt-2">
|
||||
<span className="text-sm font-medium text-primary">{featuredItem.rating}</span>
|
||||
<Button variant="secondary" size="sm">View Details</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
</Carousel>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="All" className="w-full">
|
||||
<TabsList className="mb-4">
|
||||
{categories.map((category) => (
|
||||
<TabsTrigger key={category} value={category}>
|
||||
{category}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</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>
|
||||
</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>
|
||||
))}
|
||||
</Tabs>
|
||||
|
||||
{/* Dialog for NZB Info - controlled by selectedItem state */}
|
||||
{selectedItem && (
|
||||
<Dialog open={!!selectedItem} onOpenChange={(isOpen) => !isOpen && setSelectedItem(null)}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{selectedItem.name}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Category: {selectedItem.category} | Size: {selectedItem.size} | Age: {selectedItem.age} | Downloads: {selectedItem.downloads.toLocaleString()}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4 space-y-2">
|
||||
<h4 className="font-semibold">Description:</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{selectedItem.metadata.description}
|
||||
</p>
|
||||
<h4 className="font-semibold">Groups:</h4>
|
||||
<ul className="list-disc list-inside text-sm text-muted-foreground">
|
||||
{selectedItem.metadata.groups.map((group) => (
|
||||
<li key={group}>{group}</li>
|
||||
))}
|
||||
</ul>
|
||||
<h4 className="font-semibold">Files:</h4>
|
||||
<ul className="list-disc list-inside text-sm text-muted-foreground">
|
||||
{selectedItem.metadata.files.map((file) => (
|
||||
<li key={file}>{file}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<DialogFooter className="sm:justify-start">
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="secondary">
|
||||
Close
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
97
app/settings/page.tsx
Normal file
97
app/settings/page.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function SettingsPage() {
|
||||
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="space-x-2">
|
||||
<Link href="/news" passHref>
|
||||
<Button variant="ghost">News</Button>
|
||||
</Link>
|
||||
<Link href="/upgrade" passHref>
|
||||
<Button variant="ghost">Upgrade</Button>
|
||||
</Link>
|
||||
<Link href="/" passHref>
|
||||
<Button variant="outline">Back to Indexer</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<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="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>
|
||||
);
|
||||
}
|
||||
108
app/upgrade/page.tsx
Normal file
108
app/upgrade/page.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import Link from "next/link";
|
||||
|
||||
// Placeholder SVG for QR Code
|
||||
const PlaceholderQrCode = () => (
|
||||
<svg width="128" height="128" viewBox="0 0 100 100" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="100" height="100" fill="hsl(var(--muted))"/>
|
||||
<rect x="10" y="10" width="20" height="20" fill="hsl(var(--foreground))"/>
|
||||
<rect x="40" y="10" width="20" height="20" fill="hsl(var(--foreground))"/>
|
||||
<rect x="70" y="10" width="20" height="20" fill="hsl(var(--foreground))"/>
|
||||
<rect x="10" y="40" width="20" height="20" fill="hsl(var(--foreground))"/>
|
||||
<rect x="70" y="40" width="20" height="20" fill="hsl(var(--foreground))"/>
|
||||
<rect x="10" y="70" width="20" height="20" fill="hsl(var(--foreground))"/>
|
||||
<rect x="40" y="70" width="20" height="20" fill="hsl(var(--foreground))"/>
|
||||
<rect x="70" y="70" width="20" height="20" fill="hsl(var(--foreground))"/>
|
||||
<rect x="50" y="50" width="10" height="10" fill="hsl(var(--foreground))"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function UpgradePage() {
|
||||
const xmrAddress = "4YOURXMRADDRESSXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
|
||||
const ltcAddress = "LYOURLTCADDRESSXXXXXXXXXXXXXXXXXXXXXXX";
|
||||
|
||||
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="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>
|
||||
</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.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 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.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
components.json
Normal file
21
components.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
57
components/ui/button.tsx
Normal file
57
components/ui/button.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
76
components/ui/card.tsx
Normal file
76
components/ui/card.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-xl border bg-card text-card-foreground shadow",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
262
components/ui/carousel.tsx
Normal file
262
components/ui/carousel.tsx
Normal file
@ -0,0 +1,262 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import useEmblaCarousel, {
|
||||
type UseEmblaCarouselType,
|
||||
} from "embla-carousel-react"
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1]
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
||||
type CarouselOptions = UseCarouselParameters[0]
|
||||
type CarouselPlugin = UseCarouselParameters[1]
|
||||
|
||||
type CarouselProps = {
|
||||
opts?: CarouselOptions
|
||||
plugins?: CarouselPlugin
|
||||
orientation?: "horizontal" | "vertical"
|
||||
setApi?: (api: CarouselApi) => void
|
||||
}
|
||||
|
||||
type CarouselContextProps = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
||||
api: ReturnType<typeof useEmblaCarousel>[1]
|
||||
scrollPrev: () => void
|
||||
scrollNext: () => void
|
||||
canScrollPrev: boolean
|
||||
canScrollNext: boolean
|
||||
} & CarouselProps
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useCarousel must be used within a <Carousel />")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
const Carousel = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
orientation = "horizontal",
|
||||
opts,
|
||||
setApi,
|
||||
plugins,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{
|
||||
...opts,
|
||||
axis: orientation === "horizontal" ? "x" : "y",
|
||||
},
|
||||
plugins
|
||||
)
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
||||
|
||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||
if (!api) {
|
||||
return
|
||||
}
|
||||
|
||||
setCanScrollPrev(api.canScrollPrev())
|
||||
setCanScrollNext(api.canScrollNext())
|
||||
}, [])
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev()
|
||||
}, [api])
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext()
|
||||
}, [api])
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
event.preventDefault()
|
||||
scrollPrev()
|
||||
} else if (event.key === "ArrowRight") {
|
||||
event.preventDefault()
|
||||
scrollNext()
|
||||
}
|
||||
},
|
||||
[scrollPrev, scrollNext]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) {
|
||||
return
|
||||
}
|
||||
|
||||
setApi(api)
|
||||
}, [api, setApi])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) {
|
||||
return
|
||||
}
|
||||
|
||||
onSelect(api)
|
||||
api.on("reInit", onSelect)
|
||||
api.on("select", onSelect)
|
||||
|
||||
return () => {
|
||||
api?.off("select", onSelect)
|
||||
}
|
||||
}, [api, onSelect])
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
carouselRef,
|
||||
api: api,
|
||||
opts,
|
||||
orientation:
|
||||
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn("relative", className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
)
|
||||
}
|
||||
)
|
||||
Carousel.displayName = "Carousel"
|
||||
|
||||
const CarouselContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { carouselRef, orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div ref={carouselRef} className="overflow-hidden">
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex",
|
||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
CarouselContent.displayName = "CarouselContent"
|
||||
|
||||
const CarouselItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
className={cn(
|
||||
"min-w-0 shrink-0 grow-0 basis-full",
|
||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
CarouselItem.displayName = "CarouselItem"
|
||||
|
||||
const CarouselPrevious = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute h-8 w-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "-left-12 top-1/2 -translate-y-1/2"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
{...props}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
CarouselPrevious.displayName = "CarouselPrevious"
|
||||
|
||||
const CarouselNext = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute h-8 w-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "-right-12 top-1/2 -translate-y-1/2"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
{...props}
|
||||
>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
CarouselNext.displayName = "CarouselNext"
|
||||
|
||||
export {
|
||||
type CarouselApi,
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselPrevious,
|
||||
CarouselNext,
|
||||
}
|
||||
122
components/ui/dialog.tsx
Normal file
122
components/ui/dialog.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
||||
22
components/ui/input.tsx
Normal file
22
components/ui/input.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
120
components/ui/table.tsx
Normal file
120
components/ui/table.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
Table.displayName = "Table"
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = "TableHeader"
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableBody.displayName = "TableBody"
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableFooter.displayName = "TableFooter"
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableRow.displayName = "TableRow"
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHead.displayName = "TableHead"
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCell.displayName = "TableCell"
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCaption.displayName = "TableCaption"
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
55
components/ui/tabs.tsx
Normal file
55
components/ui/tabs.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Tabs = TabsPrimitive.Root
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
6
lib/utils.ts
Normal file
6
lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
786
package-lock.json
generated
786
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@ -9,19 +9,29 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-tabs": "^1.1.12",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"embla-carousel-autoplay": "^8.6.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"lucide-react": "^0.511.0",
|
||||
"next": "15.1.8",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"next": "15.1.8"
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.8",
|
||||
"@eslint/eslintrc": "^3"
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,18 +1,62 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
export default {
|
||||
content: [
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: "var(--background)",
|
||||
foreground: "var(--foreground)",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))'
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))'
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))'
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))'
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))'
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))'
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))'
|
||||
},
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
chart: {
|
||||
'1': 'hsl(var(--chart-1))',
|
||||
'2': 'hsl(var(--chart-2))',
|
||||
'3': 'hsl(var(--chart-3))',
|
||||
'4': 'hsl(var(--chart-4))',
|
||||
'5': 'hsl(var(--chart-5))'
|
||||
}
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [],
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
} satisfies Config;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user