📋 What Was Accomplished Backend Changes: - ✅ Enhanced API Endpoint: Updated GET /api/v1/events to accept optional date query parameter - ✅ Input Validation: Added YYYY-MM-DD format validation to PaginationQueryDto - ✅ Database Filtering: Implemented timezone-aware date filtering in EventsService - ✅ Controller Integration: Updated EventsController to pass date parameter to service Frontend Changes: - ✅ Date Picker Component: Created reusable DatePicker component following project design system - ✅ Gallery UI Enhancement: Integrated date picker into gallery page with clear labeling - ✅ State Management: Implemented reactive date state with automatic re-fetching - ✅ Clear Filter Functionality: Added "Clear Filter" button for easy reset - ✅ Enhanced UX: Improved empty states for filtered vs unfiltered views 🔍 Technical Implementation API Design: GET /api/v1/events?date=2025-08-02&limit=20&cursor=xxx Key Files Modified: - meteor-web-backend/src/events/dto/pagination-query.dto.ts - meteor-web-backend/src/events/events.service.ts - meteor-web-backend/src/events/events.controller.ts - meteor-frontend/src/components/ui/date-picker.tsx (new) - meteor-frontend/src/app/gallery/page.tsx - meteor-frontend/src/hooks/use-events.ts - meteor-frontend/src/services/events.ts ✅ All Acceptance Criteria Met 1. ✅ Backend API Enhancement: Accepts optional date parameter 2. ✅ Date Filtering Logic: Returns events for specific calendar date 3. ✅ Date Picker UI: Clean, accessible interface component 4. ✅ Automatic Re-fetching: Immediate data updates on date selection 5. ✅ Filtered Display: Correctly shows only events for selected date 6. ✅ Clear Filter: One-click reset to view all events 🧪 Quality Assurance - ✅ Backend Build: Successful compilation with no errors - ✅ Frontend Build: Successful Next.js build with no warnings - ✅ Linting: All ESLint checks pass - ✅ Functionality: Feature working as specified 🎉 Epic 2 Complete! With Story 2.9 completion, Epic 2: Commercialization & Core User Experience is now DONE! Epic 2 Achievements: - 🔐 Full-stack device status monitoring - 💳 Robust payment and subscription system - 🛡️ Subscription-based access control - 📊 Enhanced data browsing with detail pages - 📅 Date-based event filtering
81 lines
2.5 KiB
TypeScript
81 lines
2.5 KiB
TypeScript
import * as React from "react"
|
|
|
|
interface MetadataDisplayProps {
|
|
metadata: Record<string, unknown>
|
|
}
|
|
|
|
export function MetadataDisplay({ metadata }: MetadataDisplayProps) {
|
|
if (!metadata || Object.keys(metadata).length === 0) {
|
|
return (
|
|
<div className="bg-gray-900 rounded-lg p-6">
|
|
<h2 className="text-xl font-semibold mb-4 text-white">Metadata</h2>
|
|
<p className="text-gray-400 text-sm">No metadata available for this event.</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const formatKey = (key: string): string => {
|
|
return key
|
|
.replace(/([A-Z])/g, ' $1') // Add space before capital letters
|
|
.replace(/^./, str => str.toUpperCase()) // Capitalize first letter
|
|
.replace(/_/g, ' ') // Replace underscores with spaces
|
|
}
|
|
|
|
const formatValue = (value: unknown): string => {
|
|
if (value === null || value === undefined) {
|
|
return 'N/A'
|
|
}
|
|
|
|
if (typeof value === 'boolean') {
|
|
return value ? 'Yes' : 'No'
|
|
}
|
|
|
|
if (typeof value === 'object') {
|
|
try {
|
|
return JSON.stringify(value, null, 2)
|
|
} catch {
|
|
return String(value)
|
|
}
|
|
}
|
|
|
|
// Check if it's a date string
|
|
if (typeof value === 'string' && value.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)) {
|
|
try {
|
|
return new Date(value).toLocaleString()
|
|
} catch {
|
|
return value
|
|
}
|
|
}
|
|
|
|
return String(value)
|
|
}
|
|
|
|
const isLongValue = (value: unknown): boolean => {
|
|
const stringValue = formatValue(value)
|
|
return stringValue.length > 50 || stringValue.includes('\n')
|
|
}
|
|
|
|
return (
|
|
<div className="bg-gray-900 rounded-lg p-6">
|
|
<h2 className="text-xl font-semibold mb-4 text-white">Metadata</h2>
|
|
<div className="space-y-3">
|
|
{Object.entries(metadata).map(([key, value]) => (
|
|
<div key={key} className="border-b border-gray-800 pb-3 last:border-b-0">
|
|
<label className="text-sm text-gray-500 font-medium block mb-1">
|
|
{formatKey(key)}
|
|
</label>
|
|
<div className={`text-sm text-gray-300 ${isLongValue(value) ? 'whitespace-pre-wrap' : ''}`}>
|
|
{isLongValue(value) && typeof formatValue(value) === 'string' && formatValue(value).includes('{') ? (
|
|
<pre className="bg-gray-800 p-3 rounded text-xs overflow-x-auto">
|
|
{formatValue(value)}
|
|
</pre>
|
|
) : (
|
|
<span className="break-words">{formatValue(value)}</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
} |