1- // Stub — will be fully implemented in Task 4
1+ import { useQuery } from "@tanstack/react-query"
2+ import { getAuthToken } from "@/clients/auth-helper"
3+
4+ // ── Types ────────────────────────────────────────────────────────────────────
5+
6+ interface QueryDrillDownResponse {
7+ queries : Array < {
8+ prompt_text : string
9+ brand_rank_today : number
10+ brand_rank_7d_ago : number
11+ rank_change : number
12+ top_competitor_today : string | null
13+ top_competitor_rank : number | null
14+ } >
15+ }
16+
217interface Props {
318 signal_type : string
419 brand_id : number
@@ -7,6 +22,151 @@ interface Props {
722 action_type : string
823}
924
10- export default function QueryDrillDown ( _props : Props ) {
11- return null
25+ // ── API helpers ──────────────────────────────────────────────────────────────
26+
27+ const API_BASE_URL = import . meta. env . VITE_API_URL ?? ""
28+ const API_PREFIX = "/api/v1"
29+
30+ async function fetchQueryDrillDown (
31+ signal_type : string ,
32+ brand_id : number ,
33+ segment : string ,
34+ date : string ,
35+ action_type : string ,
36+ ) : Promise < QueryDrillDownResponse > {
37+ const token = getAuthToken ( )
38+ const params = new URLSearchParams ( {
39+ brand_id : String ( brand_id ) ,
40+ segment,
41+ date,
42+ action_type,
43+ } )
44+ const url = `${ API_BASE_URL } ${ API_PREFIX } /insights/signal/${ encodeURIComponent ( signal_type ) } /queries?${ params . toString ( ) } `
45+ const res = await fetch ( url , {
46+ headers : {
47+ ...( token ? { Authorization : `Bearer ${ token } ` } : { } ) ,
48+ "Content-Type" : "application/json" ,
49+ } ,
50+ } )
51+ if ( ! res . ok ) {
52+ throw new Error ( `Failed to fetch query drill-down: ${ res . status } ` )
53+ }
54+ return res . json ( ) as Promise < QueryDrillDownResponse >
55+ }
56+
57+ // ── Rank change helpers ──────────────────────────────────────────────────────
58+
59+ function rankChangeColor ( change : number ) : string {
60+ if ( change < 0 ) return "text-red-400"
61+ if ( change > 0 ) return "text-green-400"
62+ return "text-slate-500"
63+ }
64+
65+ function rankChangeLabel ( change : number ) : string {
66+ if ( change > 0 ) return `+${ change } `
67+ if ( change < 0 ) return String ( change )
68+ return "0"
69+ }
70+
71+ // ── Main component ───────────────────────────────────────────────────────────
72+
73+ export default function QueryDrillDown ( {
74+ signal_type,
75+ brand_id,
76+ segment,
77+ date,
78+ action_type,
79+ } : Props ) {
80+ const { data, isLoading, isError } = useQuery < QueryDrillDownResponse > ( {
81+ queryKey : [ "query-drilldown" , signal_type , brand_id , segment , date , action_type ] ,
82+ queryFn : ( ) => fetchQueryDrillDown ( signal_type , brand_id , segment , date , action_type ) ,
83+ enabled : true ,
84+ } )
85+
86+ if ( isLoading ) {
87+ return (
88+ < div className = "flex items-center gap-2 py-3 text-slate-500 text-xs" >
89+ < svg
90+ className = "animate-spin h-3.5 w-3.5 text-slate-400"
91+ xmlns = "http://www.w3.org/2000/svg"
92+ fill = "none"
93+ viewBox = "0 0 24 24"
94+ aria-hidden = "true"
95+ >
96+ < circle
97+ className = "opacity-25"
98+ cx = "12"
99+ cy = "12"
100+ r = "10"
101+ stroke = "currentColor"
102+ strokeWidth = "4"
103+ />
104+ < path
105+ className = "opacity-75"
106+ fill = "currentColor"
107+ d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
108+ />
109+ </ svg >
110+ Loading queries…
111+ </ div >
112+ )
113+ }
114+
115+ if ( isError ) {
116+ return (
117+ < div className = "py-3 text-xs text-red-400" >
118+ Failed to load queries. Please try again.
119+ </ div >
120+ )
121+ }
122+
123+ if ( ! data || data . queries . length === 0 ) {
124+ return (
125+ < div className = "py-3 text-xs text-slate-500" >
126+ No query data available.
127+ </ div >
128+ )
129+ }
130+
131+ return (
132+ < div className = "rounded-lg border border-slate-700 overflow-hidden" >
133+ < table className = "w-full text-xs" >
134+ < thead >
135+ < tr className = "border-b border-slate-700 bg-slate-800/60" >
136+ < th className = "px-3 py-2 text-left text-slate-500 font-medium" > Query</ th >
137+ < th className = "px-3 py-2 text-right text-slate-500 font-medium" > Brand Rank</ th >
138+ < th className = "px-3 py-2 text-right text-slate-500 font-medium" > 7d Ago</ th >
139+ < th className = "px-3 py-2 text-right text-slate-500 font-medium" > Change</ th >
140+ < th className = "px-3 py-2 text-left text-slate-500 font-medium" > Top Competitor</ th >
141+ </ tr >
142+ </ thead >
143+ < tbody >
144+ { data . queries . map ( ( q , i ) => (
145+ < tr
146+ // biome-ignore lint/suspicious/noArrayIndexKey: stable list from API
147+ key = { i }
148+ className = "border-b border-slate-800 last:border-0 hover:bg-slate-800/40 transition-colors"
149+ >
150+ < td
151+ className = "px-3 py-2 text-slate-300 max-w-[200px] truncate"
152+ title = { q . prompt_text }
153+ >
154+ { q . prompt_text }
155+ </ td >
156+ < td className = "px-3 py-2 text-right text-slate-400" > { q . brand_rank_today } </ td >
157+ < td className = "px-3 py-2 text-right text-slate-400" > { q . brand_rank_7d_ago } </ td >
158+ < td className = { `px-3 py-2 text-right font-semibold ${ rankChangeColor ( q . rank_change ) } ` } >
159+ { rankChangeLabel ( q . rank_change ) }
160+ </ td >
161+ < td className = "px-3 py-2 text-slate-400 max-w-[140px] truncate" >
162+ { q . top_competitor_today
163+ ? `${ q . top_competitor_today } ${ q . top_competitor_rank !== null ? ` (#${ q . top_competitor_rank } )` : "" } `
164+ : < span className = "text-slate-600" > —</ span > }
165+ </ td >
166+ </ tr >
167+ ) ) }
168+ </ tbody >
169+ </ table >
170+ </ div >
171+ )
12172}
0 commit comments