grabbit 46d8af6084 🎉 Epic 2 Milestone: Successfully completed the final story of Epic 2: Commercialization & Core User Experience with full-stack date filtering functionality.
📋 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
2025-08-03 10:30:29 +08:00

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>
)
}