civicsetu / frontend /src /components /InputBar.tsx
adeshboudh16
frontend: sync hf-safe logo and hydration updates
1714902
'use client';
import { useEffect, useRef, useState } from 'react';
import type { Jurisdiction } from '@/lib/types';
const JURISDICTIONS = [
{ value: '' as const, label: 'All Jurisdictions', shortLabel: 'All', dotClass: 'bg-white/35' },
{ value: 'CENTRAL' as const, label: 'Central', shortLabel: 'Central', dotClass: 'bg-[#2db7a3]' },
{ value: 'MAHARASHTRA' as const, label: 'Maharashtra', shortLabel: 'Maharashtra', dotClass: 'bg-[#4e7cff]' },
{ value: 'UTTAR_PRADESH' as const, label: 'Uttar Pradesh', shortLabel: 'Uttar Pradesh', dotClass: 'bg-[#f4b63f]' },
{ value: 'KARNATAKA' as const, label: 'Karnataka', shortLabel: 'Karnataka', dotClass: 'bg-[#78b94d]' },
{ value: 'TAMIL_NADU' as const, label: 'Tamil Nadu', shortLabel: 'Tamil Nadu', dotClass: 'bg-[#ff7a59]' },
];
interface Props {
onSend: (query: string, jurisdiction: Jurisdiction | '') => void;
disabled: boolean;
pendingQuery?: string;
}
export function InputBar({ onSend, disabled, pendingQuery }: Props) {
const [query, setQuery] = useState('');
const [jurisdiction, setJurisdiction] = useState<Jurisdiction | ''>('');
const [isJurisdictionOpen, setIsJurisdictionOpen] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const jurisdictionMenuRef = useRef<HTMLDivElement>(null);
const MAX_VISIBLE_LINES = 9;
const selectedJurisdiction = JURISDICTIONS.find(item => item.value === jurisdiction) ?? JURISDICTIONS[0];
useEffect(() => {
if (!pendingQuery) {
return;
}
setQuery(pendingQuery);
// Use a slightly longer delay or multiple frames to ensure the DOM has updated
// and the layout has settled before calculating height.
setTimeout(() => {
textareaRef.current?.focus();
autoResize();
}, 0);
}, [pendingQuery]);
// Also trigger autoResize when the query state changes to ensure it's always in sync
useEffect(() => {
autoResize();
}, [query]);
useEffect(() => {
if (!isJurisdictionOpen) {
return;
}
function handlePointerDown(event: PointerEvent) {
if (!jurisdictionMenuRef.current?.contains(event.target as Node)) {
setIsJurisdictionOpen(false);
}
}
window.addEventListener('pointerdown', handlePointerDown);
return () => window.removeEventListener('pointerdown', handlePointerDown);
}, [isJurisdictionOpen]);
function autoResize() {
const element = textareaRef.current;
if (!element) {
return;
}
const computed = window.getComputedStyle(element);
const lineHeight = Number.parseFloat(computed.lineHeight) || 24;
const maxHeight = lineHeight * MAX_VISIBLE_LINES;
element.style.height = 'auto';
element.style.height = `${Math.min(element.scrollHeight, maxHeight)}px`;
element.style.overflowY = element.scrollHeight > maxHeight ? 'auto' : 'hidden';
}
function handleInput(event: React.ChangeEvent<HTMLTextAreaElement>) {
setQuery(event.target.value);
autoResize();
}
function handleSend() {
const text = query.trim();
if (!text || disabled) {
return;
}
onSend(text, jurisdiction);
setQuery('');
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
textareaRef.current.style.overflowY = 'hidden';
}
}
function handleKeyDown(event: React.KeyboardEvent<HTMLTextAreaElement>) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
handleSend();
}
}
return (
<div className="shrink-0 bg-[#0d0d0d] p-3 max-lg:pb-24">
<div ref={jurisdictionMenuRef} className="relative mb-1 w-fit">
<button
type="button"
onClick={() => setIsJurisdictionOpen(value => !value)}
className="inline-flex h-7 items-center gap-2 rounded-[7px] border border-white/[0.07] bg-[#141414] px-2.5 font-mono text-[10px] uppercase tracking-[0.2em] text-white/50 transition-[background-color,border-color,color,transform] duration-150 ease-out hover:border-white/15 hover:bg-[#1a1a1a] hover:text-white/70 active:scale-[0.97]"
aria-haspopup="listbox"
aria-expanded={isJurisdictionOpen}
>
<span className={`h-1.5 w-1.5 rounded-full ${selectedJurisdiction.dotClass}`} />
{selectedJurisdiction.shortLabel}
<span className={`text-white/25 transition-transform duration-150 ease-out ${isJurisdictionOpen ? 'rotate-180' : ''}`}>
<svg suppressHydrationWarning xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#e3e3e3"><path d="M480-384 288-576h384L480-384Z"/></svg>
</span>
</button>
{isJurisdictionOpen ? (
<div
role="listbox"
className="absolute bottom-8 left-0 z-30 w-52 border border-white/[0.08] bg-[#141414]/98 p-1 shadow-[0_18px_50px_rgba(0,0,0,0.45)] backdrop-blur"
>
{JURISDICTIONS.map(item => (
<button
key={item.value}
type="button"
role="option"
aria-selected={jurisdiction === item.value}
onClick={() => {
setJurisdiction(item.value);
setIsJurisdictionOpen(false);
}}
className={`flex w-full items-center justify-between gap-3 rounded-[6px] px-2.5 py-2 text-left text-[12px] transition-[background-color,color,transform] duration-150 ease-out active:scale-[0.98] ${
jurisdiction === item.value
? 'bg-[#4f98a3]/12 text-[#9ed4dc]'
: 'text-white/60 hover:bg-white/[0.04] hover:text-white/80'
}`}
>
<span className="flex min-w-0 items-center gap-2">
<span className={`h-1.5 w-1.5 shrink-0 rounded-full ${item.dotClass}`} />
<span className="truncate">{item.label}</span>
</span>
{jurisdiction === item.value ? (
<span className={`h-1.5 w-1.5 shrink-0 rounded-full ${item.dotClass}`} />
) : null}
</button>
))}
</div>
) : null}
</div>
<div className="relative flex items-end">
<textarea
ref={textareaRef}
value={query}
onChange={handleInput}
onKeyDown={handleKeyDown}
rows={1}
placeholder="Ask about RERA regulations..."
className="ledger-scroll min-h-[40px] w-full resize-none overflow-hidden rounded-[10px] bg-[#1a1a1a] py-2 pl-3 pr-10 text-[14px] leading-6 text-white/85 outline-none transition-[border-color,height] duration-150 ease-out placeholder:text-white/25 focus:ring-1 focus:ring-[#4f98a3]/50"
/>
<button
type="button"
onClick={handleSend}
disabled={disabled || !query.trim()}
aria-label="Send message"
className="absolute bottom-2 right-3 text-[#4f98a3] transition-[opacity,transform] duration-150 ease-out hover:opacity-80 active:scale-[0.97] disabled:opacity-25 disabled:active:scale-100"
>
<svg suppressHydrationWarning xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="20px" fill="#e3e3e3"><path d="M144-192v-576l720 288-720 288Zm72-107 454-181-454-181v109l216 72-216 72v109Zm0 0v-362 362Z"/></svg> </button>
</div>
</div>
);
}