Format yang diterima: JPEG, PNG, WebP, SVG (maks 5MB)
@@ -114,10 +130,16 @@ export function BrandLogoForm({ brand }: BrandLogoFormProps) {
-
diff --git a/src/app/manage/nfts/[id]/page.tsx b/src/app/manage/nfts/[id]/page.tsx
new file mode 100644
index 0000000..3b55391
--- /dev/null
+++ b/src/app/manage/nfts/[id]/page.tsx
@@ -0,0 +1,287 @@
+import { notFound } from 'next/navigation';
+import Link from 'next/link';
+import { getNFTById } from '@/lib/actions/nfts';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import {
+ ArrowLeft,
+ ExternalLink,
+ Copy,
+ Sparkles,
+ Tag,
+ Package,
+ Building2,
+ Wallet,
+ Hash,
+ Calendar,
+ Image as ImageIcon,
+} from 'lucide-react';
+import {
+ getTxExplorerUrl,
+ getAddressExplorerUrl,
+ getNFTExplorerUrl,
+ formatAddress,
+} from '@/lib/constants';
+
+interface NFTDetailPageProps {
+ params: Promise<{ id: string }>;
+}
+
+export default async function NFTDetailPage({ params }: NFTDetailPageProps) {
+ const { id } = await params;
+ const nftId = parseInt(id, 10);
+
+ if (isNaN(nftId)) {
+ notFound();
+ }
+
+ const nft = await getNFTById(nftId);
+
+ if (!nft) {
+ notFound();
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
+
+
+
+ NFT #{nft.tokenId}
+
+
Detail NFT Collectible
+
+
+
+
+ {/* NFT Image */}
+
+
+
+ {nft.imageUrl ? (
+

+ ) : (
+
+ )}
+
+
+
+
+ {/* NFT Details */}
+
+ {/* Basic Info */}
+
+
+ Informasi NFT
+
+
+
+
+
+ Token ID
+
+ #{nft.tokenId}
+
+
+
+
+
+ Tanggal Mint
+
+
+ {new Date(nft.createdAt).toLocaleDateString('id-ID', {
+ day: 'numeric',
+ month: 'long',
+ year: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ })}
+
+
+
+
+
+
+
+ {/* Tag Info */}
+
+
+
+
+ Tag Information
+
+
+
+
+ Tag Code
+
+ {nft.tag.code}
+
+
+
+
+
+
+ {/* Product & Brand Info */}
+ {(nft.product || nft.brand) && (
+
+
+
+
+ Product & Brand
+
+
+
+ {nft.product && (
+
+ Product
+ {nft.product.name}
+
+ )}
+ {nft.brand && (
+
+
+
+ Brand
+
+
+ {nft.brand.logoUrl && (
+

+ )}
+
{nft.brand.name}
+
+
+ )}
+
+
+ )}
+
+
+
+ {/* Blockchain Info */}
+
+
+ Blockchain Information
+
+
+
+ {/* Mint Transaction */}
+
+
+ {/* Transfer Transaction */}
+
+
Transfer Transaction
+ {nft.transferTxHash ? (
+
+ {nft.transferTxHash}
+
+
+ ) : (
+
+ Same as mint (minted directly to owner)
+
+ )}
+
+
+
+ {/* Links */}
+
+
+
+
+ );
+}
diff --git a/src/app/manage/nfts/page.tsx b/src/app/manage/nfts/page.tsx
new file mode 100644
index 0000000..db72557
--- /dev/null
+++ b/src/app/manage/nfts/page.tsx
@@ -0,0 +1,228 @@
+import { Suspense } from 'react';
+import { getNFTs, getNFTStats } from '@/lib/actions/nfts';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table';
+import { Badge } from '@/components/ui/badge';
+import { Sparkles, TrendingUp, Calendar, ImageIcon } from 'lucide-react';
+import Link from 'next/link';
+import {
+ formatAddress,
+ getTxExplorerUrl,
+ getAddressExplorerUrl,
+} from '@/lib/constants';
+
+async function NFTStatsCards() {
+ const stats = await getNFTStats();
+
+ return (
+
+
+
+ Total NFT
+
+
+
+ {stats.totalMinted}
+
+ NFT yang sudah di-mint
+
+
+
+
+
+
+ Hari Ini
+
+
+
+ {stats.mintedToday}
+ NFT baru hari ini
+
+
+
+
+
+ Minggu Ini
+
+
+
+ {stats.mintedThisWeek}
+ NFT minggu ini
+
+
+
+
+
+ Bulan Ini
+
+
+
+ {stats.mintedThisMonth}
+ NFT bulan ini
+
+
+
+ );
+}
+
+async function NFTTable() {
+ const { nfts, total } = await getNFTs({ page: 1, limit: 20 });
+
+ if (nfts.length === 0) {
+ return (
+
+
+
+ Belum Ada NFT
+
+ NFT akan muncul di sini ketika pengguna melakukan klaim first-hand.
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ NFT Collectibles
+ {total} total
+
+
+
+
+
+
+ NFT
+ Token ID
+ Tag
+ Product
+ Brand
+ Owner
+ Tanggal
+ Actions
+
+
+
+ {nfts.map((nft) => (
+
+
+
+ {nft.imageUrl ? (
+

+ ) : (
+
+
+
+ )}
+
+
+ #{nft.tokenId}
+
+
+ {nft.tag.code}
+
+
+ {nft.product?.name || '-'}
+ {nft.brand?.name || '-'}
+
+
+ {formatAddress(nft.ownerAddress)}
+
+
+
+ {new Date(nft.createdAt).toLocaleDateString('id-ID', {
+ day: 'numeric',
+ month: 'short',
+ year: 'numeric',
+ })}
+
+
+
+
+ Detail
+
+ {nft.mintTxHash && (
+
+ Tx
+
+ )}
+
+
+
+ ))}
+
+
+
+
+ );
+}
+
+export default function NFTsPage() {
+ return (
+
+
+
NFT Collectibles
+
+ Monitor dan kelola NFT collectible yang sudah di-mint
+
+
+
+
+ {[...Array(4)].map((_, i) => (
+
+
+
+
+
+ ))}
+
+ }
+ >
+
+
+
+
+
+
+
+
+ }
+ >
+
+
+
+ );
+}
diff --git a/src/app/manage/products/page.tsx b/src/app/manage/products/page.tsx
index a8c1ea5..105d75a 100644
--- a/src/app/manage/products/page.tsx
+++ b/src/app/manage/products/page.tsx
@@ -1,17 +1,50 @@
import { auth } from '@/lib/auth';
import { redirect } from 'next/navigation';
-import { getProducts } from '@/lib/actions/products';
+import { getProducts, getProductStats } from '@/lib/actions/products';
import { ProductsTable } from './products-table';
import { ProductsHeader } from './products-header';
+import { ProductStatsCards } from './product-stats-cards';
import { Suspense } from 'react';
import { TableSkeleton } from '../table-skeleton';
+import { Skeleton } from '@/components/ui/skeleton';
+import { Pagination } from '@/components/ui/pagination';
-async function ProductsTableWrapper({ isAdmin }: { isAdmin: boolean }) {
- const { products } = await getProducts(1, 50);
- return
;
+async function ProductsTableWrapper({
+ isAdmin,
+ page,
+}: {
+ isAdmin: boolean;
+ page: number;
+}) {
+ const { products, pagination } = await getProducts(page, 10);
+ return (
+ <>
+
+ >
+ );
+}
+
+async function ProductStatsWrapper() {
+ const stats = await getProductStats();
+ return
;
}
-export default async function ProductsPage() {
+function StatsCardsSkeleton() {
+ return (
+
+ );
+}
+
+export default async function ProductsPage({
+ searchParams,
+}: {
+ searchParams: Promise<{ page?: string }>;
+}) {
const session = await auth();
if (!session?.user) {
@@ -19,13 +52,22 @@ export default async function ProductsPage() {
}
const isAdmin = session.user.role === 'admin';
+ const params = await searchParams;
+ const page = parseInt(params.page || '1', 10);
return (
diff --git a/src/app/manage/products/product-stats-cards.tsx b/src/app/manage/products/product-stats-cards.tsx
new file mode 100644
index 0000000..b608dec
--- /dev/null
+++ b/src/app/manage/products/product-stats-cards.tsx
@@ -0,0 +1,101 @@
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card';
+import { Package, CheckCircle, Tag, PackageX } from 'lucide-react';
+
+type ProductStatsCardsProps = {
+ stats: {
+ totalProducts: number;
+ activeProducts: number;
+ productsWithTags: number;
+ productsWithoutTags: number;
+ };
+};
+
+export function ProductStatsCards({ stats }: ProductStatsCardsProps) {
+ return (
+
+ );
+}
diff --git a/src/app/manage/profile/avatar-form.tsx b/src/app/manage/profile/avatar-form.tsx
index 5bd4f49..6206f44 100644
--- a/src/app/manage/profile/avatar-form.tsx
+++ b/src/app/manage/profile/avatar-form.tsx
@@ -17,6 +17,7 @@ import {
removeAvatar,
type ProfileFormState,
} from '@/lib/actions/profile';
+import { Camera } from 'lucide-react';
type AvatarFormProps = {
user: {
@@ -63,29 +64,44 @@ export function AvatarForm({ user }: AvatarFormProps) {
};
return (
-