Spaces:
Running
Running
feat: Add Trusted Circle management UI in settings
Browse files- backend/routes/trusted_circle.py +33 -0
- frontend/app/settings/page.tsx +45 -2
- frontend/lib/api.ts +15 -0
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 |
|