From 31ed73bb6acd0cb37566e7f59fe2c370ac360c9f Mon Sep 17 00:00:00 2001 From: deekshas99 Date: Tue, 23 Dec 2025 20:00:56 +0530 Subject: [PATCH 1/9] Contact card --- app/components/ContactCard.tsx | 379 +++++++++++++++++++++++++++++++++ 1 file changed, 379 insertions(+) create mode 100644 app/components/ContactCard.tsx diff --git a/app/components/ContactCard.tsx b/app/components/ContactCard.tsx new file mode 100644 index 0000000..bba9a3e --- /dev/null +++ b/app/components/ContactCard.tsx @@ -0,0 +1,379 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { PencilIcon, ArrowUpTrayIcon, ChevronUpIcon } from '@heroicons/react/24/outline'; +import { cn } from '@/lib/utils'; + +interface Contact { + id: string; + firstName: string | null; + lastName: string | null; + screenName: string | null; + email: string | null; + nostrPubkey: string | null; + metadata: Record | null; +} + +interface SharedPrism { + id: string; + name: string; + slug: string; + thumbnail?: string | null; // Profile image/thumbnail URL +} + +interface Tag { + text: string; + color?: string; // Hex color code +} + +interface ContactCardProps { + contact: Contact; + variant?: 'open' | 'close'; +} + +export default function ContactCard({ contact, variant = 'open' }: ContactCardProps) { + // Match Figma variants: + // - "open": details panel expanded by default + // - "close": compact card with no details panel + const [isOpen, setIsOpen] = useState(variant === 'open'); + const [sharedPrisms, setSharedPrisms] = useState([]); + const [detailsCount, setDetailsCount] = useState(0); + + // Fetch shared prisms for this contact + useEffect(() => { + const fetchSharedPrisms = async () => { + try { + const response = await fetch(`/api/contacts/${contact.id}/shared-prisms`); + if (response.ok) { + const data = await response.json(); + setSharedPrisms(data.prisms || []); + } + } catch (err) { + console.error('Error fetching shared prisms:', err); + setSharedPrisms([]); + } + }; + + fetchSharedPrisms(); + }, [contact.id]); + + // Calculate details count + useEffect(() => { + let count = 0; + if (contact.email) count++; + if (contact.nostrPubkey) count++; + const metadata = contact.metadata && typeof contact.metadata === 'object' ? contact.metadata : {}; + if (metadata.telegram) count++; + if (metadata.twitter) count++; + if (metadata.github) count++; + setDetailsCount(count); + }, [contact]); + + const displayName = contact.firstName && contact.lastName + ? `${contact.firstName} ${contact.lastName}` + : contact.screenName || 'Unnamed Contact'; + + const initials = contact.firstName && contact.lastName + ? `${contact.firstName[0]}${contact.lastName[0]}`.toUpperCase() + : contact.screenName + ? contact.screenName[0].toUpperCase() + : '?'; + + const metadata = contact.metadata && typeof contact.metadata === 'object' ? contact.metadata : {}; + const telegram = typeof metadata.telegram === 'string' ? metadata.telegram : null; + const twitter = typeof metadata.twitter === 'string' ? metadata.twitter : null; + const github = typeof metadata.github === 'string' ? metadata.github : null; + + // Extract tags from metadata with color support + const tags: Tag[] = []; + + // Support tags as array of objects with text and color + if (Array.isArray(metadata.tags)) { + metadata.tags.forEach((tag: unknown) => { + if (typeof tag === 'string') { + tags.push({ text: tag }); + } else if (typeof tag === 'object' && tag !== null && 'text' in tag) { + tags.push({ + text: String(tag.text), + color: 'color' in tag ? String(tag.color) : undefined, + }); + } + }); + } + + // Support interests as tags + if (Array.isArray(metadata.interests)) { + metadata.interests.forEach((interest: unknown) => { + if (typeof interest === 'string') { + tags.push({ text: interest }); + } + }); + } + + // Extract tags from bio if it contains specific keywords + if (typeof metadata.bio === 'string' && metadata.bio) { + const bioLower = metadata.bio.toLowerCase(); + if (bioLower.includes('lightning')) { + tags.push({ text: 'Lightning Network enthusiast', color: '#8a05ff' }); + } + if (bioLower.includes('conference') || bioLower.includes('bitcoin')) { + tags.push({ text: 'bitcoin Conference 2023', color: '#345204' }); + } + } + + // Format screen name display + const screenNameDisplay = contact.screenName + ? `${contact.firstName || contact.screenName}@${contact.screenName}` + : contact.email || ''; + + const toggleDetails = () => { + setIsOpen(!isOpen); + }; + + const handleShareProfile = async () => { + try { + // Create a shareable link or copy to clipboard + const profileUrl = `${window.location.origin}/contacts/${contact.id}`; + + if (navigator.share) { + await navigator.share({ + title: `${displayName}'s Profile`, + text: `Check out ${displayName}'s contact profile`, + url: profileUrl, + }); + } else { + // Fallback: copy to clipboard + await navigator.clipboard.writeText(profileUrl); + alert('Profile link copied to clipboard!'); + } + } catch (err) { + console.error('Error sharing profile:', err); + } + }; + + const handleCopyAddress = async () => { + const address = contact.nostrPubkey || contact.email || screenNameDisplay; + if (address) { + try { + await navigator.clipboard.writeText(address); + // Could show a toast notification here + } catch (err) { + console.error('Failed to copy:', err); + } + } + }; + + // Default tag colors if not specified + const getTagColor = (index: number, tag: Tag): string => { + if (tag.color) return tag.color; + // Default colors: purple, green, blue, orange, etc. + const defaultColors = ['#8a05ff', '#345204', '#0066cc', '#ff6600', '#cc0066']; + return defaultColors[index % defaultColors.length]; + }; + + // Get remaining prism count (after showing first 3) + const remainingPrismCount = sharedPrisms.length > 3 ? sharedPrisms.length - 3 : 0; + const visiblePrisms = sharedPrisms.slice(0, 3); + + // Only show the details section for the "open" variant + const showDetailsSection = variant === 'open'; + + return ( +
+ {/* Header with Shared Prisms and CTAs */} +
+ {/* Shared Prisms with Thumbnails */} +
+ {sharedPrisms.length > 0 && ( + <> +
+ {/* Show up to 3 prism thumbnails */} + {visiblePrisms.map((prism, index) => ( +
+ {prism.thumbnail ? ( + {prism.name} { + // Fallback to blank if image fails to load + e.currentTarget.style.display = 'none'; + }} + /> + ) : ( + // Blank thumbnail if no profile set +
+ )} +
+ ))} +
+ {remainingPrismCount > 0 && ( +
+

+ +{remainingPrismCount} +

+

+ prism +

+
+ )} + + )} +
+ + {/* CTA Buttons: Edit Profile and Share Profile */} +
+ + + + +
+
+ + {/* Main Content */} +
+ {/* Avatar and Name Section */} +
+ {/* Avatar */} +
+ {initials} + {/* Placeholder for avatar image if available */} +
+ + {/* Name and Screen Name */} +
+

+ {displayName} +

+ +
+
+ + {/* 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} +

+
+ ); + })} +
+ )} +
+
+ ); +} From d70ce4db9add97a79fc10a3e2f095a2cdfc81491 Mon Sep 17 00:00:00 2001 From: deekshas99 Date: Tue, 23 Dec 2025 20:21:09 +0530 Subject: [PATCH 2/9] Refactor ContactCard component to improve prism count handling and display. Updated logic to show total prism count instead of remaining count, enhancing clarity in the UI. --- app/components/ContactCard.tsx | 71 ++++++++++++++++------------------ 1 file changed, 33 insertions(+), 38 deletions(-) diff --git a/app/components/ContactCard.tsx b/app/components/ContactCard.tsx index bba9a3e..a70c9f7 100644 --- a/app/components/ContactCard.tsx +++ b/app/components/ContactCard.tsx @@ -173,7 +173,8 @@ export default function ContactCard({ contact, variant = 'open' }: ContactCardPr }; // Get remaining prism count (after showing first 3) - const remainingPrismCount = sharedPrisms.length > 3 ? sharedPrisms.length - 3 : 0; + const prismCount = sharedPrisms.length; + const remainingPrismCount = prismCount > 3 ? prismCount - 3 : 0; const visiblePrisms = sharedPrisms.slice(0, 3); // Only show the details section for the "open" variant @@ -185,44 +186,38 @@ export default function ContactCard({ contact, variant = 'open' }: ContactCardPr
{/* Shared Prisms with Thumbnails */}
- {sharedPrisms.length > 0 && ( - <> -
- {/* Show up to 3 prism thumbnails */} - {visiblePrisms.map((prism, index) => ( -
- {prism.thumbnail ? ( - {prism.name} { - // Fallback to blank if image fails to load - e.currentTarget.style.display = 'none'; - }} - /> - ) : ( - // Blank thumbnail if no profile set -
- )} -
- ))} +
+ {/* Show up to 3 prism thumbnails */} + {visiblePrisms.map((prism) => ( +
+ {prism.thumbnail ? ( + {prism.name} { + // Fallback to blank if image fails to load + e.currentTarget.style.display = 'none'; + }} + /> + ) : ( + // Blank thumbnail if no profile set +
+ )}
- {remainingPrismCount > 0 && ( -
-

- +{remainingPrismCount} -

-

- prism -

-
- )} - - )} + ))} +
+
+

+ +{prismCount} +

+

+ prism +

+
{/* CTA Buttons: Edit Profile and Share Profile */} From 2656a7504de309e669a394ce3e5b378e2681377b Mon Sep 17 00:00:00 2001 From: deekshas99 Date: Tue, 23 Dec 2025 20:27:10 +0530 Subject: [PATCH 3/9] Update ContactCard component styles for improved UI consistency. Adjusted button and SVG sizes, and modified hover effects for better accessibility and visual appeal. --- app/components/ContactCard.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/ContactCard.tsx b/app/components/ContactCard.tsx index a70c9f7..ead2845 100644 --- a/app/components/ContactCard.tsx +++ b/app/components/ContactCard.tsx @@ -265,11 +265,11 @@ export default function ContactCard({ contact, variant = 'open' }: ContactCardPr From 0db05c242dece3ef9224057ca97561e37d015756 Mon Sep 17 00:00:00 2001 From: deekshas99 Date: Tue, 23 Dec 2025 20:41:34 +0530 Subject: [PATCH 4/9] Refactor ContactsPage to utilize ContactCard component for rendering contacts. Updated layout styles for improved responsiveness and consistency. --- app/components/ContactCard.tsx | 2 +- app/contacts/page.tsx | 45 +++++----------------------------- 2 files changed, 7 insertions(+), 40 deletions(-) diff --git a/app/components/ContactCard.tsx b/app/components/ContactCard.tsx index ead2845..1239895 100644 --- a/app/components/ContactCard.tsx +++ b/app/components/ContactCard.tsx @@ -181,7 +181,7 @@ export default function ContactCard({ contact, variant = 'open' }: ContactCardPr const showDetailsSection = variant === 'open'; return ( -
+
{/* Header with Shared Prisms and CTAs */}
{/* Shared Prisms with Thumbnails */} diff --git a/app/contacts/page.tsx b/app/contacts/page.tsx index 4f506c7..2f68398 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,7 +61,7 @@ export default function ContactsPage() { if (loading) { return ( -
+
@@ -79,7 +79,7 @@ export default function ContactsPage() { if (error) { return ( -
+

Error

{error}

@@ -99,7 +99,7 @@ export default function ContactsPage() { } return ( -
+

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}

- )} -
+ ))}
From abf2fab2f0f99e75f672cb77df3acc3f99e92f22 Mon Sep 17 00:00:00 2001 From: deekshas99 Date: Tue, 23 Dec 2025 20:44:09 +0530 Subject: [PATCH 5/9] Contact card --- app/api/contacts/[id]/prism-count/route.ts | 35 ++++++++++++++ app/api/contacts/[id]/shared-prisms/route.ts | 50 ++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 app/api/contacts/[id]/prism-count/route.ts create mode 100644 app/api/contacts/[id]/shared-prisms/route.ts 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 }); + } +} + From c095b7e2a01fb35bdfa560071c9370203b070479 Mon Sep 17 00:00:00 2001 From: deekshas99 Date: Fri, 9 Jan 2026 16:51:10 +0530 Subject: [PATCH 6/9] side and top nav bar --- DESIGN_TOKENS.md | 200 ++++++++++++++++++++++++++ app/components/ContactForm.tsx | 2 +- app/components/PrismForm.tsx | 2 +- app/components/Sidebar.tsx | 239 ++++++++++++++++++++++++++++++++ app/components/TopNav.tsx | 101 ++++++++++++++ app/contacts/[id]/edit/page.tsx | 2 +- app/contacts/layout.tsx | 8 +- app/contacts/page.tsx | 6 +- app/globals.css | 6 +- app/layout.tsx | 18 ++- app/node/layout.tsx | 9 +- app/node/page.tsx | 2 +- app/page.tsx | 2 +- app/prisms/[id]/page.tsx | 4 +- app/prisms/new/page.tsx | 2 +- app/prisms/page.tsx | 2 +- 16 files changed, 571 insertions(+), 34 deletions(-) create mode 100644 DESIGN_TOKENS.md create mode 100644 app/components/Sidebar.tsx create mode 100644 app/components/TopNav.tsx diff --git a/DESIGN_TOKENS.md b/DESIGN_TOKENS.md new file mode 100644 index 0000000..4562adf --- /dev/null +++ b/DESIGN_TOKENS.md @@ -0,0 +1,200 @@ +# 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 + +--- + +## 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/components/ContactForm.tsx b/app/components/ContactForm.tsx index 386d146..c74bccb 100644 --- a/app/components/ContactForm.tsx +++ b/app/components/ContactForm.tsx @@ -154,7 +154,7 @@ export default function ContactForm({ }; return ( -
+

{title}

diff --git a/app/components/PrismForm.tsx b/app/components/PrismForm.tsx index dfb992b..2436458 100644 --- a/app/components/PrismForm.tsx +++ b/app/components/PrismForm.tsx @@ -143,7 +143,7 @@ export default function PrismForm({ if (loading) { return ( -
+
diff --git a/app/components/Sidebar.tsx b/app/components/Sidebar.tsx new file mode 100644 index 0000000..a40d272 --- /dev/null +++ b/app/components/Sidebar.tsx @@ -0,0 +1,239 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { cn } from '@/lib/utils'; +import { + LayoutDashboard, + Receipt, + Users, + Network, + Gem, + ChevronLeft +} from 'lucide-react'; + +interface NavItem { + label: string; + href: string; + icon: React.ComponentType<{ className?: string }>; + notificationCount?: number; +} + +export default function Sidebar() { + const pathname = usePathname(); + const [isCollapsed, setIsCollapsed] = useState(false); + const [notificationCounts, setNotificationCounts] = useState<{ + dashboard?: number; + prisms?: number; + }>({}); + + // Load collapsed state from localStorage on mount + useEffect(() => { + 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: LayoutDashboard, notificationCount: notificationCounts.dashboard }, + { label: 'Prisms', href: '/prisms', icon: Gem, notificationCount: notificationCounts.prisms }, + { label: 'Contacts', href: '/contacts', icon: Users }, + { label: 'Node Info', href: '/node', icon: Network }, + ]; + + const isActive = (href: string) => { + if (href === '/') { + return pathname === href; + } + return pathname.startsWith(href); + }; + + return ( +
+ {/* Logo Section */} +
+
+
+
+ {!isCollapsed && ( +

+ Aurora +

+ )} +
+ + {/* Collapse Toggle Button - Floating */} + + + {/* Navigation Items */} +
+ {navItems.map((item) => { + const active = isActive(item.href); + const Icon = item.icon; + + 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..7a3d8bd --- /dev/null +++ b/app/components/TopNav.tsx @@ -0,0 +1,101 @@ +'use client'; + +import { useNostr } from '../contexts/NostrContext'; +import { Search, Bell, LogOut, ChevronDown, Sparkles } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { useRouter } from 'next/navigation'; + +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/contacts/[id]/edit/page.tsx b/app/contacts/[id]/edit/page.tsx index 5a6d7de..146b932 100644 --- a/app/contacts/[id]/edit/page.tsx +++ b/app/contacts/[id]/edit/page.tsx @@ -90,7 +90,7 @@ 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 2f68398..0e83d47 100644 --- a/app/contacts/page.tsx +++ b/app/contacts/page.tsx @@ -61,7 +61,7 @@ export default function ContactsPage() { if (loading) { return ( -
+
@@ -79,7 +79,7 @@ export default function ContactsPage() { if (error) { return ( -
+

Error

{error}

@@ -99,7 +99,7 @@ export default function ContactsPage() { } return ( -
+

Contacts

+ {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 2436458..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/TopNav.tsx b/app/components/TopNav.tsx index 7a3d8bd..3d5c4cc 100644 --- a/app/components/TopNav.tsx +++ b/app/components/TopNav.tsx @@ -29,71 +29,75 @@ export default function TopNav() { }; return ( -