feat(business): implement domain-specific BCT components
- Add EventCard component with comprehensive event display - Implement TicketTypeRow for ticket selection and pricing - Create OrderSummary for purchase flow display - Add FeeBreakdown for transparent pricing - Implement ScanStatusBadge for QR scanning interface - Include business type definitions and mock data Components provide realistic Black Canyon Tickets functionality with proper pricing display, event management, and ticketing flows. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,22 +1,23 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import type { Order, ScanStatus } from '../types/business';
|
import { MOCK_USERS } from '../types/auth';
|
||||||
import {
|
import {
|
||||||
MOCK_EVENTS,
|
MOCK_EVENTS,
|
||||||
MOCK_TICKET_TYPES,
|
MOCK_TICKET_TYPES,
|
||||||
DEFAULT_FEE_STRUCTURE
|
DEFAULT_FEE_STRUCTURE
|
||||||
} from '../types/business';
|
} from '../types/business';
|
||||||
|
|
||||||
import { MOCK_USERS } from '../types/auth';
|
|
||||||
|
|
||||||
import { EventCard } from './events';
|
|
||||||
import { TicketTypeRow } from './tickets';
|
|
||||||
import { OrderSummary } from './checkout';
|
|
||||||
import { ScanStatusBadge } from './scanning';
|
|
||||||
import { FeeBreakdown } from './billing';
|
import { FeeBreakdown } from './billing';
|
||||||
import { Card, CardHeader, CardBody } from './ui/Card';
|
import { OrderSummary } from './checkout';
|
||||||
import { Button } from './ui/Button';
|
import { EventCard } from './events';
|
||||||
import { MainContainer } from './layout';
|
import { MainContainer } from './layout';
|
||||||
|
import { ScanStatusBadge } from './scanning';
|
||||||
|
import { TicketTypeRow } from './tickets';
|
||||||
|
import { Button } from './ui/Button';
|
||||||
|
import { Card, CardHeader, CardBody } from './ui/Card';
|
||||||
|
|
||||||
|
import type { Order, ScanStatus } from '../types/business';
|
||||||
|
|
||||||
const DomainShowcase: React.FC = () => {
|
const DomainShowcase: React.FC = () => {
|
||||||
const [currentUser] = useState(MOCK_USERS[1]); // Organizer user
|
const [currentUser] = useState(MOCK_USERS[1]); // Organizer user
|
||||||
@@ -76,15 +77,15 @@ const DomainShowcase: React.FC = () => {
|
|||||||
updatedAt: new Date().toISOString()
|
updatedAt: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEventAction = (action: string, eventId: string) => {
|
const handleEventAction = (_action: string, _eventId: string) => {
|
||||||
// Handle event actions in real application
|
// Handle event actions in real application
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTicketAction = (action: string, ticketTypeId: string, value?: unknown) => {
|
const handleTicketAction = (_action: string, _ticketTypeId: string, _value?: unknown) => {
|
||||||
// Handle ticket type actions in real application
|
// Handle ticket type actions in real application
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePromoCode = async (code: string) => {
|
const handlePromoCode = async (_code: string) => {
|
||||||
// Apply promo code in real application
|
// Apply promo code in real application
|
||||||
// Simulate API call
|
// Simulate API call
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
@@ -92,11 +93,12 @@ const DomainShowcase: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const simulateScan = () => {
|
const simulateScan = () => {
|
||||||
|
const isValid = Math.random() > 0.5;
|
||||||
const newStatus: ScanStatus = {
|
const newStatus: ScanStatus = {
|
||||||
isValid: Math.random() > 0.5,
|
isValid,
|
||||||
status: Math.random() > 0.7 ? 'valid' : 'invalid',
|
status: Math.random() > 0.7 ? 'valid' : 'invalid',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
errorMessage: Math.random() > 0.5 ? 'Invalid QR format' : undefined,
|
...(isValid ? {} : { errorMessage: 'Invalid QR format' }),
|
||||||
ticketInfo: {
|
ticketInfo: {
|
||||||
eventTitle: 'Contemporary Dance Showcase',
|
eventTitle: 'Contemporary Dance Showcase',
|
||||||
ticketTypeName: 'General Admission',
|
ticketTypeName: 'General Admission',
|
||||||
@@ -129,7 +131,7 @@ const DomainShowcase: React.FC = () => {
|
|||||||
<EventCard
|
<EventCard
|
||||||
key={event.id}
|
key={event.id}
|
||||||
event={event}
|
event={event}
|
||||||
currentUser={currentUser}
|
{...(currentUser && { currentUser })}
|
||||||
onView={(id) => handleEventAction('view', id)}
|
onView={(id) => handleEventAction('view', id)}
|
||||||
onEdit={(id) => handleEventAction('edit', id)}
|
onEdit={(id) => handleEventAction('edit', id)}
|
||||||
onManage={(id) => handleEventAction('manage', id)}
|
onManage={(id) => handleEventAction('manage', id)}
|
||||||
@@ -154,7 +156,7 @@ const DomainShowcase: React.FC = () => {
|
|||||||
key={ticketType.id}
|
key={ticketType.id}
|
||||||
ticketType={ticketType}
|
ticketType={ticketType}
|
||||||
layout="card"
|
layout="card"
|
||||||
currentUser={currentUser}
|
{...(currentUser && { currentUser })}
|
||||||
onEdit={(tt) => handleTicketAction('edit', tt.id)}
|
onEdit={(tt) => handleTicketAction('edit', tt.id)}
|
||||||
onDelete={(id) => handleTicketAction('delete', id)}
|
onDelete={(id) => handleTicketAction('delete', id)}
|
||||||
onToggleStatus={(id, status) => handleTicketAction('toggle-status', id, status)}
|
onToggleStatus={(id, status) => handleTicketAction('toggle-status', id, status)}
|
||||||
@@ -190,7 +192,7 @@ const DomainShowcase: React.FC = () => {
|
|||||||
key={ticketType.id}
|
key={ticketType.id}
|
||||||
ticketType={ticketType}
|
ticketType={ticketType}
|
||||||
layout="table"
|
layout="table"
|
||||||
currentUser={currentUser}
|
{...(currentUser && { currentUser })}
|
||||||
onEdit={(tt) => handleTicketAction('edit', tt.id)}
|
onEdit={(tt) => handleTicketAction('edit', tt.id)}
|
||||||
onDelete={(id) => handleTicketAction('delete', id)}
|
onDelete={(id) => handleTicketAction('delete', id)}
|
||||||
onToggleStatus={(id, status) => handleTicketAction('toggle-status', id, status)}
|
onToggleStatus={(id, status) => handleTicketAction('toggle-status', id, status)}
|
||||||
@@ -289,21 +291,21 @@ const DomainShowcase: React.FC = () => {
|
|||||||
<h3 className="text-lg font-medium text-fg-primary">Badge Sizes</h3>
|
<h3 className="text-lg font-medium text-fg-primary">Badge Sizes</h3>
|
||||||
<div className="flex flex-wrap gap-4 items-center">
|
<div className="flex flex-wrap gap-4 items-center">
|
||||||
<ScanStatusBadge
|
<ScanStatusBadge
|
||||||
scanStatus={scanStatuses[0]}
|
scanStatus={scanStatuses[0]!}
|
||||||
size="sm"
|
size="sm"
|
||||||
showTimestamp={false}
|
showTimestamp={false}
|
||||||
showTicketInfo={false}
|
showTicketInfo={false}
|
||||||
animated={false}
|
animated={false}
|
||||||
/>
|
/>
|
||||||
<ScanStatusBadge
|
<ScanStatusBadge
|
||||||
scanStatus={scanStatuses[0]}
|
scanStatus={scanStatuses[0]!}
|
||||||
size="md"
|
size="md"
|
||||||
showTimestamp={false}
|
showTimestamp={false}
|
||||||
showTicketInfo={false}
|
showTicketInfo={false}
|
||||||
animated={false}
|
animated={false}
|
||||||
/>
|
/>
|
||||||
<ScanStatusBadge
|
<ScanStatusBadge
|
||||||
scanStatus={scanStatuses[0]}
|
scanStatus={scanStatuses[0]!}
|
||||||
size="lg"
|
size="lg"
|
||||||
showTimestamp={false}
|
showTimestamp={false}
|
||||||
showTicketInfo={false}
|
showTicketInfo={false}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { FeeStructure, Order, DEFAULT_FEE_STRUCTURE } from '../../types/business';
|
|
||||||
import { Card, CardHeader, CardBody } from '../ui/Card';
|
import { DEFAULT_FEE_STRUCTURE } from '../../types/business';
|
||||||
import { Button } from '../ui/Button';
|
|
||||||
import { Badge } from '../ui/Badge';
|
import { Badge } from '../ui/Badge';
|
||||||
|
import { Button } from '../ui/Button';
|
||||||
|
import { Card, CardHeader, CardBody } from '../ui/Card';
|
||||||
|
|
||||||
|
import type { FeeStructure, Order} from '../../types/business';
|
||||||
|
|
||||||
export interface FeeBreakdownProps {
|
export interface FeeBreakdownProps {
|
||||||
order: Order;
|
order: Order;
|
||||||
@@ -37,7 +40,7 @@ const Tooltip: React.FC<TooltipProps> = ({ content, children }) => {
|
|||||||
{isVisible && (
|
{isVisible && (
|
||||||
<div className="absolute z-10 bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 bg-bg-primary border border-border-subtle rounded-lg shadow-lg text-sm text-fg-secondary max-w-xs">
|
<div className="absolute z-10 bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 bg-bg-primary border border-border-subtle rounded-lg shadow-lg text-sm text-fg-secondary max-w-xs">
|
||||||
{content}
|
{content}
|
||||||
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-bg-primary"></div>
|
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-bg-primary" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -56,21 +59,17 @@ const FeeBreakdown: React.FC<FeeBreakdownProps> = ({
|
|||||||
const [isExpanded, setIsExpanded] = useState(layout === 'detailed');
|
const [isExpanded, setIsExpanded] = useState(layout === 'detailed');
|
||||||
|
|
||||||
// Format currency helper
|
// Format currency helper
|
||||||
const formatCurrency = (amountInCents: number) => {
|
const formatCurrency = (amountInCents: number) => new Intl.NumberFormat('en-US', {
|
||||||
return new Intl.NumberFormat('en-US', {
|
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: 'USD'
|
currency: 'USD'
|
||||||
}).format(amountInCents / 100);
|
}).format(amountInCents / 100);
|
||||||
};
|
|
||||||
|
|
||||||
// Format percentage helper
|
// Format percentage helper
|
||||||
const formatPercentage = (rate: number) => {
|
const formatPercentage = (rate: number) => `${(rate * 100).toFixed(2)}%`;
|
||||||
return `${(rate * 100).toFixed(2)}%`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calculate fee breakdown details
|
// Calculate fee breakdown details
|
||||||
const calculateFeeDetails = () => {
|
const calculateFeeDetails = () => {
|
||||||
const subtotal = order.subtotal;
|
const {subtotal} = order;
|
||||||
|
|
||||||
// Platform fee breakdown
|
// Platform fee breakdown
|
||||||
const platformFeeVariable = Math.round(subtotal * feeStructure.platformFeeRate);
|
const platformFeeVariable = Math.round(subtotal * feeStructure.platformFeeRate);
|
||||||
@@ -115,13 +114,11 @@ const FeeBreakdown: React.FC<FeeBreakdownProps> = ({
|
|||||||
const feeDetails = calculateFeeDetails();
|
const feeDetails = calculateFeeDetails();
|
||||||
|
|
||||||
// Compliance information
|
// Compliance information
|
||||||
const getComplianceInfo = () => {
|
const getComplianceInfo = () => ({
|
||||||
return {
|
|
||||||
platformFee: "Service fee for platform usage, event management tools, and customer support.",
|
platformFee: "Service fee for platform usage, event management tools, and customer support.",
|
||||||
processingFee: "Credit card processing fee charged by payment processor for secure transaction handling.",
|
processingFee: "Credit card processing fee charged by payment processor for secure transaction handling.",
|
||||||
tax: "Local sales tax as required by applicable tax authorities. Tax-exempt organizations may qualify for reduced rates."
|
tax: "Local sales tax as required by applicable tax authorities. Tax-exempt organizations may qualify for reduced rates."
|
||||||
};
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const compliance = getComplianceInfo();
|
const compliance = getComplianceInfo();
|
||||||
|
|
||||||
@@ -275,12 +272,12 @@ const FeeBreakdown: React.FC<FeeBreakdownProps> = ({
|
|||||||
<td className="border border-border-subtle px-4 py-2 font-semibold text-fg-primary">
|
<td className="border border-border-subtle px-4 py-2 font-semibold text-fg-primary">
|
||||||
Total Fees & Taxes
|
Total Fees & Taxes
|
||||||
</td>
|
</td>
|
||||||
<td className="border border-border-subtle px-4 py-2"></td>
|
<td className="border border-border-subtle px-4 py-2" />
|
||||||
<td className="border border-border-subtle px-4 py-2 text-right font-bold text-accent-primary">
|
<td className="border border-border-subtle px-4 py-2 text-right font-bold text-accent-primary">
|
||||||
{formatCurrency(order.platformFee + order.processingFee + order.tax)}
|
{formatCurrency(order.platformFee + order.processingFee + order.tax)}
|
||||||
</td>
|
</td>
|
||||||
{showCalculations && (
|
{showCalculations && (
|
||||||
<td className="border border-border-subtle px-4 py-2"></td>
|
<td className="border border-border-subtle px-4 py-2" />
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Order, FeeStructure, PromoCode, DEFAULT_FEE_STRUCTURE } from '../../types/business';
|
|
||||||
import { Card, CardHeader, CardBody, CardFooter } from '../ui/Card';
|
import { DEFAULT_FEE_STRUCTURE } from '../../types/business';
|
||||||
import { Button } from '../ui/Button';
|
|
||||||
import { Input } from '../ui/Input';
|
|
||||||
import { Badge } from '../ui/Badge';
|
|
||||||
import { Alert } from '../ui/Alert';
|
import { Alert } from '../ui/Alert';
|
||||||
|
import { Badge } from '../ui/Badge';
|
||||||
|
import { Button } from '../ui/Button';
|
||||||
|
import { Card, CardHeader, CardBody, CardFooter } from '../ui/Card';
|
||||||
|
import { Input } from '../ui/Input';
|
||||||
|
|
||||||
|
import type { Order, FeeStructure, PromoCode} from '../../types/business';
|
||||||
|
|
||||||
export interface OrderSummaryProps {
|
export interface OrderSummaryProps {
|
||||||
order: Order;
|
order: Order;
|
||||||
@@ -31,16 +34,14 @@ const OrderSummary: React.FC<OrderSummaryProps> = ({
|
|||||||
const [showFeeBreakdown, setShowFeeBreakdown] = useState(false);
|
const [showFeeBreakdown, setShowFeeBreakdown] = useState(false);
|
||||||
|
|
||||||
// Format currency helper
|
// Format currency helper
|
||||||
const formatCurrency = (amountInCents: number) => {
|
const formatCurrency = (amountInCents: number) => new Intl.NumberFormat('en-US', {
|
||||||
return new Intl.NumberFormat('en-US', {
|
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: 'USD'
|
currency: 'USD'
|
||||||
}).format(amountInCents / 100);
|
}).format(amountInCents / 100);
|
||||||
};
|
|
||||||
|
|
||||||
// Handle promo code application
|
// Handle promo code application
|
||||||
const handleApplyPromo = async () => {
|
const handleApplyPromo = async () => {
|
||||||
if (!promoCodeInput.trim() || !onPromoCodeApply) return;
|
if (!promoCodeInput.trim() || !onPromoCodeApply) {return;}
|
||||||
|
|
||||||
setIsApplyingPromo(true);
|
setIsApplyingPromo(true);
|
||||||
setPromoError(null);
|
setPromoError(null);
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Event, EventStats } from '../../types/business';
|
|
||||||
import { User } from '../../types/auth';
|
|
||||||
import { Card, CardBody, CardFooter } from '../ui/Card';
|
|
||||||
import { Button } from '../ui/Button';
|
|
||||||
import { Badge } from '../ui/Badge';
|
import { Badge } from '../ui/Badge';
|
||||||
|
import { Button } from '../ui/Button';
|
||||||
|
import { Card, CardBody, CardFooter } from '../ui/Card';
|
||||||
|
|
||||||
|
import type { User } from '../../types/auth';
|
||||||
|
import type { Event, EventStats } from '../../types/business';
|
||||||
|
|
||||||
export interface EventCardProps {
|
export interface EventCardProps {
|
||||||
event: Event;
|
event: Event;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { ScanStatus } from '../../types/business';
|
|
||||||
|
import type { ScanStatus } from '../../types/business';
|
||||||
|
|
||||||
export interface ScanStatusBadgeProps {
|
export interface ScanStatusBadgeProps {
|
||||||
scanStatus: ScanStatus;
|
scanStatus: ScanStatus;
|
||||||
@@ -30,6 +31,7 @@ const ScanStatusBadge: React.FC<ScanStatusBadgeProps> = ({
|
|||||||
const timer = setTimeout(() => setIsAnimating(false), 600);
|
const timer = setTimeout(() => setIsAnimating(false), 600);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
|
return undefined;
|
||||||
}, [scanStatus, animated]);
|
}, [scanStatus, animated]);
|
||||||
|
|
||||||
// Handle accessibility announcements
|
// Handle accessibility announcements
|
||||||
@@ -57,7 +59,7 @@ const ScanStatusBadge: React.FC<ScanStatusBadgeProps> = ({
|
|||||||
|
|
||||||
// Format timestamp
|
// Format timestamp
|
||||||
const formatTimestamp = (timestamp?: string) => {
|
const formatTimestamp = (timestamp?: string) => {
|
||||||
if (!timestamp) return null;
|
if (!timestamp) {return null;}
|
||||||
|
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -66,10 +68,10 @@ const ScanStatusBadge: React.FC<ScanStatusBadgeProps> = ({
|
|||||||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
if (diffMins < 1) return 'Just now';
|
if (diffMins < 1) {return 'Just now';}
|
||||||
if (diffMins < 60) return `${diffMins}m ago`;
|
if (diffMins < 60) {return `${diffMins}m ago`;}
|
||||||
if (diffHours < 24) return `${diffHours}h ago`;
|
if (diffHours < 24) {return `${diffHours}h ago`;}
|
||||||
if (diffDays < 7) return `${diffDays}d ago`;
|
if (diffDays < 7) {return `${diffDays}d ago`;}
|
||||||
|
|
||||||
return date.toLocaleDateString('en-US', {
|
return date.toLocaleDateString('en-US', {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { TicketType, TicketTypeStats } from '../../types/business';
|
|
||||||
import { User } from '../../types/auth';
|
|
||||||
import { Button } from '../ui/Button';
|
|
||||||
import { Badge } from '../ui/Badge';
|
import { Badge } from '../ui/Badge';
|
||||||
|
import { Button } from '../ui/Button';
|
||||||
import { Input } from '../ui/Input';
|
import { Input } from '../ui/Input';
|
||||||
|
|
||||||
|
import type { User } from '../../types/auth';
|
||||||
|
import type { TicketType, TicketTypeStats } from '../../types/business';
|
||||||
|
|
||||||
export interface TicketTypeRowProps {
|
export interface TicketTypeRowProps {
|
||||||
ticketType: TicketType;
|
ticketType: TicketType;
|
||||||
stats?: TicketTypeStats;
|
stats?: TicketTypeStats;
|
||||||
|
|||||||
Reference in New Issue
Block a user