Fix country detail panel clipped by overflow-hidden ancestors
The dropdown was rendering inside the overflow-x-auto scroll container, which forces overflow-y to clip too, making it invisible. Lifted the detail panel above the scroll row as a separate component, with selected state managed by GlobeCard so only one expands at a time. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,25 +18,35 @@ function countryCodeToFlag(code: string): string {
|
|||||||
return [...code.toUpperCase()].map(c => String.fromCodePoint(0x1f1e6 + c.charCodeAt(0) - 65)).join('');
|
return [...code.toUpperCase()].map(c => String.fromCodePoint(0x1f1e6 + c.charCodeAt(0) - 65)).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function CountryChip({ country }: { country: CountryData }) {
|
function CountryChip({ country, isSelected, onSelect }: { country: CountryData; isSelected: boolean; onSelect: (code: string | null) => void }) {
|
||||||
const [expanded, setExpanded] = useState(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex-shrink-0">
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setExpanded(!expanded)}
|
onClick={() => onSelect(isSelected ? null : country.code)}
|
||||||
className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-neutral-800/80 border border-neutral-700/50 hover:border-neutral-600 transition-colors text-sm cursor-pointer"
|
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-lg border transition-colors text-sm cursor-pointer flex-shrink-0 ${
|
||||||
|
isSelected
|
||||||
|
? 'bg-neutral-700/80 border-neutral-500'
|
||||||
|
: 'bg-neutral-800/80 border-neutral-700/50 hover:border-neutral-600'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<span className="text-base leading-none">{countryCodeToFlag(country.code)}</span>
|
<span className="text-base leading-none">{countryCodeToFlag(country.code)}</span>
|
||||||
<span className="text-neutral-400 font-mono text-xs">{country.code}</span>
|
<span className="text-neutral-400 font-mono text-xs">{country.code}</span>
|
||||||
<span className="text-neutral-200 font-semibold text-xs">{country.count}</span>
|
<span className="text-neutral-200 font-semibold text-xs">{country.count}</span>
|
||||||
</button>
|
</button>
|
||||||
{expanded && (
|
);
|
||||||
<div className="absolute bottom-full left-0 mb-1 w-56 bg-neutral-800 border border-neutral-700 rounded-lg shadow-xl z-30 p-2 max-h-48 overflow-y-auto">
|
}
|
||||||
|
|
||||||
|
function CountryDetail({ country }: { country: CountryData }) {
|
||||||
|
return (
|
||||||
|
<div className="mb-2 bg-neutral-800/90 border border-neutral-700 rounded-lg p-2.5 max-h-36 overflow-y-auto">
|
||||||
|
<div className="flex items-center gap-2 mb-1.5 text-xs text-neutral-300 font-medium">
|
||||||
|
<span className="text-sm">{countryCodeToFlag(country.code)}</span>
|
||||||
|
<span>{country.code}</span>
|
||||||
|
<span className="text-neutral-500">{country.count} hits</span>
|
||||||
|
</div>
|
||||||
{country.regions.map((r) => (
|
{country.regions.map((r) => (
|
||||||
<div key={r.region} className="mb-1.5 last:mb-0">
|
<div key={r.region} className="mb-1 last:mb-0">
|
||||||
<div className="flex justify-between text-xs px-1">
|
<div className="flex justify-between text-xs px-1">
|
||||||
<span className="text-neutral-300 font-medium">{r.region}</span>
|
<span className="text-neutral-300">{r.region}</span>
|
||||||
<span className="text-neutral-500">{r.count}</span>
|
<span className="text-neutral-500">{r.count}</span>
|
||||||
</div>
|
</div>
|
||||||
{r.cities.map((c) => (
|
{r.cities.map((c) => (
|
||||||
@@ -48,8 +58,6 @@ function CountryChip({ country }: { country: CountryData }) {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +80,7 @@ export function GlobeCard() {
|
|||||||
countries: [],
|
countries: [],
|
||||||
});
|
});
|
||||||
const [grabbing, setGrabbing] = useState(false);
|
const [grabbing, setGrabbing] = useState(false);
|
||||||
|
const [selectedCountry, setSelectedCountry] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchVisitors() {
|
async function fetchVisitors() {
|
||||||
@@ -190,9 +199,18 @@ export function GlobeCard() {
|
|||||||
|
|
||||||
{visitorStats.countries.length > 0 && (
|
{visitorStats.countries.length > 0 && (
|
||||||
<div className="absolute bottom-0 left-0 right-0 z-10 px-4 pb-3 pt-6 bg-gradient-to-t from-neutral-900 via-neutral-900/90 to-transparent">
|
<div className="absolute bottom-0 left-0 right-0 z-10 px-4 pb-3 pt-6 bg-gradient-to-t from-neutral-900 via-neutral-900/90 to-transparent">
|
||||||
|
{selectedCountry && (() => {
|
||||||
|
const country = visitorStats.countries.find(c => c.code === selectedCountry);
|
||||||
|
return country ? <CountryDetail country={country} /> : null;
|
||||||
|
})()}
|
||||||
<div className="flex gap-2 overflow-x-auto pb-1 scrollbar-none">
|
<div className="flex gap-2 overflow-x-auto pb-1 scrollbar-none">
|
||||||
{visitorStats.countries.map((country) => (
|
{visitorStats.countries.map((country) => (
|
||||||
<CountryChip key={country.code} country={country} />
|
<CountryChip
|
||||||
|
key={country.code}
|
||||||
|
country={country}
|
||||||
|
isSelected={selectedCountry === country.code}
|
||||||
|
onSelect={setSelectedCountry}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user