Sifa professional network frontend (Next.js, React, TailwindCSS)
sifa.id/
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}