diff --git a/DESIGN_TOKENS.md b/DESIGN_TOKENS.md new file mode 100644 index 0000000..12f884d --- /dev/null +++ b/DESIGN_TOKENS.md @@ -0,0 +1,240 @@ +# Aurora - Prism Project Design Token Structure + +This document summarizes the design token structure from the Figma design system for the Aurora - Prism Project. + +## Overview + +The design system is organized into several main categories: Colors, Typography, Spacing, Radius, and Effects. Each category contains semantic tokens that map to specific use cases within the application. + +--- + +## Color Tokens + +### Base Color Palettes + +The design system includes comprehensive color palettes with scales from 50 (lightest) to 950 (darkest): + +#### Primary Color Palettes +- **Red**: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950 +- **Yellow**: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950 +- **Green**: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950 +- **Violet**: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950 +- **Greys**: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950 + +#### Gradient Tokens +- **Gradient - Fill**: Primary gradient fill +- **Gradient - Hover**: Hover state gradient +- **Gradient - Stroke**: Stroke gradient +- **Gradient - Blue**: Blue gradient variant + +### Semantic Color Tokens + +#### Backgrounds +- `Background / Elevated`: Elevated background surface +- `Background / Default`: Default background surface + +#### Surfaces +- `Surface / Subtle`: Subtle surface variant +- `Surface / Default`: Default surface +- `Surface / Disabled`: Disabled state surface +- `Surface / Gradient-Primary`: Primary gradient surface +- `Surface / Gradient-Secondary`: Secondary gradient surface +- `Surface / Elevated lv.1`: First level elevated surface +- `Surface / Elevated lv.2`: Second level elevated surface + +#### Borders +- `Border / Default`: Default border color +- `Border / Disabled`: Disabled border color +- `Border / Darker`: Darker border variant +- `Border / Active`: Active state border +- `Border / Secondary`: Secondary border +- `Border / tertiary`: Tertiary border + +#### Text/Icon/Symbols +- `Text / Title`: Title text color +- `Text / Body`: Body text color +- `Text / Subtitle`: Subtitle text color +- `Text / Icon / primary`: Primary icon/text color +- `Text / Icon / darker`: Darker icon/text variant +- `Text / Icon / white`: White icon/text variant + +#### Semantics +- `Semantic / primary`: Primary semantic color +- `Semantic / darker`: Darker semantic variant +- `Semantic / white`: White semantic variant + +--- + +## Typography Tokens + +### Type Scale + +The typography system includes the following text styles with multiple weight variants: + +#### Headings +- **H1**: Bold, Regular, Light + - Size: 61px (3.813rem) +- **H2**: Bold, Regular, Light + - Size: 49px (3.063rem) +- **H3**: Bold, Regular, Light + - Size: 39px (2.438rem) +- **H4**: Bold, Regular, Light + - Size: 31px (1.938rem) +- **H5**: Bold, Regular, Light + - Size: 24px (1.563rem) + +#### Other Text Styles +- **Headline**: Bold, Regular, Light + - Size: 20px (1.250rem) +- **Body**: Bold, Regular, Light + - Size: 16px (1.000rem) +- **Subtitle**: Bold, Regular + - Size: 13px (0.813rem) +- **Caption**: Bold, Regular + - Size: 10px (0.625rem) +- **Footnote**: Bold, Regular + - Size: 8px (0.500rem) + +### Font Weights +- **Bold**: Heavy weight for emphasis +- **Regular**: Standard weight for body text +- **Light**: Light weight for subtle text + +--- + +## Spacing & Number Scale + +The spacing system uses a consistent numerical scale: + +### Spacing Values +- `2` (2px) +- `4` (4px) +- `6` (6px) +- `8` (8px) +- `12` (12px) +- `16` (16px) +- `20` (20px) +- `24` (24px) +- `32` (32px) +- `40` (40px) +- `999` (999px - likely for full-width/max values) +- `Full` (100% width) + +--- + +## Radius Tokens + +Border radius values for rounded corners: + +- **S** (Small): Minimal rounding +- **M** (Medium): Standard rounding +- **L** (Large): Large rounding +- **XL** (Extra Large): Extra large rounding +- **Full**: Fully rounded (circular/pill shape) + +--- + +## Effects Tokens + +### Blur +- **Background Blur**: Blur effect for background elements + +--- + +## Icon Tokens + +### Navigation & UI Icons + +These icons should be used across all screens for consistency. Each icon has default (white), green (active), and dark variants where available. + +- **Dashboard** + - Default: https://www.figma.com/api/mcp/asset/7ea22fe7-76f3-4dcd-be2a-27584735bf3e + - Green: https://www.figma.com/api/mcp/asset/46fe45e5-90ae-4730-b5f5-6775c3ea68b7 + - Dark: https://www.figma.com/api/mcp/asset/a4ec5af4-7202-4a2c-af55-4f15dddc4549 +- **Prisms** + - Default: https://www.figma.com/api/mcp/asset/344137cd-61ab-4c91-80d3-746b4b1078ef + - Green: https://www.figma.com/api/mcp/asset/6c56fea7-8bb6-4c02-be2c-fa73a3c6e210 + - Dark: https://www.figma.com/api/mcp/asset/e9890cf3-3675-49e7-8a57-e15013aed9d5 +- **Contacts** + - Default: https://www.figma.com/api/mcp/asset/db895cc0-2873-4f73-a16f-71332b53de60 + - Green: https://www.figma.com/api/mcp/asset/af5bddfe-c935-4178-a465-7b22f4a480c5 + - Dark: https://www.figma.com/api/mcp/asset/425d7d31-b102-4dfd-98bd-6e9f84ab25b0 +- **Notifications** + - Default: https://www.figma.com/api/mcp/asset/9bef6dcd-9734-44fa-b3c8-fda66e0a8bb7 + - Green: https://www.figma.com/api/mcp/asset/7023b275-ef0d-485b-846e-a823595fab0c + - Dark: https://www.figma.com/api/mcp/asset/efde19ab-cd3c-4d42-a31a-827179fc0433 +- **Settings** + - Default: https://www.figma.com/api/mcp/asset/f1c526ef-385e-49c0-95f7-2ee7831d0d1f + - Variant2: https://www.figma.com/api/mcp/asset/6d2b67c0-1f7e-4a36-b5b3-20fb8eaede33 + - Variant3: https://www.figma.com/api/mcp/asset/55e572ed-04e4-47ca-9149-5815e4f6e3e9 +- **Wallet** + - Default: https://www.figma.com/api/mcp/asset/9b443623-e832-4d0e-a38c-850d8be7a35e + - Green: https://www.figma.com/api/mcp/asset/f5bfa230-d75d-46fe-a700-2bab54cd9fee +- **Finance** + - Default: https://www.figma.com/api/mcp/asset/89d6edc7-18cd-4d29-97b3-db4116db9ed0 + - Variant2: https://www.figma.com/api/mcp/asset/22c366a9-5a17-4eb7-9da8-248d0a7a4e76 + - Variant3: https://www.figma.com/api/mcp/asset/6bd0f1ab-4945-47da-bf52-f72129f762d9 +- **Vector** + - Default: https://www.figma.com/api/mcp/asset/0ca23d71-1965-41a9-b1bb-42fc1827f05a + - Variant2: https://www.figma.com/api/mcp/asset/329bbee7-c73b-433d-97e4-dd2a7179ee28 + - Variant3: https://www.figma.com/api/mcp/asset/45f3e1e8-dce4-4de1-904a-9cbda44f996e + +--- + +## Token Organization Structure + +The design tokens are organized hierarchically: + +``` +Design Tokens +├── Colors +│ ├── Base Palettes (Red, Yellow, Green, Violet, Greys) +│ ├── Gradients +│ └── Semantic Tokens +│ ├── Backgrounds +│ ├── Surfaces +│ ├── Borders +│ ├── Text/Icon/Symbols +│ └── Semantics +├── Typography +│ ├── Headings (H1-H5) +│ └── Text Styles (Headline, Body, Subtitle, Caption, Footnote) +├── Spacing +│ └── Number Scale (2, 4, 6, 8, 12, 16, 20, 24, 32, 40, 999, Full) +├── Radius +│ └── Size Variants (S, M, L, XL, Full) +└── Effects + └── Blur +``` + +--- + +## Usage Notes + +1. **Color Tokens**: Use semantic tokens (e.g., `Text / Title`) rather than raw color values to ensure consistency and maintainability. + +2. **Typography**: Each text style has multiple weight variants. Choose the appropriate weight based on hierarchy and emphasis. + +3. **Spacing**: The number scale provides consistent spacing throughout the application. Use these values for padding, margins, and gaps. + +4. **Radius**: Apply radius tokens consistently to maintain visual harmony across rounded elements. + +5. **Gradients**: Use gradient tokens for surfaces and backgrounds that require depth and visual interest. + +--- + +## Implementation Recommendations + +When implementing these tokens in code: + +1. **CSS Variables**: Map tokens to CSS custom properties for easy theming +2. **Type Safety**: Create TypeScript interfaces for token names +3. **Documentation**: Keep this document updated as tokens evolve +4. **Naming Convention**: Follow the semantic naming structure (e.g., `surface-default`, `text-title`) +5. **Accessibility**: Ensure color contrast ratios meet WCAG guidelines when using text tokens + +--- + +*Last updated: Based on Figma file structure analysis* +*Figma File: [Aurora - Prism Project](https://www.figma.com/design/eTUcSLVwAKhRIQxMqBqo8v/Aurora---Prism-Project?node-id=2825-3977)* + diff --git a/app/api/contacts/[id]/prism-count/route.ts b/app/api/contacts/[id]/prism-count/route.ts new file mode 100644 index 0000000..7d23d07 --- /dev/null +++ b/app/api/contacts/[id]/prism-count/route.ts @@ -0,0 +1,35 @@ +import { NextRequest, NextResponse } from 'next/server'; +import prisma from '@/lib/prisma'; +import { requireSuperAdmin } from '@/lib/auth'; + +export async function GET( + request: NextRequest, + context: { params: Promise<{ id: string }> } +) { + const { id } = await context.params; + try { + await requireSuperAdmin(); + + // Count unique prisms for this contact through payment destinations -> splits -> prisms + const prismCount = await prisma.prism.count({ + where: { + splits: { + some: { + paymentDestination: { + contactId: id, + }, + }, + }, + }, + }); + + return NextResponse.json({ count: prismCount }); + } catch (error) { + if (error instanceof Error && error.message.includes('Unauthorized')) { + return new NextResponse('Unauthorized', { status: 401 }); + } + console.error('Error fetching prism count:', error); + return new NextResponse('Internal Server Error', { status: 500 }); + } +} + diff --git a/app/api/contacts/[id]/shared-prisms/route.ts b/app/api/contacts/[id]/shared-prisms/route.ts new file mode 100644 index 0000000..f3f74c6 --- /dev/null +++ b/app/api/contacts/[id]/shared-prisms/route.ts @@ -0,0 +1,50 @@ +import { NextRequest, NextResponse } from 'next/server'; +import prisma from '@/lib/prisma'; +import { requireSuperAdmin } from '@/lib/auth'; + +export async function GET( + request: NextRequest, + context: { params: Promise<{ id: string }> } +) { + const { id } = await context.params; + try { + await requireSuperAdmin(); + + // Get unique prisms for this contact through payment destinations -> splits -> prisms + const prisms = await prisma.prism.findMany({ + where: { + splits: { + some: { + paymentDestination: { + contactId: id, + }, + }, + }, + }, + select: { + id: true, + name: true, + slug: true, + }, + distinct: ['id'], + }); + + // For now, prisms don't have thumbnails in the schema + // When thumbnail support is added, it can be included here + const prismsWithThumbnails = prisms.map(prism => ({ + id: prism.id, + name: prism.name, + slug: prism.slug, + thumbnail: null, // Will show blank thumbnail if no profile is set + })); + + return NextResponse.json({ prisms: prismsWithThumbnails, count: prisms.length }); + } catch (error) { + if (error instanceof Error && error.message.includes('Unauthorized')) { + return new NextResponse('Unauthorized', { status: 401 }); + } + console.error('Error fetching shared prisms:', error); + return new NextResponse('Internal Server Error', { status: 500 }); + } +} + diff --git a/app/components/Button.tsx b/app/components/Button.tsx index 9ca59df..be2f2fd 100644 --- a/app/components/Button.tsx +++ b/app/components/Button.tsx @@ -44,52 +44,21 @@ export default function Button({ const iconElement = showIcon && (icon || defaultIcon); - if (style === 'Secondary') { - return ( - - ); - } - return ( + + + + {/* Main Content */} +
+ {/* Avatar and Name Section */} +
+ {/* Avatar */} +
+ {initials} + {/* Placeholder for avatar image if available */} +
+ + {/* Name and Screen Name */} +
+

+ {displayName} +

+
+ + {screenNameDisplay} + + +
+
+
+ + {/* Details Section */} + {showDetailsSection && ( +
+ + + {/* Expanded Details */} + {isOpen && ( +
+ {contact.email && ( +
+

Email

+

+ {contact.email} +

+
+ )} + + {contact.nostrPubkey && ( +
+

Nostr

+

+ {contact.nostrPubkey.slice(0, 20)}... +

+
+ )} + + {telegram && ( +
+

Telegram

+

+ {telegram} +

+
+ )} + + {twitter && ( +
+

Twitter

+

+ {twitter} +

+
+ )} + + {github && ( +
+

Github

+

+ {github} +

+
+ )} +
+ )} +
+ )} + + {/* Tags Section with Varied Colors */} + {tags.length > 0 && ( +
+ {tags.map((tag, index) => { + const tagColor = getTagColor(index, tag); + return ( +
+

+ {tag.text} +

+
+ ); + })} +
+ )} +
+ + ); +} diff --git a/app/components/ContactForm.tsx b/app/components/ContactForm.tsx index 386d146..5d137aa 100644 --- a/app/components/ContactForm.tsx +++ b/app/components/ContactForm.tsx @@ -154,9 +154,9 @@ export default function ContactForm({ }; return ( -
-
-
+
+
+

{title}

+ {isDatePickerOpen && ( +
+
+
+ + { + const newStart = e.target.value ? new Date(e.target.value) : null; + setSelectedDate(prev => ({ ...prev, start: newStart })); + }} + className="bg-[#292e2d] border border-white/15 rounded px-3 py-2 text-white text-sm" + /> +
+
+ + { + const newEnd = e.target.value ? new Date(e.target.value) : null; + setSelectedDate(prev => ({ ...prev, end: newEnd })); + }} + className="bg-[#292e2d] border border-white/15 rounded px-3 py-2 text-white text-sm" + /> +
+
+ + +
+
+
+ )} +
+ + {/* Drafts */} + + + {/* Select User */} +
+ + {isUserDropdownOpen && ( +
+ + {contacts.map((contact) => ( + + ))} +
+ )} +
+ + {/* Select Prism */} +
+ + {isPrismDropdownOpen && ( +
+ + {prisms.map((prism) => ( + + ))} +
+ )} +
+ + {/* Mode of payment */} +
+ + {isPaymentModeDropdownOpen && ( +
+ + {paymentModes.map((mode) => ( + + ))} +
+ )} +
+
+ + {/* Reset All button */} + +
+ ); +} + diff --git a/app/components/PrismForm.tsx b/app/components/PrismForm.tsx index dfb992b..fac5e3f 100644 --- a/app/components/PrismForm.tsx +++ b/app/components/PrismForm.tsx @@ -143,10 +143,10 @@ export default function PrismForm({ if (loading) { return ( -
-
+
+
-
+
@@ -159,9 +159,9 @@ export default function PrismForm({ } return ( -
-
-
+
+
+

{title}

+ + {/* Dropdown Menu */} + {isPrismDropdownOpen && ( +
+ + {prisms.map((prism) => ( + + ))} +
+ )} +
+
+ + {/* Date Range Filter */} +
+ + + {/* Date Picker Dropdown */} + {isDatePickerOpen && ( +
+
+
+ + { + const newStart = new Date(e.target.value); + if (newStart <= currentDateRange.end) { + handleDateRangeChange(newStart, currentDateRange.end); + } + }} + className="bg-[#292e2d] border border-white/15 rounded px-3 py-2 text-white text-sm" + /> +
+
+ + { + const newEnd = new Date(e.target.value); + if (newEnd >= currentDateRange.start) { + handleDateRangeChange(currentDateRange.start, newEnd); + } + }} + className="bg-[#292e2d] border border-white/15 rounded px-3 py-2 text-white text-sm" + /> +
+
+ + +
+
+
+ )} +
+
+
+ ); +} + diff --git a/app/components/Sidebar.tsx b/app/components/Sidebar.tsx new file mode 100644 index 0000000..7cc3a9d --- /dev/null +++ b/app/components/Sidebar.tsx @@ -0,0 +1,247 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { cn } from '@/lib/utils'; +import { iconTokens } from '@/app/lib/iconTokens'; +import { ChevronLeft } from 'lucide-react'; + +interface NavItem { + label: string; + href: string; + icon: { + default: string; + green?: string; + }; + iconClassName?: string; + notificationCount?: number; +} + +export default function Sidebar() { + const pathname = usePathname(); + const [isMounted, setIsMounted] = useState(false); + const [isCollapsed, setIsCollapsed] = useState(false); + const [notificationCounts, setNotificationCounts] = useState<{ + dashboard?: number; + prisms?: number; + }>({}); + + // Load collapsed state from localStorage on mount + useEffect(() => { + setIsMounted(true); + const savedState = localStorage.getItem('sidebarCollapsed'); + if (savedState !== null) { + setIsCollapsed(savedState === 'true'); + } + }, []); + + // Fetch notification counts + useEffect(() => { + const fetchCounts = async () => { + try { + // Fetch prisms count + const prismsResponse = await fetch('/api/prisms'); + if (prismsResponse.ok) { + const prismsData = await prismsResponse.json(); + const prismsCount = Array.isArray(prismsData) ? prismsData.length : 0; + setNotificationCounts(prev => ({ ...prev, prisms: prismsCount })); + } + } catch (error) { + console.error('Error fetching notification counts:', error); + } + }; + + fetchCounts(); + }, []); + + // Save collapsed state to localStorage + const toggleCollapse = () => { + const newState = !isCollapsed; + setIsCollapsed(newState); + localStorage.setItem('sidebarCollapsed', String(newState)); + }; + + const navItems: NavItem[] = [ + { label: 'Dashboard', href: '/', icon: iconTokens.dashboard, notificationCount: notificationCounts.dashboard }, + { label: 'Prisms', href: '/prisms', icon: iconTokens.prisms, notificationCount: notificationCounts.prisms }, + { label: 'Contacts', href: '/contacts', icon: iconTokens.contacts, iconClassName: 'w-[15px] h-[19px]' }, + { label: 'Node Info', href: '/node', icon: iconTokens.settings }, + ]; + + const isActive = (href: string) => { + if (href === '/') { + return pathname === href; + } + return pathname.startsWith(href); + }; + + if (!isMounted) { + return null; + } + + return ( +
+ {/* Logo Section */} +
+
+
+
+ {!isCollapsed && ( +

+ Aurora +

+ )} +
+ + {/* Collapse Toggle Button - Floating */} + + + {/* Navigation Items */} +
+ {navItems.map((item) => { + const active = isActive(item.href); + const iconSrc = active ? item.icon.green || item.icon.default : item.icon.default; + + return ( + + {/* Active Indicator Bar */} + {!isCollapsed && ( +
+
+
+ )} + + {/* Navigation Button */} +
+ {isCollapsed ? ( + <> + {/* Collapsed: Just icon and badge */} +
+
+ +
+ {item.notificationCount && item.notificationCount > 0 && ( +
+

+ {item.notificationCount > 9 ? '9+' : item.notificationCount} +

+
+ )} +
+ + ) : ( + <> +
+ {/* Icon Container */} +
+
+ +
+
+ + {/* Label */} +

+ {item.label} +

+
+ + {/* Notification Badge */} + {item.notificationCount && item.notificationCount > 0 && ( +
+

+ {item.notificationCount > 9 ? '9+' : item.notificationCount} +

+
+ )} + + )} +
+ + ); + })} +
+ +
+ ); +} + diff --git a/app/components/TopNav.tsx b/app/components/TopNav.tsx new file mode 100644 index 0000000..6e2fa2e --- /dev/null +++ b/app/components/TopNav.tsx @@ -0,0 +1,110 @@ +'use client'; + +import { useNostr } from '../contexts/NostrContext'; +import { Search, LogOut, ChevronDown, Sparkles } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { useRouter } from 'next/navigation'; +import { iconTokens } from '@/app/lib/iconTokens'; + +export default function TopNav() { + const { publicKey, npub, logout } = useNostr(); + const router = useRouter(); + + // Format npub for display (shortened version) + const displayNpub = npub ? `${npub.slice(0, 8)}...${npub.slice(-8)}` : 'Not connected'; + + // Get user name from npub or use a default + const displayName = publicKey ? displayNpub : 'Guest User'; + + const handleLogout = async () => { + try { + await logout(); + router.push('/'); + } catch (error) { + console.error('Error logging out:', error); + } + }; + + const handleGeneratePrism = () => { + router.push('/prisms/new'); + }; + + return ( + + ); +} + diff --git a/app/components/TransactionTable.tsx b/app/components/TransactionTable.tsx new file mode 100644 index 0000000..82d3c69 --- /dev/null +++ b/app/components/TransactionTable.tsx @@ -0,0 +1,661 @@ +'use client'; + +import { useState, useEffect, useRef } from 'react'; +import { useRouter } from 'next/navigation'; +import { ChevronUpIcon, ChevronRightIcon, ChevronLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline'; + +const calendarIcon = "https://www.figma.com/api/mcp/asset/c83e973c-82cb-4edc-a5b4-c87f5774c875"; +const starIconFilled = "https://www.figma.com/api/mcp/asset/e4b8e6a1-81fb-4b30-8c31-dad418948756"; +const starIconOutline = "https://www.figma.com/api/mcp/asset/af286206-fd17-4ad9-9042-9ba3cc6cf3cc"; +const chevronRightIcon = "https://www.figma.com/api/mcp/asset/2b212d2d-2875-4356-8198-db09e52ea283"; +const chevronDownIcon = "https://www.figma.com/api/mcp/asset/e353d42f-828c-4a09-a6cf-c4d48027b1b6"; +const arrowLeftIcon = "https://www.figma.com/api/mcp/asset/79630a33-05bd-489c-a889-a56fbcdbdc81"; + +interface Transaction { + id: string; + date: string; + prism: string; + prismId?: string; + amount: string; + status: 'Successful' | 'Pending' | 'Active'; + account: string; + accountId?: string; + paymentMode?: string; + isFavorite?: boolean; +} + +interface TransactionTableProps { + transactions?: Transaction[]; + prisms?: Array<{ id: string; name: string }>; + contacts?: Array<{ id: string; firstName?: string | null; lastName?: string | null; screenName?: string | null; email?: string | null }>; +} + +export default function TransactionTable({ + transactions = [], + prisms = [], + contacts = [] +}: TransactionTableProps) { + const router = useRouter(); + const [favorites, setFavorites] = useState>(new Set()); + const [currentPage, setCurrentPage] = useState(1); + const [rowsPerPage, setRowsPerPage] = useState(10); + + // Filter states + const [selectedDate, setSelectedDate] = useState<{ start: Date | null; end: Date | null }>({ start: null, end: null }); + const [selectedUserId, setSelectedUserId] = useState('all'); + const [selectedPrismId, setSelectedPrismId] = useState('all'); + const [selectedPaymentMode, setSelectedPaymentMode] = useState('all'); + + // Dropdown states + const [isDatePickerOpen, setIsDatePickerOpen] = useState(false); + const [isUserDropdownOpen, setIsUserDropdownOpen] = useState(false); + const [isPrismDropdownOpen, setIsPrismDropdownOpen] = useState(false); + const [isPaymentModeDropdownOpen, setIsPaymentModeDropdownOpen] = useState(false); + const [isRowsPerPageOpen, setIsRowsPerPageOpen] = useState(false); + + // Refs for click outside + const datePickerRef = useRef(null); + const userDropdownRef = useRef(null); + const prismDropdownRef = useRef(null); + const paymentModeDropdownRef = useRef(null); + const rowsPerPageRef = useRef(null); + + // Default sample data if none provided + const defaultTransactions: Transaction[] = [ + { + id: '1', + date: '06/2025', + prism: 'bitcoin Pizza', + amount: '$1,250.00', + status: 'Successful', + account: 'Jamie Smith', + isFavorite: false, + }, + { + id: '2', + date: '07/2025', + prism: 'Crypto Feast', + amount: '$500.00', + status: 'Pending', + account: 'Alex Johnson', + isFavorite: false, + }, + { + id: '3', + date: '09/2025', + prism: 'Tech Summit', + amount: '225078764578.00 sats', + status: 'Successful', + account: 'QHFI8WE8DYHWEBJhbsbdcus...', + isFavorite: true, + }, + { + id: '4', + date: '11/2025', + prism: 'Health Expo', + amount: '3000.00 Sats', + status: 'Active', + account: 'Michael Brown', + isFavorite: false, + }, + { + id: '5', + date: '06/2024', + prism: 'The true man show movie...', + amount: '999999999999999 Sats', + status: 'Active', + account: 'deekshasatapathy@twelve.cash', + isFavorite: false, + }, + { + id: '6', + date: '04/2025', + prism: 'Fashion Week', + amount: '56.5643679 Sats', + status: 'Successful', + account: 'Jessica Lee', + isFavorite: false, + }, + { + id: '7', + date: '08/2025', + prism: 'Food Festival', + amount: '1 Btc', + status: 'Successful', + account: 'kcuabcjbau2e482r982hufwueff...', + isFavorite: false, + }, + { + id: '8', + date: '12/2025', + prism: 'Finance Forum', + amount: '$1,200.00', + status: 'Pending', + account: 'Rachel Adams', + isFavorite: false, + }, + ]; + + // Close dropdowns when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (datePickerRef.current && !datePickerRef.current.contains(event.target as Node)) { + setIsDatePickerOpen(false); + } + if (userDropdownRef.current && !userDropdownRef.current.contains(event.target as Node)) { + setIsUserDropdownOpen(false); + } + if (prismDropdownRef.current && !prismDropdownRef.current.contains(event.target as Node)) { + setIsPrismDropdownOpen(false); + } + if (paymentModeDropdownRef.current && !paymentModeDropdownRef.current.contains(event.target as Node)) { + setIsPaymentModeDropdownOpen(false); + } + if (rowsPerPageRef.current && !rowsPerPageRef.current.contains(event.target as Node)) { + setIsRowsPerPageOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + // Parse date from MM/YYYY format + const parseTransactionDate = (dateStr: string): Date => { + const [month, year] = dateStr.split('/'); + return new Date(parseInt(year), parseInt(month) - 1, 1); + }; + + // Filter transactions based on selected filters + const filterTransactions = (txns: Transaction[]): Transaction[] => { + return txns.filter(txn => { + // Date filter + if (selectedDate.start || selectedDate.end) { + try { + const txDate = parseTransactionDate(txn.date); + if (selectedDate.start) { + const startDate = new Date(selectedDate.start); + startDate.setHours(0, 0, 0, 0); + if (txDate < startDate) return false; + } + if (selectedDate.end) { + const endDate = new Date(selectedDate.end); + endDate.setHours(23, 59, 59, 999); + if (txDate > endDate) return false; + } + } catch (e) { + // If date parsing fails, include the transaction + } + } + + // User filter + if (selectedUserId !== 'all' && txn.accountId !== selectedUserId) { + return false; + } + + // Prism filter + if (selectedPrismId !== 'all' && txn.prismId !== selectedPrismId) { + return false; + } + + // Payment mode filter + if (selectedPaymentMode !== 'all' && txn.paymentMode !== selectedPaymentMode) { + return false; + } + + return true; + }); + }; + + const displayTransactions = transactions.length > 0 ? transactions : defaultTransactions; + const filteredTransactions = filterTransactions(displayTransactions); + const totalTransactions = filteredTransactions.length; + const totalPages = Math.ceil(totalTransactions / rowsPerPage); + const startIndex = (currentPage - 1) * rowsPerPage; + const endIndex = startIndex + rowsPerPage; + const paginatedTransactions = filteredTransactions.slice(startIndex, endIndex); + + // Reset page when filters change + useEffect(() => { + setCurrentPage(1); + }, [selectedDate, selectedUserId, selectedPrismId, selectedPaymentMode]); + + // Reset all filters + const handleResetAll = () => { + setSelectedDate({ start: null, end: null }); + setSelectedUserId('all'); + setSelectedPrismId('all'); + setSelectedPaymentMode('all'); + setCurrentPage(1); + }; + + // Get unique payment modes from transactions + const paymentModes = Array.from(new Set(displayTransactions.map(t => t.paymentMode).filter(Boolean))); + + // Get display name for user + const getUserDisplayName = (contact: { firstName?: string | null; lastName?: string | null; screenName?: string | null; email?: string | null }) => { + if (contact.firstName && contact.lastName) { + return `${contact.firstName} ${contact.lastName}`; + } + return contact.screenName || contact.email || 'Unknown'; + }; + + // Format date for display + const formatDateFilter = () => { + if (!selectedDate.start && !selectedDate.end) return 'Select Date'; + if (selectedDate.start && selectedDate.end) { + return `${selectedDate.start.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} - ${selectedDate.end.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}`; + } + if (selectedDate.start) { + return `From ${selectedDate.start.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}`; + } + if (selectedDate.end) { + return `Until ${selectedDate.end.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}`; + } + return 'Select Date'; + }; + + const toggleFavorite = (id: string) => { + setFavorites(prev => { + const newSet = new Set(prev); + if (newSet.has(id)) { + newSet.delete(id); + } else { + newSet.add(id); + } + return newSet; + }); + }; + + const getStatusColor = (status: string) => { + switch (status) { + case 'Successful': + return 'text-[#0acaa1]'; + case 'Pending': + return 'text-[#ff0509]'; + case 'Active': + return 'text-[#ffd905]'; + default: + return 'text-white'; + } + }; + + return ( +
+
+ {/* Header Section */} +
+ {/* Title */} +
+
+
+ +
+

+ All transactions +

+
+
+ + {/* Filters */} +
+
+ {/* Select Date */} +
+ + {isDatePickerOpen && ( +
+
+
+ + { + const newStart = e.target.value ? new Date(e.target.value) : null; + setSelectedDate(prev => ({ ...prev, start: newStart })); + }} + className="bg-[#292e2d] border border-white/15 rounded px-3 py-2 text-white text-sm" + /> +
+
+ + { + const newEnd = e.target.value ? new Date(e.target.value) : null; + setSelectedDate(prev => ({ ...prev, end: newEnd })); + }} + className="bg-[#292e2d] border border-white/15 rounded px-3 py-2 text-white text-sm" + /> +
+
+
+ )} +
+ + {/* Select User */} +
+ + {isUserDropdownOpen && ( +
+ + {contacts.map((contact) => ( + + ))} +
+ )} +
+ + {/* Select Prism */} +
+ + {isPrismDropdownOpen && ( +
+ + {prisms.map((prism) => ( + + ))} +
+ )} +
+ + {/* Mode of payment */} +
+ + {isPaymentModeDropdownOpen && ( +
+ + {paymentModes.map((mode) => ( + + ))} +
+ )} +
+
+ + {/* Reset All */} + +
+
+ + {/* Table */} +
+ {/* Table Header */} +
+
+

+
+
+

Date

+
+
+

Prism

+
+
+

Amount

+
+
+

Status

+
+
+

Account

+
+
+

+ View Details +

+
+
+ + {/* Table Rows */} +
+ {paginatedTransactions.map((transaction) => { + const isFavorite = favorites.has(transaction.id) || transaction.isFavorite; + return ( +
+ {/* Star Icon - Left Side */} +
+ +
+ + {/* Date */} +
+

+ {transaction.date} +

+
+ + {/* Prism */} +
+

+ {transaction.prism} +

+
+ + {/* Amount */} +
+

+ {transaction.amount} +

+
+ + {/* Status */} +
+

+ {transaction.status} +

+
+ + {/* Account */} +
+

+ {transaction.account} +

+
+ + {/* View Details Arrow Icon */} +
+ +
+
+ ); + })} +
+
+ + {/* Pagination */} +
+
+

+ {startIndex + 1} - {Math.min(endIndex, totalTransactions)} of {totalTransactions} +

+
+
+ {/* Rows per page */} +
+

+ Rows per page: +

+ + {isRowsPerPageOpen && ( +
+ {[5, 10, 20, 50, 100].map((num) => ( + + ))} +
+ )} +
+ + {/* Navigation */} +
+ + +
+
+
+
+
+ ); +} + diff --git a/app/contacts/[id]/edit/page.tsx b/app/contacts/[id]/edit/page.tsx index 5a6d7de..84d32be 100644 --- a/app/contacts/[id]/edit/page.tsx +++ b/app/contacts/[id]/edit/page.tsx @@ -90,10 +90,10 @@ export default function EditContactPage({ if (loading) { return ( -
-
+
+
-
+
diff --git a/app/contacts/layout.tsx b/app/contacts/layout.tsx index e6a2325..63f0661 100644 --- a/app/contacts/layout.tsx +++ b/app/contacts/layout.tsx @@ -1,7 +1,6 @@ import { cookies } from 'next/headers'; import { redirect } from 'next/navigation'; import { validateSuperAdmin } from '@/lib/auth'; -import Header from '@/app/components/Header'; export default async function ContactsLayout({ children, @@ -19,12 +18,7 @@ export default async function ContactsLayout({ redirect('/'); } - return ( - <> -
- {children} - - ); + return <>{children}; } catch (error) { console.error('Error in contacts layout:', error); redirect('/'); diff --git a/app/contacts/page.tsx b/app/contacts/page.tsx index 4f506c7..b5a5d32 100644 --- a/app/contacts/page.tsx +++ b/app/contacts/page.tsx @@ -1,9 +1,9 @@ 'use client'; import { useEffect, useState } from 'react'; -import Link from 'next/link'; import { useRouter } from 'next/navigation'; import Button from '@/app/components/Button'; +import ContactCard from '@/app/components/ContactCard'; interface Contact { id: string; @@ -61,16 +61,18 @@ export default function ContactsPage() { if (loading) { return ( -
-
-
-
- {[...Array(5)].map((_, i) => ( -
-
-
-
- ))} +
+
+
+
+
+ {[...Array(5)].map((_, i) => ( +
+
+
+
+ ))} +
@@ -79,73 +81,44 @@ export default function ContactsPage() { if (error) { return ( -
-
-

Error

-

{error}

-
); } return ( -
-
-

Contacts

-
+
+
+
+

Contacts

+
-
- {contacts.map((contact) => ( -
-
-
-

- {contact.firstName && contact.lastName - ? `${contact.firstName} ${contact.lastName}` - : contact.screenName || 'Unnamed Contact'} -

- {contact.screenName && ( -

@{contact.screenName}

- )} - {contact.email && ( -

{contact.email}

- )} - {contact.nostrPubkey && ( -

- {contact.nostrPubkey} -

- )} -
- - Edit - - - - -
- {contact.metadata && typeof contact.metadata === 'object' && 'bio' in contact.metadata && typeof contact.metadata.bio === 'string' && ( -

{contact.metadata.bio}

- )} -
- ))} +
+ {contacts.map((contact) => ( + + ))} +
); diff --git a/app/globals.css b/app/globals.css index 6b717ad..4f1c14a 100644 --- a/app/globals.css +++ b/app/globals.css @@ -3,13 +3,13 @@ @tailwind utilities; :root { - --background: #ffffff; - --foreground: #171717; + --background: #030404; + --foreground: #ededed; } @media (prefers-color-scheme: dark) { :root { - --background: #0a0a0a; + --background: #030404; --foreground: #ededed; } } diff --git a/app/layout.tsx b/app/layout.tsx index 7d1e0eb..baeb15e 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,7 +2,8 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; import { NostrProvider } from "./contexts/NostrContext"; -import Header from "./components/Header"; +import Sidebar from "./components/Sidebar"; +import TopNav from "./components/TopNav"; const inter = Inter({ subsets: ["latin"] }); @@ -18,10 +19,19 @@ export default function RootLayout({ }) { return ( - + -
- {children} +
+
+ +
+
+ +
+ {children} +
+
+
diff --git a/app/lib/iconTokens.ts b/app/lib/iconTokens.ts new file mode 100644 index 0000000..cf757fa --- /dev/null +++ b/app/lib/iconTokens.ts @@ -0,0 +1,42 @@ +export const iconTokens = { + dashboard: { + default: 'https://www.figma.com/api/mcp/asset/7ea22fe7-76f3-4dcd-be2a-27584735bf3e', + green: 'https://www.figma.com/api/mcp/asset/46fe45e5-90ae-4730-b5f5-6775c3ea68b7', + dark: 'https://www.figma.com/api/mcp/asset/a4ec5af4-7202-4a2c-af55-4f15dddc4549', + }, + prisms: { + default: 'https://www.figma.com/api/mcp/asset/344137cd-61ab-4c91-80d3-746b4b1078ef', + green: 'https://www.figma.com/api/mcp/asset/6c56fea7-8bb6-4c02-be2c-fa73a3c6e210', + dark: 'https://www.figma.com/api/mcp/asset/e9890cf3-3675-49e7-8a57-e15013aed9d5', + }, + contacts: { + default: 'https://www.figma.com/api/mcp/asset/db895cc0-2873-4f73-a16f-71332b53de60', + green: 'https://www.figma.com/api/mcp/asset/af5bddfe-c935-4178-a465-7b22f4a480c5', + dark: 'https://www.figma.com/api/mcp/asset/425d7d31-b102-4dfd-98bd-6e9f84ab25b0', + }, + notifications: { + default: 'https://www.figma.com/api/mcp/asset/9bef6dcd-9734-44fa-b3c8-fda66e0a8bb7', + green: 'https://www.figma.com/api/mcp/asset/7023b275-ef0d-485b-846e-a823595fab0c', + dark: 'https://www.figma.com/api/mcp/asset/efde19ab-cd3c-4d42-a31a-827179fc0433', + }, + settings: { + default: 'https://www.figma.com/api/mcp/asset/f1c526ef-385e-49c0-95f7-2ee7831d0d1f', + variant2: 'https://www.figma.com/api/mcp/asset/6d2b67c0-1f7e-4a36-b5b3-20fb8eaede33', + variant3: 'https://www.figma.com/api/mcp/asset/55e572ed-04e4-47ca-9149-5815e4f6e3e9', + }, + wallet: { + default: 'https://www.figma.com/api/mcp/asset/9b443623-e832-4d0e-a38c-850d8be7a35e', + green: 'https://www.figma.com/api/mcp/asset/f5bfa230-d75d-46fe-a700-2bab54cd9fee', + }, + finance: { + default: 'https://www.figma.com/api/mcp/asset/89d6edc7-18cd-4d29-97b3-db4116db9ed0', + variant2: 'https://www.figma.com/api/mcp/asset/22c366a9-5a17-4eb7-9da8-248d0a7a4e76', + variant3: 'https://www.figma.com/api/mcp/asset/6bd0f1ab-4945-47da-bf52-f72129f762d9', + }, + vector: { + default: 'https://www.figma.com/api/mcp/asset/0ca23d71-1965-41a9-b1bb-42fc1827f05a', + variant2: 'https://www.figma.com/api/mcp/asset/329bbee7-c73b-433d-97e4-dd2a7179ee28', + variant3: 'https://www.figma.com/api/mcp/asset/45f3e1e8-dce4-4de1-904a-9cbda44f996e', + }, +} as const; + diff --git a/app/node/layout.tsx b/app/node/layout.tsx index e9986c6..d73a27d 100644 --- a/app/node/layout.tsx +++ b/app/node/layout.tsx @@ -1,14 +1,7 @@ -import Header from "../components/Header"; - export default function NodeLayout({ children, }: { children: React.ReactNode; }) { - return ( - <> -
- {children} - - ); + return <>{children}; } \ No newline at end of file diff --git a/app/node/page.tsx b/app/node/page.tsx index ab8c313..968b0a0 100644 --- a/app/node/page.tsx +++ b/app/node/page.tsx @@ -2,10 +2,12 @@ import PhoenixInfo from "../components/PhoenixInfo"; export default function NodePage() { return ( -
-

Node Information

-
- +
+
+

Node Information

+
+ +
); diff --git a/app/page.tsx b/app/page.tsx index 82662ee..1e70147 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,10 +1,99 @@ 'use client'; +import { useState, useEffect } from 'react'; import { useNostr } from "./contexts/NostrContext"; import Button from "./components/Button"; +import PrismFaceCard from "./components/PrismFaceCard"; +import PrismHeadingCard from "./components/PrismHeadingCard"; +import PrismInfoCard from "./components/PrismInfoCard"; +import TransactionTable from "./components/TransactionTable"; + +interface Prism { + id: string; + name: string; + slug: string; + description: string | null; + active: boolean; + createdAt: string; + splits?: Array<{ + paymentDestination: { + contact: { + id: string; + }; + }; + }>; +} + +interface Contact { + id: string; + firstName: string | null; + lastName: string | null; + screenName: string | null; + email: string | null; +} export default function Home() { const { publicKey, login } = useNostr(); + const [prisms, setPrisms] = useState([]); + const [contacts, setContacts] = useState([]); + const [loading, setLoading] = useState(false); + const [selectedPrismId, setSelectedPrismId] = useState('all'); + const [dateRange, setDateRange] = useState<{ start: Date; end: Date }>(() => { + const today = new Date(); + const startOfWeek = new Date(today); + startOfWeek.setDate(today.getDate() - today.getDay()); + const endOfWeek = new Date(startOfWeek); + endOfWeek.setDate(startOfWeek.getDate() + 6); + return { start: startOfWeek, end: endOfWeek }; + }); + + useEffect(() => { + if (publicKey) { + fetchPrisms(); + fetchContacts(); + } + }, [publicKey]); + + const fetchContacts = async () => { + try { + const response = await fetch('/api/contacts'); + if (response.ok) { + const data = await response.json(); + setContacts(data); + } + } catch (err) { + console.error('Error fetching contacts:', err); + } + }; + + const fetchPrisms = async () => { + setLoading(true); + try { + const response = await fetch('/api/prisms'); + if (response.ok) { + const data = await response.json(); + // Fetch detailed data for each prism to get member count + const prismsWithDetails = await Promise.all( + data.map(async (prism: Prism) => { + try { + const detailResponse = await fetch(`/api/prisms/${prism.id}`); + if (detailResponse.ok) { + return await detailResponse.json(); + } + return prism; + } catch { + return prism; + } + }) + ); + setPrisms(prismsWithDetails); + } + } catch (err) { + console.error('Error fetching prisms:', err); + } finally { + setLoading(false); + } + }; const handleLogin = async () => { try { @@ -14,16 +103,108 @@ export default function Home() { } }; + // Calculate member count from splits + const getMemberCount = (prism: Prism): number => { + if (!prism.splits) return 0; + const uniqueContacts = new Set( + prism.splits.map(split => split.paymentDestination.contact.id) + ); + return uniqueContacts.size; + }; + + // Calculate amounts based on selected prism and date range + // TODO: Replace with actual API calls to get transaction data + const calculateAmounts = () => { + // For now, return placeholder values + // In a real implementation, you would: + // 1. Filter transactions by selectedPrismId and dateRange + // 2. Calculate totalCollected, totalDispatched, and pending + return { + totalCollected: 412000.95, + totalDispatched: 412000.95, + pending: 0, + }; + }; + + const amounts = calculateAmounts(); + return (
{publicKey ? ( // Logged in view -
-
-

Welcome to Aurora

-

- Your lightning prism management interface. Navigate through the menu to access different features. -

+
+
+ {/* Welcome Section */} +
+
+

Welcome to Aurora

+

+ Split any payment across wallets or accounts instantly, transparently, and securely. +

+
+ + {/* Info Cards */} +
+ ({ id: p.id, name: p.name }))} + selectedPrismId={selectedPrismId} + onPrismChange={setSelectedPrismId} + dateRange={dateRange} + onDateRangeChange={setDateRange} + /> +
+
+ + {/* Hot Prisms Section */} +
+
+ +
+ + {/* Prism Cards Grid */} + {loading ? ( +
+ {[1, 2, 3, 4, 5].map((i) => ( +
+ ))} +
+ ) : prisms.length > 0 ? ( +
+ {prisms.map((prism) => ( + + ))} +
+ ) : ( +
+

No prisms found.

+

+ Create your first prism to get started. +

+
+ )} +
+ + {/* Transaction Table */} +
+ ({ id: p.id, name: p.name }))} + contacts={contacts} + /> +
) : ( diff --git a/app/prisms/[id]/page.tsx b/app/prisms/[id]/page.tsx index 38949d9..ef8e3e1 100644 --- a/app/prisms/[id]/page.tsx +++ b/app/prisms/[id]/page.tsx @@ -93,10 +93,10 @@ export default function PrismPage({ if (loading) { return ( -
-
+
+
-
+
@@ -110,10 +110,10 @@ export default function PrismPage({ if (!prism) { return ( -
-
+
+
-

Prism Not Found

+

Prism Not Found

The prism you're looking for doesn't exist or you don't have permission to view it.

diff --git a/app/prisms/new/page.tsx b/app/prisms/new/page.tsx index 0c5042b..cfeb274 100644 --- a/app/prisms/new/page.tsx +++ b/app/prisms/new/page.tsx @@ -185,10 +185,10 @@ export default function NewPrismPage() { }; return ( -
-
-
-

Create New Prism

+
+
+
+

Create New Prism