Sifa professional network frontend (Next.js, React, TailwindCSS) sifa.id/
at main 150 lines 4.6 kB view raw
1'use client'; 2 3import { useCallback, useEffect, useState } from 'react'; 4import { useTranslations } from 'next-intl'; 5import { toast } from 'sonner'; 6import { NotePencil, PencilSimple } from '@phosphor-icons/react'; 7import { Button } from '@/components/ui/button'; 8import { useAuth } from '@/components/auth-provider'; 9 10const API_URL = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3100'; 11 12interface ProfileNoteProps { 13 did: string; 14} 15 16export function ProfileNote({ did }: ProfileNoteProps) { 17 const tn = useTranslations('peopleNotes'); 18 const { session } = useAuth(); 19 const [note, setNote] = useState<string | null>(null); 20 const [loading, setLoading] = useState(true); 21 const [editing, setEditing] = useState(false); 22 const [content, setContent] = useState(''); 23 const [saving, setSaving] = useState(false); 24 25 useEffect(() => { 26 if (!session) { 27 setLoading(false); 28 return; 29 } 30 31 fetch(`${API_URL}/api/notes/${encodeURIComponent(did)}`, { 32 credentials: 'include', 33 }) 34 .then(async (res) => { 35 if (res.ok) { 36 const data = (await res.json()) as { content: string }; 37 setNote(data.content); 38 setContent(data.content); 39 } 40 }) 41 .catch(() => {}) 42 .finally(() => setLoading(false)); 43 }, [did, session]); 44 45 const handleSave = useCallback(async () => { 46 setSaving(true); 47 try { 48 const res = await fetch(`${API_URL}/api/notes/${encodeURIComponent(did)}`, { 49 method: 'PUT', 50 headers: { 'Content-Type': 'application/json' }, 51 credentials: 'include', 52 body: JSON.stringify({ content: content.trim() }), 53 }); 54 if (res.ok) { 55 toast.success(tn('noteSaved')); 56 setNote(content.trim()); 57 setEditing(false); 58 } 59 } catch { 60 toast.error('Failed to save'); 61 } finally { 62 setSaving(false); 63 } 64 }, [content, did, tn]); 65 66 const handleDelete = useCallback(async () => { 67 try { 68 const res = await fetch(`${API_URL}/api/notes/${encodeURIComponent(did)}`, { 69 method: 'DELETE', 70 credentials: 'include', 71 }); 72 if (res.ok) { 73 toast.success(tn('noteDeleted')); 74 setNote(null); 75 setContent(''); 76 setEditing(false); 77 } 78 } catch { 79 toast.error('Failed to delete'); 80 } 81 }, [did, tn]); 82 83 // Don't render for own profile or when not authenticated 84 if (!session || loading || session.did === did) return null; 85 86 if (editing) { 87 return ( 88 <div className="mt-3 rounded-lg border border-border p-3"> 89 <textarea 90 value={content} 91 onChange={(e) => setContent(e.target.value)} 92 maxLength={500} 93 rows={3} 94 placeholder={tn('notePlaceholder')} 95 className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" 96 /> 97 <p className="mt-1 text-xs text-muted-foreground/70">{tn('notePrivacy')}</p> 98 <div className="mt-1 flex items-center justify-between"> 99 <span className="text-xs text-muted-foreground">{content.length}/500</span> 100 <div className="flex gap-2"> 101 {note && ( 102 <Button variant="ghost" size="sm" onClick={handleDelete}> 103 {tn('deleteNote')} 104 </Button> 105 )} 106 <Button 107 variant="outline" 108 size="sm" 109 onClick={() => { 110 setContent(note ?? ''); 111 setEditing(false); 112 }} 113 > 114 {tn('cancel')} 115 </Button> 116 <Button size="sm" onClick={handleSave} disabled={saving || !content.trim()}> 117 {saving ? '...' : tn('save')} 118 </Button> 119 </div> 120 </div> 121 </div> 122 ); 123 } 124 125 if (note) { 126 return ( 127 <button 128 onClick={() => setEditing(true)} 129 className="mt-3 flex w-full items-start gap-2 rounded-lg border border-border p-3 text-left hover:bg-muted/50" 130 > 131 <PencilSimple 132 className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" 133 weight="fill" 134 aria-hidden="true" 135 /> 136 <span className="text-sm text-foreground">{note}</span> 137 </button> 138 ); 139 } 140 141 return ( 142 <button 143 onClick={() => setEditing(true)} 144 className="mt-3 flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground" 145 > 146 <NotePencil className="h-4 w-4" weight="fill" aria-hidden="true" /> 147 <span>{tn('addNote')}</span> 148 </button> 149 ); 150}