Download frontend/src/components/workspace/ActivityTimeline.jsx from Seth0330/qualifier: direct link, hf CLI and curl.
- Browser
- Download file 4.04 kB
-
https://huggingface.co/spaces/Seth0330/qualifier/resolve/main/frontend/src/components/workspace/ActivityTimeline.jsx
- Command line
-
hf download hf://spaces/Seth0330/qualifier/frontend/src/components/workspace/ActivityTimeline.jsx
-
curl -L -o ActivityTimeline.jsx https://huggingface.co/spaces/Seth0330/qualifier/resolve/main/frontend/src/components/workspace/ActivityTimeline.jsx
4.04 kB
| import React, { useEffect, useState } from 'react'; | |
| import { Phone, MapPin, Loader2 } from 'lucide-react'; | |
| import { apiFetch } from '@/lib/api'; | |
| function formatWhen(iso) { | |
| if (!iso) return ''; | |
| const d = new Date(iso); | |
| if (Number.isNaN(d.getTime())) return ''; | |
| return d.toLocaleString(undefined, { | |
| month: 'short', | |
| day: 'numeric', | |
| hour: 'numeric', | |
| minute: '2-digit', | |
| }); | |
| } | |
| /** | |
| * Chronological Call/Visit activity feed. Self-fetching — pass `endpoint` (the GET URL) and bump | |
| * `refreshKey` after logging a new activity elsewhere to re-fetch. | |
| */ | |
| export default function ActivityTimeline({ endpoint, refreshKey, heading = 'Activity timeline', className }) { | |
| const [activities, setActivities] = useState([]); | |
| const [loading, setLoading] = useState(true); | |
| useEffect(() => { | |
| if (!endpoint) return; | |
| let cancelled = false; | |
| setLoading(true); | |
| (async () => { | |
| try { | |
| const res = await apiFetch(endpoint); | |
| if (!res.ok) return; | |
| const data = await res.json(); | |
| if (!cancelled) setActivities(data.activities || []); | |
| } catch (e) { | |
| console.error(e); | |
| } finally { | |
| if (!cancelled) setLoading(false); | |
| } | |
| })(); | |
| return () => { | |
| cancelled = true; | |
| }; | |
| }, [endpoint, refreshKey]); | |
| if (!endpoint) return null; | |
| return ( | |
| <div className={className}> | |
| <h4 className="mb-3 font-semibold text-slate-800">{heading}</h4> | |
| {loading ? ( | |
| <div className="flex items-center gap-2 text-sm text-slate-500"> | |
| <Loader2 className="h-4 w-4 animate-spin" aria-hidden /> | |
| Loading… | |
| </div> | |
| ) : activities.length === 0 ? ( | |
| <p className="text-sm text-slate-500">No calls or visits logged yet.</p> | |
| ) : ( | |
| <ul className="space-y-2"> | |
| {activities.map((a) => ( | |
| <li | |
| key={a.id} | |
| className="flex gap-2.5 rounded-lg border border-slate-200 bg-white px-3 py-2" | |
| > | |
| <div | |
| className={ | |
| 'mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full ' + | |
| (a.activity_type === 'call' ? 'bg-emerald-100 text-emerald-700' : 'bg-violet-100 text-violet-700') | |
| } | |
| > | |
| {a.activity_type === 'call' ? ( | |
| <Phone className="h-3.5 w-3.5" aria-hidden /> | |
| ) : ( | |
| <MapPin className="h-3.5 w-3.5" aria-hidden /> | |
| )} | |
| </div> | |
| <div className="min-w-0 flex-1"> | |
| <p className="text-xs text-slate-500"> | |
| <span className="font-medium text-slate-700"> | |
| {a.activity_type === 'call' ? 'Call' : 'Visit'} | |
| </span> | |
| {a.contact_name ? <span> with {a.contact_name}</span> : null} | |
| {a.user_display_name ? <span> · {a.user_display_name}</span> : null} | |
| {a.created_at ? <span> · {formatWhen(a.created_at)}</span> : null} | |
| </p> | |
| {a.notes ? ( | |
| <p className="mt-0.5 whitespace-pre-wrap text-sm text-slate-800">{a.notes}</p> | |
| ) : null} | |
| </div> | |
| </li> | |
| ))} | |
| </ul> | |
| )} | |
| </div> | |
| ); | |
| } | |