11"use client" ;
22
3+ import { useCallback } from "react" ;
34import StatCard from "@/components/StatCard" ;
45import { api } from "@/lib/api" ;
56import { usePolling } from "@/lib/use-polling" ;
@@ -13,6 +14,30 @@ function fmtUsdc(value: number): string {
1314 ) ;
1415}
1516
17+ function maskEmail ( email : string ) : string {
18+ const [ local , domain ] = email . split ( "@" ) ;
19+ if ( ! domain ) return "***" ;
20+ const masked = local . length > 1 ? local [ 0 ] + "***" : "*" ;
21+ return `${ masked } @${ domain } ` ;
22+ }
23+
24+ function maskPhone ( phone : string ) : string {
25+ if ( phone . length <= 4 ) return "***" ;
26+ return phone . slice ( 0 , 3 ) + "***" + phone . slice ( - 2 ) ;
27+ }
28+
29+ function downloadCSV ( rows : string [ ] , filename : string ) : void {
30+ const blob = new Blob ( [ rows . join ( "\n" ) ] , { type : "text/csv;charset=utf-8;" } ) ;
31+ const url = URL . createObjectURL ( blob ) ;
32+ const link = document . createElement ( "a" ) ;
33+ link . href = url ;
34+ link . download = filename ;
35+ document . body . appendChild ( link ) ;
36+ link . click ( ) ;
37+ document . body . removeChild ( link ) ;
38+ URL . revokeObjectURL ( url ) ;
39+ }
40+
1641export default function OverviewPage ( ) {
1742 const { data : feedData , loading : feedLoading , error : feedError } = usePolling (
1843 ( ) => api . socialFeed ( ) ,
@@ -37,16 +62,50 @@ export default function OverviewPage() {
3762 const yieldDistributed = yieldData ?. total_yield_distributed ?? 0 ;
3863 const apy = yieldData ?. apy ?? 0 ;
3964
65+ const handleExportUsers = useCallback ( async ( ) => {
66+ try {
67+ const links = await api . identityLinks ( ) ;
68+ const headers = [ "User ID" , "Privy DID" , "Stellar Address" , "Display Name" , "Email" , "Phone" , "Status" , "Linked At" ] ;
69+ const csvRows = [ headers . join ( "," ) ] ;
70+ for ( const link of links . links ) {
71+ const email = link . email ? maskEmail ( link . email ) : "" ;
72+ const phone = link . display_name ? maskPhone ( link . display_name ) : "" ;
73+ csvRows . push ( [
74+ link . user_id ,
75+ link . privy_did ,
76+ link . stellar_address ,
77+ link . display_name ?? "" ,
78+ email ,
79+ phone ,
80+ link . status ,
81+ link . linked_at ,
82+ ] . map ( ( v ) => `"${ v } "` ) . join ( "," ) ) ;
83+ }
84+ downloadCSV ( csvRows , `privy-users-export-${ new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) } .csv` ) ;
85+ } catch {
86+ // silently fail
87+ }
88+ } , [ ] ) ;
89+
4090 return (
4191 < div >
42- { /* Social Overview */ }
43- < div className = "mb-6" >
44- < h1 className = "text-2xl font-bold text-slate-900" > Social Overview</ h1 >
45- < p className = "mt-1 text-sm text-slate-500" >
46- Live engagement across recent payment feeds.
47- </ p >
92+ < div className = "flex items-center justify-between mb-6" >
93+ < div >
94+ < h1 className = "text-2xl font-bold text-slate-900" > Social Overview</ h1 >
95+ < p className = "mt-1 text-sm text-slate-500" >
96+ Live engagement across recent payment feeds.
97+ </ p >
98+ </ div >
99+ < button
100+ onClick = { handleExportUsers }
101+ className = "inline-flex items-center gap-2 rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 transition-colors"
102+ >
103+ < svg xmlns = "http://www.w3.org/2000/svg" width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" strokeLinecap = "round" strokeLinejoin = "round" > < path d = "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> < polyline points = "7 10 12 15 17 10" /> < line x1 = "12" x2 = "12" y1 = "15" y2 = "3" /> </ svg >
104+ Export Users (CSV)
105+ </ button >
48106 </ div >
49107
108+ { /* Social Overview */ }
50109 { feedError && (
51110 < div className = "mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-700" >
52111 { feedError } — showing the most recently loaded values
0 commit comments