Seth
Claude Sonnet 5
Add Call/Visit activity logging and timeline for contacts and companies
41c19e3 Download frontend/src/components/workspace/ContactActivityLogger.jsx from Seth0330/EMAILOUT: direct link, hf CLI and curl.
- Browser
- Download file 5.67 kB
-
https://huggingface.co/spaces/Seth0330/EMAILOUT/resolve/main/frontend/src/components/workspace/ContactActivityLogger.jsx
- Command line
-
hf download hf://spaces/Seth0330/EMAILOUT/frontend/src/components/workspace/ContactActivityLogger.jsx
-
curl -L -o ContactActivityLogger.jsx https://huggingface.co/spaces/Seth0330/EMAILOUT/resolve/main/frontend/src/components/workspace/ContactActivityLogger.jsx
5.67 kB
| import React, { useState } from 'react'; | |
| import { Phone, MapPin, Loader2, X } from 'lucide-react'; | |
| import { Button } from '@/components/ui/button'; | |
| import { apiFetch } from '@/lib/api'; | |
| import { cn } from '@/lib/utils'; | |
| /** | |
| * "Call" / "Visit" icon buttons for a contact. Clicking one opens an inline textarea to note | |
| * what was discussed; saving POSTs to /api/contacts/{contactId}/activities. Self-contained — | |
| * manages its own open/saving state, calls `onLogged` after a successful save so the parent can | |
| * refresh whatever activity timeline is showing. | |
| */ | |
| export default function ContactActivityLogger({ contactId, onLogged, className, iconSize = 'h-3.5 w-3.5' }) { | |
| const [openType, setOpenType] = useState(null); // 'call' | 'visit' | null | |
| const [notes, setNotes] = useState(''); | |
| const [saving, setSaving] = useState(false); | |
| const [error, setError] = useState(''); | |
| if (!contactId) return null; | |
| const toggle = (type) => (e) => { | |
| e.stopPropagation(); | |
| setError(''); | |
| setOpenType((cur) => (cur === type ? null : type)); | |
| setNotes(''); | |
| }; | |
| const cancel = (e) => { | |
| e.stopPropagation(); | |
| setOpenType(null); | |
| setNotes(''); | |
| setError(''); | |
| }; | |
| const save = async (e) => { | |
| e.stopPropagation(); | |
| if (!openType) return; | |
| setSaving(true); | |
| setError(''); | |
| try { | |
| const res = await apiFetch(`/api/contacts/${contactId}/activities`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ activity_type: openType, notes: notes.trim() }), | |
| }); | |
| const data = await res.json().catch(() => ({})); | |
| if (!res.ok) { | |
| setError(typeof data.detail === 'string' ? data.detail : 'Could not save activity'); | |
| return; | |
| } | |
| setOpenType(null); | |
| setNotes(''); | |
| onLogged && onLogged(data); | |
| } catch (err) { | |
| console.error(err); | |
| setError('Network error'); | |
| } finally { | |
| setSaving(false); | |
| } | |
| }; | |
| return ( | |
| <div className={className} onClick={(e) => e.stopPropagation()}> | |
| <div className="flex items-center gap-1"> | |
| <Button | |
| type="button" | |
| variant="ghost" | |
| size="icon" | |
| className={cn( | |
| 'h-7 w-7 text-slate-500 hover:text-emerald-700 hover:bg-emerald-50', | |
| openType === 'call' && 'bg-emerald-100 text-emerald-800 hover:bg-emerald-100' | |
| )} | |
| title="Log a call" | |
| aria-label="Log a call" | |
| onClick={toggle('call')} | |
| > | |
| <Phone className={iconSize} aria-hidden /> | |
| </Button> | |
| <Button | |
| type="button" | |
| variant="ghost" | |
| size="icon" | |
| className={cn( | |
| 'h-7 w-7 text-slate-500 hover:text-violet-700 hover:bg-violet-50', | |
| openType === 'visit' && 'bg-violet-100 text-violet-800 hover:bg-violet-100' | |
| )} | |
| title="Log a visit" | |
| aria-label="Log a visit" | |
| onClick={toggle('visit')} | |
| > | |
| <MapPin className={iconSize} aria-hidden /> | |
| </Button> | |
| </div> | |
| {openType ? ( | |
| <div className="mt-2 w-72 rounded-lg border border-slate-200 bg-white p-2 shadow-sm"> | |
| <div className="mb-1 flex items-center justify-between"> | |
| <span className="text-xs font-medium text-slate-600"> | |
| {openType === 'call' ? 'What did you discuss on the call?' : 'What did you discuss on the visit?'} | |
| </span> | |
| <button | |
| type="button" | |
| className="text-slate-400 hover:text-slate-600" | |
| onClick={cancel} | |
| aria-label="Cancel" | |
| > | |
| <X className="h-3.5 w-3.5" aria-hidden /> | |
| </button> | |
| </div> | |
| <textarea | |
| autoFocus | |
| rows={2} | |
| className="w-full resize-none rounded-md border border-slate-200 px-2 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-violet-400" | |
| placeholder="Notes (optional)" | |
| value={notes} | |
| onChange={(e) => setNotes(e.target.value)} | |
| disabled={saving} | |
| onClick={(e) => e.stopPropagation()} | |
| /> | |
| {error ? <p className="mt-1 text-xs text-red-600">{error}</p> : null} | |
| <div className="mt-1.5 flex justify-end gap-1.5"> | |
| <Button type="button" size="sm" variant="outline" className="h-7 px-2 text-xs" onClick={cancel} disabled={saving}> | |
| Cancel | |
| </Button> | |
| <Button type="button" size="sm" className="h-7 px-2 text-xs" onClick={save} disabled={saving}> | |
| {saving ? <Loader2 className="h-3.5 w-3.5 animate-spin" aria-hidden /> : 'Save'} | |
| </Button> | |
| </div> | |
| </div> | |
| ) : null} | |
| </div> | |
| ); | |
| } | |