diff --git a/figma/feature/-design-search-digital-assets-lightmode.txt b/figma/feature/-design-search-digital-assets-lightmode.txt deleted file mode 100644 index d7768a5..0000000 --- a/figma/feature/-design-search-digital-assets-lightmode.txt +++ /dev/null @@ -1,9 +0,0 @@ -Figma Design Issues Worked on are: - -Figma (Light Mode): Global Certificate Search Results (List View) -Figma (Light Mode): Global Certificate Search Results (Grid View) -Figma (Light Mode): Digital Products / Assets (Table View) -Figma (Light Mode): Digital Products / Assets (Grid View) - - -https://www.figma.com/design/2kP82C0Z8z0X3CncLQmfhB/Stellar-Project-Fig?node-id=227-2&p=f&t=LhlzEJ7fINwVaUuh-0 diff --git a/figma/feature/design-billing-subscription-management-lightmode.txt b/figma/feature/design-billing-subscription-management-lightmode.txt deleted file mode 100644 index a41f2a7..0000000 --- a/figma/feature/design-billing-subscription-management-lightmode.txt +++ /dev/null @@ -1,9 +0,0 @@ -This is my design and link to it - -https://www.figma.com/design/2kP82C0Z8z0X3CncLQmfhB/Stellar-Project-Fig?node-id=227-2&p=f&t=LhlzEJ7fINwVaUuh-0 - - -Figma (Light Mode): Billing Dashboard (Table View) -Figma (Light Mode): Billing Invoices (Table View) -Figma (Light Mode): Subscription Plan Manager -Figma (Light Mode): Gas Fee Estimator UI diff --git a/figma/feature/design-pricing-payment-experience-lightmode.txt b/figma/feature/design-pricing-payment-experience-lightmode.txt deleted file mode 100644 index 67ba73f..0000000 --- a/figma/feature/design-pricing-payment-experience-lightmode.txt +++ /dev/null @@ -1,10 +0,0 @@ -The Design Issue include - -Figma (Light Mode): Pricing Tiers Column Card -Figma (Light Mode): Pricing Comparison Table -Figma (Light Mode): Checkout Flow - Pay with XLM (Freighter) -Figma (Light Mode): Checkout Flow - Pay with Fiat (Credit Card) - - -https://www.figma.com/design/2kP82C0Z8z0X3CncLQmfhB/Stellar-Project-Fig?node-id=227-2&p=f&t=LhlzEJ7fINwVaUuh-0 - diff --git a/figma/feature/design-search-discovery-experience-lightmode.txt b/figma/feature/design-search-discovery-experience-lightmode.txt deleted file mode 100644 index 5169b5d..0000000 --- a/figma/feature/design-search-discovery-experience-lightmode.txt +++ /dev/null @@ -1,10 +0,0 @@ -These are the Figma Light Mode Designs Issues and reference link - -Figma (Light Mode): Advanced Search Filters Modal -Figma (Light Mode): Creator Directory (Search View) -Figma (Light Mode): Search No-Results -Figma (Light Mode): Search Empty Vault State - - -https://www.figma.com/design/2kP82C0Z8z0X3CncLQmfhB/Stellar-Project-Fig?node-id=227-2&p=f&t=LhlzEJ7fINwVaUuh-0 - diff --git a/frontend/app/dashboard/assets/components/AssetGrid.test.tsx b/frontend/app/dashboard/assets/components/AssetGrid.test.tsx deleted file mode 100644 index 8bf5a2f..0000000 --- a/frontend/app/dashboard/assets/components/AssetGrid.test.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; -import AssetGrid, { Asset } from './AssetGrid'; - -const mockAssets: Asset[] = [ - { id: '1', title: 'C', createdAt: '2023-01-03', type: 'image', thumbnailUrl: '', status: 'verified' }, - { id: '2', title: 'A', createdAt: '2023-01-01', type: 'image', thumbnailUrl: '', status: 'verified' }, - { id: '3', title: 'B', createdAt: '2023-01-02', type: 'image', thumbnailUrl: '', status: 'verified' }, - { id: '4', title: 'D', createdAt: '2023-01-04', type: 'image', thumbnailUrl: '', status: 'verified' }, - { id: '5', title: 'E', createdAt: '2023-01-05', type: 'image', thumbnailUrl: '', status: 'verified' }, - { id: '6', title: 'F', createdAt: '2023-01-06', type: 'image', thumbnailUrl: '', status: 'verified' }, - { id: '7', title: 'G', createdAt: '2023-01-07', type: 'image', thumbnailUrl: '', status: 'verified' }, - { id: '8', title: 'H', createdAt: '2023-01-08', type: 'image', thumbnailUrl: '', status: 'verified' }, - { id: '9', title: 'I', createdAt: '2023-01-09', type: 'image', thumbnailUrl: '', status: 'verified' }, -]; - -describe('AssetGrid', () => { - it('renders assets sorted by newest first by default', () => { - render(); - const assetTitles = screen.getAllByRole('heading', { level: 3 }).map(h => h.textContent); - expect(assetTitles[0]).toBe('I'); - }); - - it('sorts assets by title alphabetically', () => { - render(); - const sortSelect = screen.getByRole('combobox'); - fireEvent.change(sortSelect, { target: { value: 'title-asc' } }); - const assetTitles = screen.getAllByRole('heading', { level: 3 }).map(h => h.textContent); - expect(assetTitles[0]).toBe('A'); - }); - - it('paginates through assets', () => { - render(); - expect(screen.getByText('Page 1 of 2')).toBeInTheDocument(); - expect(screen.getAllByRole('article').length).toBe(8); - - const nextButton = screen.getByText('Next'); - fireEvent.click(nextButton); - - expect(screen.getByText('Page 2 of 2')).toBeInTheDocument(); - expect(screen.getAllByRole('article').length).toBe(1); - - const prevButton = screen.getByText('Previous'); - fireEvent.click(prevButton); - - expect(screen.getByText('Page 1 of 2')).toBeInTheDocument(); - expect(screen.getAllByRole('article').length).toBe(8); - }); - - it('renders empty state', () => { - render(); - expect(screen.getByText('No verified assets yet')).toBeInTheDocument(); - }); - - it('renders loading skeleton', () => { - render(); - expect(screen.getAllByRole('heading', { level: 3 })[0]).toBeInTheDocument(); - }); -}); diff --git a/frontend/app/dashboard/assets/components/AssetGrid.tsx b/frontend/app/dashboard/assets/components/AssetGrid.tsx index 94c0779..ed8444d 100644 --- a/frontend/app/dashboard/assets/components/AssetGrid.tsx +++ b/frontend/app/dashboard/assets/components/AssetGrid.tsx @@ -1,12 +1,21 @@ "use client"; -import React, { useState, useMemo } from "react"; +import { useEffect, useState } from "react"; import Image from "next/image"; +/** + * Gallery / grid view for a user's verified digital products (assets). + * + * Renders a responsive grid of thumbnails for verified images and videos with + * a status badge, media-type indicator and capture date. Data is currently + * mocked; once the asset service layer is available this component can source + * its items from `fetchAssets` without changing the presentation. + */ + type AssetType = "image" | "video"; type AssetStatus = "verified" | "pending" | "revoked"; -export interface Asset { +interface Asset { id: string; title: string; type: AssetType; @@ -15,6 +24,81 @@ export interface Asset { createdAt: string; } +const MOCK_ASSETS: Asset[] = [ + { + id: "asset-001", + title: "Sunset Over the Serengeti", + type: "image", + thumbnailUrl: + "https://images.unsplash.com/photo-1516426122078-c23e76319801?w=600&h=450&fit=crop", + status: "verified", + createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2).toISOString(), + }, + { + id: "asset-002", + title: "Product Launch Teaser", + type: "video", + thumbnailUrl: + "https://images.unsplash.com/photo-1485846234645-a62644f84728?w=600&h=450&fit=crop", + status: "verified", + createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5).toISOString(), + }, + { + id: "asset-003", + title: "Golden Hour Portrait Series", + type: "image", + thumbnailUrl: + "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=600&h=450&fit=crop", + status: "pending", + createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 9).toISOString(), + }, + { + id: "asset-004", + title: "Aerial City Flythrough", + type: "video", + thumbnailUrl: + "https://images.unsplash.com/photo-1449824913935-59a10b8d2000?w=600&h=450&fit=crop", + status: "verified", + createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 14).toISOString(), + }, + { + id: "asset-005", + title: "Studio Still Life", + type: "image", + thumbnailUrl: + "https://images.unsplash.com/photo-1503602642458-232111445657?w=600&h=450&fit=crop", + status: "revoked", + createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 21).toISOString(), + }, + { + id: "asset-006", + title: "Mountain Timelapse", + type: "video", + thumbnailUrl: + "https://images.unsplash.com/photo-1454496522488-7a8e488e8606?w=600&h=450&fit=crop", + status: "verified", + createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 28).toISOString(), + }, + { + id: "asset-007", + title: "Macro Botanicals", + type: "image", + thumbnailUrl: + "https://images.unsplash.com/photo-1462530260150-162092dbf011?w=600&h=450&fit=crop", + status: "verified", + createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 33).toISOString(), + }, + { + id: "asset-008", + title: "Neon Night Reel", + type: "video", + thumbnailUrl: + "https://images.unsplash.com/photo-1492684223066-81342ee5ff30?w=600&h=450&fit=crop", + status: "pending", + createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 40).toISOString(), + }, +]; + const STATUS_BADGE: Record = { verified: "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400", @@ -31,102 +115,6 @@ function formatDate(iso: string): string { }); } -// ... (VideoIcon, AssetGridSkeleton, EmptyState, AssetCard components remain the same) - -interface AssetGridProps { - assets: Asset[]; - isLoading?: boolean; -} - -type SortKey = "title" | "createdAt"; - -export default function AssetGrid({ assets, isLoading = false }: AssetGridProps) { - const [sortKey, setSortKey] = useState("createdAt"); - const [sortDirection, setSortDirection] = useState<"asc" | "dsc">("dsc"); - const [currentPage, setCurrentPage] = useState(1); - const itemsPerPage = 8; - - const sortedAssets = useMemo(() => { - return [...assets].sort((a, b) => { - const aValue = a[sortKey]; - const bValue = b[sortKey]; - if (aValue < bValue) return sortDirection === "asc" ? -1 : 1; - if (aValue > bValue) return sortDirection === "asc" ? 1 : -1; - return 0; - }); - }, [assets, sortKey, sortDirection]); - - const paginatedAssets = useMemo(() => { - const startIndex = (currentPage - 1) * itemsPerPage; - return sortedAssets.slice(startIndex, startIndex + itemsPerPage); - }, [sortedAssets, currentPage]); - - const totalPages = Math.ceil(sortedAssets.length / itemsPerPage); - - const handleSortChange = (e: React.ChangeEvent) => { - const value = e.target.value; - const [key, direction] = value.split('-') as [SortKey, "asc" | "dsc"]; - setSortKey(key); - setSortDirection(direction); - }; - - return ( -
-
-

- Assets -

-
- - {assets.length} asset{assets.length !== 1 ? "s" : ""} - - -
-
- - {isLoading ? ( - - ) : assets.length === 0 ? ( - - ) : ( - <> -
- {paginatedAssets.map((asset) => ( - - ))} -
- {totalPages > 1 && ( -
- - - Page {currentPage} of {totalPages} - - -
- )} - - )} -
- ); -} - function VideoIcon({ className }: { className?: string }) { return ( (null); + const [items, setItems] = useState(assets ?? null); const [isLoading, setIsLoading] = useState(!assets); useEffect(() => { - if (assets) return; + if (assets) { + setItems(assets); + setIsLoading(false); + return; + } let cancelled = false; + setIsLoading(true); // Simulate asset retrieval until the service layer is wired in. const timer = setTimeout(() => { if (!cancelled) { - setMockItems(MOCK_ASSETS); + setItems(MOCK_ASSETS); setIsLoading(false); } }, 400); @@ -282,8 +275,6 @@ export default function AssetGrid({ assets }: AssetGridProps) { }; }, [assets]); - const items = assets ?? mockItems; - return (
diff --git a/frontend/app/dashboard/assets/components/AssetTable.test.tsx b/frontend/app/dashboard/assets/components/AssetTable.test.tsx index 381493a..45e0c39 100644 --- a/frontend/app/dashboard/assets/components/AssetTable.test.tsx +++ b/frontend/app/dashboard/assets/components/AssetTable.test.tsx @@ -1,29 +1,35 @@ import React from "react"; -import { render, screen, fireEvent } from "@testing-library/react"; +import { render, screen } from "@testing-library/react"; import AssetTable from "./AssetTable"; -import { MOCK_ASSETS, DigitalAsset } from "@/services/assetsMock"; - -const mockAssets: DigitalAsset[] = [ - ...MOCK_ASSETS, - { id: 'asset-9', name: 'Zebra Stripes', type: 'Image', owner: 'user-1', contentHash: 'hash9', kmsEncryptionStatus: 'encrypted', status: 'verified', sizeBytes: 1024, verifiedAt: '2023-01-09' }, - { id: 'asset-10', name: 'Apple Logo', type: 'Image', owner: 'user-1', contentHash: 'hash10', kmsEncryptionStatus: 'encrypted', status: 'verified', sizeBytes: 2048, verifiedAt: '2023-01-10' }, - { id: 'asset-11', name: 'Google Banner', type: 'Image', owner: 'user-1', contentHash: 'hash11', kmsEncryptionStatus: 'encrypted', status: 'verified', sizeBytes: 4096, verifiedAt: '2023-01-11' }, - { id: 'asset-12', name: 'Microsoft Word Doc', type: 'Document', owner: 'user-1', contentHash: 'hash12', kmsEncryptionStatus: 'unencrypted', status: 'pending', sizeBytes: 8192, verifiedAt: '2023-01-12' }, -]; +import { MOCK_ASSETS } from "@/services/assetsMock"; describe("AssetTable", () => { it("renders a header for every column", () => { - render(); - ["Asset", "Type", "Owner", "Content Hash", "KMS Encryption", "Verified At", "Status", "Size"].forEach((heading) => { - const header = screen.getByText(heading); - expect(header).toBeInTheDocument(); - expect(header.tagName).toBe('TH'); + render(); + + [ + "Asset", + "Type", + "Owner", + "Content Hash", + "KMS Encryption", + "Verified At", + "Status", + "Size", + ].forEach((heading) => { + expect( + screen.getByRole("columnheader", { name: heading }) + ).toBeInTheDocument(); }); }); - it("renders a row for every asset on the first page", () => { - render(); - expect(screen.getAllByRole("row")).toHaveLength(11); // 1 header + 10 rows + it("renders a row for every asset with its KMS status and verification info", () => { + render(); + + expect(screen.getAllByRole("row")).toHaveLength(MOCK_ASSETS.length + 1); + expect(screen.getByText("Limited Edition Digital Artwork")).toBeInTheDocument(); + expect(screen.getAllByText(/encrypted/i).length).toBeGreaterThan(0); + expect(screen.getByText(/unencrypted/i)).toBeInTheDocument(); }); it("renders a loading skeleton", () => { @@ -35,45 +41,4 @@ describe("AssetTable", () => { render(); expect(screen.getByText(/no assets found/i)).toBeInTheDocument(); }); - - it("sorts by asset name when header is clicked", () => { - render(); - const assetHeader = screen.getByText("Asset"); - - // Initial render (unsorted) - let rows = screen.getAllByRole("row"); - expect(rows[1]).toHaveTextContent(mockAssets[0].name); - - // Sort ascending - fireEvent.click(assetHeader); - rows = screen.getAllByRole("row"); - expect(rows[1]).toHaveTextContent("Apple Logo"); - - // Sort descending - fireEvent.click(assetHeader); - rows = screen.getAllByRole("row"); - expect(rows[1]).toHaveTextContent("Zebra Stripes"); - }); - - it("paginates through the assets", () => { - render(); - - // Check initial page - expect(screen.getByText("Page 1 of 2")).toBeInTheDocument(); - expect(screen.getByText(mockAssets[0].name)).toBeInTheDocument(); - expect(screen.queryByText(mockAssets[10].name)).not.toBeInTheDocument(); - expect(screen.getByText("Previous")).toBeDisabled(); - - // Go to next page - fireEvent.click(screen.getByText("Next")); - expect(screen.getByText("Page 2 of 2")).toBeInTheDocument(); - expect(screen.queryByText(mockAssets[0].name)).not.toBeInTheDocument(); - expect(screen.getByText(mockAssets[10].name)).toBeInTheDocument(); - expect(screen.getByText("Next")).toBeDisabled(); - - // Go back to previous page - fireEvent.click(screen.getByText("Previous")); - expect(screen.getByText("Page 1 of 2")).toBeInTheDocument(); - expect(screen.getByText(mockAssets[0].name)).toBeInTheDocument(); - }); }); diff --git a/frontend/app/dashboard/assets/components/AssetTable.tsx b/frontend/app/dashboard/assets/components/AssetTable.tsx index aef519d..1798fd6 100644 --- a/frontend/app/dashboard/assets/components/AssetTable.tsx +++ b/frontend/app/dashboard/assets/components/AssetTable.tsx @@ -1,7 +1,7 @@ "use client"; -import React, { useState, useMemo } from "react"; -import { ShieldCheck, ShieldAlert, ShieldOff, ChevronUp, ChevronDown, ArrowUpDown } from "lucide-react"; +import React from "react"; +import { ShieldCheck, ShieldAlert, ShieldOff } from "lucide-react"; import { cn } from "@/utils/cn"; import { TableSkeleton } from "@/components/ui/Skeleton"; import type { @@ -16,23 +16,15 @@ export interface AssetTableProps { className?: string; } -type SortableColumn = "name" | "type" | "status" | "sizeBytes"; -type SortDirection = "asc" | "dsc"; - -interface SortConfig { - key: SortableColumn; - direction: SortDirection; -} - -const COLUMNS: { key: SortableColumn | string; label: string; sortable: boolean }[] = [ - { key: "name", label: "Asset", sortable: true }, - { key: "type", label: "Type", sortable: true }, - { key: "owner", label: "Owner", sortable: false }, - { key: "contentHash", label: "Content Hash", sortable: false }, - { key: "kmsEncryptionStatus", label: "KMS Encryption", sortable: false }, - { key: "verifiedAt", label: "Verified At", sortable: false }, - { key: "status", label: "Status", sortable: true }, - { key: "sizeBytes", label: "Size", sortable: true }, +const COLUMNS = [ + "Asset", + "Type", + "Owner", + "Content Hash", + "KMS Encryption", + "Verified At", + "Status", + "Size", ]; const STATUS_STYLES: Record = { @@ -145,51 +137,6 @@ export default function AssetTable({ isLoading = false, className, }: AssetTableProps) { - const [sortConfig, setSortConfig] = useState(null); - const [currentPage, setCurrentPage] = useState(1); - const itemsPerPage = 10; - - const sortedAssets = useMemo(() => { - let sortableItems = [...assets]; - if (sortConfig !== null) { - sortableItems.sort((a, b) => { - if (a[sortConfig.key] < b[sortConfig.key]) { - return sortConfig.direction === "asc" ? -1 : 1; - } - if (a[sortConfig.key] > b[sortConfig.key]) { - return sortConfig.direction === "asc" ? 1 : -1; - } - return 0; - }); - } - return sortableItems; - }, [assets, sortConfig]); - - const paginatedAssets = useMemo(() => { - const startIndex = (currentPage - 1) * itemsPerPage; - return sortedAssets.slice(startIndex, startIndex + itemsPerPage); - }, [sortedAssets, currentPage, itemsPerPage]); - - const totalPages = Math.ceil(sortedAssets.length / itemsPerPage); - - const requestSort = (key: SortableColumn) => { - let direction: SortDirection = "asc"; - if (sortConfig && sortConfig.key === key && sortConfig.direction === "asc") { - direction = "dsc"; - } - setSortConfig({ key, direction }); - }; - - const SortIcon = ({ columnKey }: { columnKey: SortableColumn | string }) => { - if (!sortConfig || sortConfig.key !== columnKey) { - return ; - } - if (sortConfig.direction === 'asc') { - return ; - } - return ; - }; - return (
) : ( - <> -
- - - - {COLUMNS.map((column) => ( - - ))} - - - - {paginatedAssets.map((asset) => ( - +
+
- {column.sortable ? ( - - ) : ( - column.label - )} -
+ + + {COLUMNS.map((heading) => ( + ))} - -
+ {heading} +
-
- {totalPages > 1 && ( -
- - - Page {currentPage} of {totalPages} - - -
- )} - + + + + {assets.map((asset) => ( + + ))} + + +
)}
); diff --git a/frontend/app/report-issue/ManifestGeneratorStep.tsx b/frontend/app/report-issue/ManifestGeneratorStep.tsx deleted file mode 100644 index dd78c7f..0000000 --- a/frontend/app/report-issue/ManifestGeneratorStep.tsx +++ /dev/null @@ -1,175 +0,0 @@ -"use client"; - -import React from "react"; -import { useForm, SubmitHandler } from "react-hook-form"; -import { useTheme } from "@/app/context/ThemeContext"; - -interface ManifestFormState { - title: string; - creator: string; - description: string; - issuer: string; -} - -export function ManifestGeneratorStep() { - const { theme } = useTheme(); - const isDark = theme === "dark"; - - const { - register, - handleSubmit, - formState: { errors, isSubmitting }, - } = useForm({ - defaultValues: { - title: "", - creator: "", - description: "", - issuer: "", - }, - }); - - const onSubmit: SubmitHandler = async (data) => { - // Simulate API call - await new Promise((resolve) => setTimeout(resolve, 1500)); - console.log("Form Data Submitted:", data); - // In a real implementation, you would proceed to the next step of the wizard here. - }; - - // Reusable Tailwind CSS classes for form elements - const labelClass = `block text-sm font-semibold mb-2 ${ - isDark ? "text-white" : "text-gray-900" - }`; - const inputBase = `w-full px-4 py-2 rounded-md border transition-colors focus:ring-2 focus:ring-blue-500 focus:border-transparent`; - const inputNormal = isDark - ? "border-gray-600 bg-gray-700 text-white placeholder-gray-400" - : "border-gray-300 bg-white text-gray-900 placeholder-gray-500"; - const inputError = "border-red-500"; - - return ( -
-
-

- Generate Manifest -

-

- Enter the metadata for the digital asset. This information will be - part of the verifiable certificate. -

-
- -
- {/* Title */} -
- - - {errors.title && ( -

- {errors.title.message} -

- )} -
- - {/* Creator */} -
- - - {errors.creator && ( -

- {errors.creator.message} -

- )} -
- - {/* Issuer */} -
- - -
- - {/* Description */} -
- -