E5K7 commited on
Commit
831ce9c
Β·
1 Parent(s): 4d8276f

feat: Add Trusted Circle management UI in settings

Browse files
backend/routes/trusted_circle.py CHANGED
@@ -92,3 +92,36 @@ async def invite_trusted_member(
92
  )
93
 
94
  return {"success": True, "message": "Invitation sent successfully."}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  )
93
 
94
  return {"success": True, "message": "Invitation sent successfully."}
95
+
96
+ @router.get("/trusted-circle")
97
+ async def get_trusted_members(
98
+ current_user: User = Depends(get_current_user),
99
+ db: Session = Depends(get_db)
100
+ ):
101
+ members = db.query(TrustedMember).filter(
102
+ TrustedMember.user_id == current_user.id,
103
+ TrustedMember.is_active == True
104
+ ).order_by(TrustedMember.joined_at.desc()).all()
105
+
106
+ return [{"id": m.id, "email": m.email, "joined_at": m.joined_at.isoformat()} for m in members]
107
+
108
+ @router.delete("/trusted-circle/{member_id}")
109
+ async def remove_trusted_member(
110
+ member_id: int,
111
+ current_user: User = Depends(get_current_user),
112
+ db: Session = Depends(get_db)
113
+ ):
114
+ member = db.query(TrustedMember).filter(
115
+ TrustedMember.id == member_id,
116
+ TrustedMember.user_id == current_user.id
117
+ ).first()
118
+
119
+ if not member:
120
+ raise HTTPException(
121
+ status_code=status.HTTP_404_NOT_FOUND,
122
+ detail="Member not found."
123
+ )
124
+
125
+ db.delete(member)
126
+ db.commit()
127
+ return {"success": True, "message": "Member removed."}
frontend/app/settings/page.tsx CHANGED
@@ -1,8 +1,8 @@
1
  "use client";
2
  import { useTheme } from "@/contexts/ThemeContext";
3
  import { useAuth } from "@/contexts/AuthContext";
4
- import { Moon, Sun, LogOut } from "lucide-react";
5
- import { useState } from "react";
6
  import { api } from "@/lib/api";
7
 
8
  export default function SettingsPage() {
@@ -12,6 +12,22 @@ export default function SettingsPage() {
12
  const [isInviting, setIsInviting] = useState(false);
13
  const [inviteSuccess, setInviteSuccess] = useState(false);
14
  const [inviteError, setInviteError] = useState("");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  const handleInvite = async () => {
17
  if (!trustedEmail) return;
@@ -21,6 +37,7 @@ export default function SettingsPage() {
21
  await api.inviteTrustedMember(trustedEmail);
22
  setInviteSuccess(true);
23
  setTrustedEmail("");
 
24
  setTimeout(() => setInviteSuccess(false), 3000);
25
  } catch (e: any) {
26
  setInviteError(e.message || "Failed to send invitation.");
@@ -29,6 +46,15 @@ export default function SettingsPage() {
29
  }
30
  };
31
 
 
 
 
 
 
 
 
 
 
32
  return (
33
  <div className="p-6 md:p-10 pt-20 md:pt-10 max-w-2xl mx-auto space-y-8 min-h-screen">
34
  <h1 className="text-3xl font-bold">Settings</h1>
@@ -124,6 +150,23 @@ export default function SettingsPage() {
124
  </button>
125
  </div>
126
  {inviteError && <p className="text-red-400 text-xs mt-2">{inviteError}</p>}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  </section>
128
 
129
  {/* Crisis Resources */}
 
1
  "use client";
2
  import { useTheme } from "@/contexts/ThemeContext";
3
  import { useAuth } from "@/contexts/AuthContext";
4
+ import { Moon, Sun, LogOut, Trash2 } from "lucide-react";
5
+ import { useState, useEffect } from "react";
6
  import { api } from "@/lib/api";
7
 
8
  export default function SettingsPage() {
 
12
  const [isInviting, setIsInviting] = useState(false);
13
  const [inviteSuccess, setInviteSuccess] = useState(false);
14
  const [inviteError, setInviteError] = useState("");
15
+ const [trustedMembers, setTrustedMembers] = useState<{id: number, email: string}[]>([]);
16
+
17
+ const fetchMembers = async () => {
18
+ try {
19
+ const data = await api.getTrustedMembers();
20
+ setTrustedMembers(data);
21
+ } catch (e) {
22
+ console.error("Failed to load trusted members", e);
23
+ }
24
+ };
25
+
26
+ useEffect(() => {
27
+ if (user) {
28
+ fetchMembers();
29
+ }
30
+ }, [user]);
31
 
32
  const handleInvite = async () => {
33
  if (!trustedEmail) return;
 
37
  await api.inviteTrustedMember(trustedEmail);
38
  setInviteSuccess(true);
39
  setTrustedEmail("");
40
+ fetchMembers();
41
  setTimeout(() => setInviteSuccess(false), 3000);
42
  } catch (e: any) {
43
  setInviteError(e.message || "Failed to send invitation.");
 
46
  }
47
  };
48
 
49
+ const handleRemoveMember = async (id: number) => {
50
+ try {
51
+ await api.removeTrustedMember(id);
52
+ fetchMembers();
53
+ } catch (e) {
54
+ console.error("Failed to remove member", e);
55
+ }
56
+ };
57
+
58
  return (
59
  <div className="p-6 md:p-10 pt-20 md:pt-10 max-w-2xl mx-auto space-y-8 min-h-screen">
60
  <h1 className="text-3xl font-bold">Settings</h1>
 
150
  </button>
151
  </div>
152
  {inviteError && <p className="text-red-400 text-xs mt-2">{inviteError}</p>}
153
+
154
+ {trustedMembers.length > 0 && (
155
+ <div className="mt-6 space-y-2">
156
+ <div className="text-xs uppercase tracking-widest text-white/30 font-semibold mb-3">Active Members</div>
157
+ {trustedMembers.map((member) => (
158
+ <div key={member.id} className="flex items-center justify-between bg-white/5 border border-white/10 rounded-lg p-3">
159
+ <span className="text-sm text-white/80">{member.email}</span>
160
+ <button
161
+ onClick={() => handleRemoveMember(member.id)}
162
+ className="p-1.5 text-white/40 hover:text-red-400 hover:bg-red-400/10 rounded-md transition-colors"
163
+ >
164
+ <Trash2 className="w-4 h-4" />
165
+ </button>
166
+ </div>
167
+ ))}
168
+ </div>
169
+ )}
170
  </section>
171
 
172
  {/* Crisis Resources */}
frontend/lib/api.ts CHANGED
@@ -36,6 +36,15 @@ export async function apiPut<T>(path: string, body?: unknown): Promise<T> {
36
  return res.json();
37
  }
38
 
 
 
 
 
 
 
 
 
 
39
  // ── Typed API functions ───────────────────────────────────────────────────────
40
 
41
  export interface VoiceEntry {
@@ -144,6 +153,12 @@ export const api = {
144
  inviteTrustedMember: (email: string) =>
145
  apiPost<{ success: boolean; message: string }>("/api/trusted-circle/invite", { email }),
146
 
 
 
 
 
 
 
147
  broadcastWeeklyReport: () =>
148
  apiPost<{ success: boolean; sent_count: number; errors?: string[] }>("/api/weekly-report/broadcast", {}),
149
 
 
36
  return res.json();
37
  }
38
 
39
+ export async function apiDelete<T>(path: string): Promise<T> {
40
+ const res = await fetch(`${API_URL}${path}`, {
41
+ method: "DELETE",
42
+ headers: { ...getAuthHeader() },
43
+ });
44
+ if (!res.ok) throw new Error(`API Error ${res.status}: ${await res.text()}`);
45
+ return res.json();
46
+ }
47
+
48
  // ── Typed API functions ───────────────────────────────────────────────────────
49
 
50
  export interface VoiceEntry {
 
153
  inviteTrustedMember: (email: string) =>
154
  apiPost<{ success: boolean; message: string }>("/api/trusted-circle/invite", { email }),
155
 
156
+ getTrustedMembers: () =>
157
+ apiGet<{ id: number; email: string; joined_at: string }[]>("/api/trusted-circle"),
158
+
159
+ removeTrustedMember: (id: number) =>
160
+ apiDelete<{ success: boolean; message: string }>(`/api/trusted-circle/${id}`),
161
+
162
  broadcastWeeklyReport: () =>
163
  apiPost<{ success: boolean; sent_count: number; errors?: string[] }>("/api/weekly-report/broadcast", {}),
164