mirror of
https://github.com/AnmolSaini16/mapcn
synced 2026-04-25 16:14:54 +02:00
refactor(ui): shared Logo, centralize theme hotkey, layout and CSS tweaks
This commit is contained in:
@@ -40,7 +40,7 @@ This project uses [CARTO Basemaps](https://docs.carto.com/faqs/carto-basemaps) w
|
||||
|
||||
- **Commercial use**: Requires a CARTO Enterprise license. [Request a demo](https://carto.com/request-live-demo) for pricing details.
|
||||
- **Non-commercial use**: Free for CARTO grantees under their [basemap terms](https://carto.com/legal/bmap).
|
||||
- **Alternative**: You can switch to [OpenStreetMap](https://www.openstreetmap.org/) tiles or any other MapLibre-compatible tile provider.
|
||||
- **Alternative**: You can switch to [OpenStreetMap](https://www.openstreetmap.org/) tiles or any other MapLibre-compatible tile provider (MapTiler, Stadia Maps, etc).
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -7,7 +7,7 @@ interface IframePreviewProps {
|
||||
|
||||
export function IframePreview({ src, title }: IframePreviewProps) {
|
||||
return (
|
||||
<div className="relative w-full overflow-hidden rounded-xl border h-(--block-preview-height)">
|
||||
<div className="relative h-(--block-preview-height) w-full overflow-hidden rounded-xl border">
|
||||
<iframe src={src} title={title} className="size-full border-0" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Link from "next/link";
|
||||
import { MapPin } from "lucide-react";
|
||||
import { Logo } from "./logo";
|
||||
|
||||
const footerLinks = {
|
||||
product: [
|
||||
@@ -36,12 +36,7 @@ export function Footer() {
|
||||
<div className="container py-12 md:py-16">
|
||||
<div className="grid grid-cols-2 gap-8 md:grid-cols-4">
|
||||
<div className="col-span-2 md:col-span-1">
|
||||
<Link href="/" className="flex w-fit items-center gap-1.5">
|
||||
<MapPin className="size-4 shrink-0" />
|
||||
<span className="text-lg font-semibold tracking-tight">
|
||||
mapcn
|
||||
</span>
|
||||
</Link>
|
||||
<Logo className="w-fit" />
|
||||
<p className="text-muted-foreground mt-3 max-w-xs text-sm leading-relaxed">
|
||||
Free & open-source, ready-to-use, customizable map components for
|
||||
React.
|
||||
|
||||
@@ -20,14 +20,14 @@ export function Header({ className }: { className?: string }) {
|
||||
<header
|
||||
className={cn("bg-background sticky top-0 z-50 h-14 w-full", className)}
|
||||
>
|
||||
<nav className="container flex size-full items-center gap-2">
|
||||
<nav className="container flex size-full items-center">
|
||||
<MobileNav />
|
||||
<Logo className="hidden shrink-0 lg:flex" />
|
||||
<Logo className="mr-3 hidden shrink-0 lg:flex" />
|
||||
<MainNav className="hidden lg:flex" />
|
||||
|
||||
<div className="ml-auto flex h-4.5 items-center gap-2">
|
||||
<CommandSearch />
|
||||
<Separator orientation="vertical" className="hidden md:block" />
|
||||
<Separator orientation="vertical" className="ml-2 hidden md:block" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import { MapPin } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
interface LogoProps {
|
||||
className?: string;
|
||||
@@ -10,16 +9,16 @@ interface LogoProps {
|
||||
|
||||
export function Logo({ className, onClick }: LogoProps) {
|
||||
return (
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className={cn("px-2.5 text-base font-semibold", className)}
|
||||
<Link
|
||||
href="/"
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"flex h-8 items-center gap-1.5 text-lg font-semibold",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Link href="/" onClick={onClick}>
|
||||
<MapPin />
|
||||
mapcn
|
||||
</Link>
|
||||
</Button>
|
||||
<MapPin className="size-4" />
|
||||
mapcn
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,15 +12,9 @@ export function MainNav({ className, ...props }: React.ComponentProps<"nav">) {
|
||||
if (!navItems?.length) return null;
|
||||
|
||||
return (
|
||||
<nav className={cn("flex items-center gap-1", className)} {...props}>
|
||||
<nav className={cn("flex items-center", className)} {...props}>
|
||||
{navItems.map((item) => (
|
||||
<Button
|
||||
key={item.href}
|
||||
variant="ghost"
|
||||
asChild
|
||||
size="sm"
|
||||
className="px-2.5"
|
||||
>
|
||||
<Button key={item.href} variant="ghost" asChild size="sm">
|
||||
<Link
|
||||
href={item.href}
|
||||
className="relative inline-flex items-center gap-1.5"
|
||||
|
||||
@@ -46,7 +46,7 @@ function PageHeader({
|
||||
|
||||
<section
|
||||
className={cn(
|
||||
"container mx-auto flex w-full max-w-6xl flex-col gap-4 py-16 md:py-24 lg:pt-26 lg:pb-24",
|
||||
"container mx-auto flex w-full max-w-6xl flex-col gap-4 py-16 md:py-20 lg:py-24",
|
||||
align === "center"
|
||||
? "items-center text-center"
|
||||
: "items-start text-left",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import type { ComponentProps } from "react";
|
||||
import * as React from "react";
|
||||
import { ThemeProvider as NextThemesProvider, useTheme } from "next-themes";
|
||||
|
||||
export function ThemeProvider({
|
||||
function ThemeProvider({
|
||||
children,
|
||||
...props
|
||||
}: ComponentProps<typeof NextThemesProvider>) {
|
||||
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||
return (
|
||||
<NextThemesProvider
|
||||
attribute="class"
|
||||
@@ -15,7 +15,57 @@ export function ThemeProvider({
|
||||
disableTransitionOnChange
|
||||
{...props}
|
||||
>
|
||||
<ThemeHotkey />
|
||||
{children}
|
||||
</NextThemesProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function isTypingTarget(target: EventTarget | null) {
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
target.isContentEditable ||
|
||||
target.tagName === "INPUT" ||
|
||||
target.tagName === "TEXTAREA" ||
|
||||
target.tagName === "SELECT"
|
||||
);
|
||||
}
|
||||
|
||||
function ThemeHotkey() {
|
||||
const { resolvedTheme, setTheme } = useTheme();
|
||||
|
||||
React.useEffect(() => {
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (event.defaultPrevented || event.repeat) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.metaKey || event.ctrlKey || event.altKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key.toLowerCase() !== "t") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTypingTarget(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTheme(resolvedTheme === "dark" ? "light" : "dark");
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, [resolvedTheme, setTheme]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export { ThemeProvider };
|
||||
|
||||
@@ -25,27 +25,6 @@ export function ThemeToggle() {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
// Don't trigger if typing in an input/textarea
|
||||
if (
|
||||
e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
(e.target as HTMLElement)?.isContentEditable
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((e.key === "t" || e.key === "T") && !e.metaKey && !e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
toggleTheme();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [toggleTheme]);
|
||||
|
||||
if (!mounted) {
|
||||
return <Skeleton className="size-8" />;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ function getSystemTheme(): Theme {
|
||||
|
||||
function useResolvedTheme(themeProp?: "light" | "dark"): Theme {
|
||||
const [detectedTheme, setDetectedTheme] = useState<Theme>(
|
||||
() => getDocumentTheme() ?? getSystemTheme()
|
||||
() => getDocumentTheme() ?? getSystemTheme(),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -146,11 +146,11 @@ type MapProps = {
|
||||
|
||||
function DefaultLoader() {
|
||||
return (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center bg-background/50 backdrop-blur-xs">
|
||||
<div className="bg-background/50 absolute inset-0 z-10 flex items-center justify-center backdrop-blur-xs">
|
||||
<div className="flex gap-1">
|
||||
<span className="size-1.5 rounded-full bg-muted-foreground/60 animate-pulse" />
|
||||
<span className="size-1.5 rounded-full bg-muted-foreground/60 animate-pulse [animation-delay:150ms]" />
|
||||
<span className="size-1.5 rounded-full bg-muted-foreground/60 animate-pulse [animation-delay:300ms]" />
|
||||
<span className="bg-muted-foreground/60 size-1.5 animate-pulse rounded-full" />
|
||||
<span className="bg-muted-foreground/60 size-1.5 animate-pulse rounded-full [animation-delay:150ms]" />
|
||||
<span className="bg-muted-foreground/60 size-1.5 animate-pulse rounded-full [animation-delay:300ms]" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -178,7 +178,7 @@ const Map = forwardRef<MapRef, MapProps>(function Map(
|
||||
loading = false,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
ref,
|
||||
) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [mapInstance, setMapInstance] = useState<MapLibreGL.Map | null>(null);
|
||||
@@ -199,7 +199,7 @@ const Map = forwardRef<MapRef, MapProps>(function Map(
|
||||
dark: styles?.dark ?? defaultStyles.dark,
|
||||
light: styles?.light ?? defaultStyles.light,
|
||||
}),
|
||||
[styles]
|
||||
[styles],
|
||||
);
|
||||
|
||||
// Expose the map instance to the parent component
|
||||
@@ -318,14 +318,14 @@ const Map = forwardRef<MapRef, MapProps>(function Map(
|
||||
map: mapInstance,
|
||||
isLoaded: isLoaded && isStyleLoaded,
|
||||
}),
|
||||
[mapInstance, isLoaded, isStyleLoaded]
|
||||
[mapInstance, isLoaded, isStyleLoaded],
|
||||
);
|
||||
|
||||
return (
|
||||
<MapContext.Provider value={contextValue}>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn("relative w-full h-full", className)}
|
||||
className={cn("relative h-full w-full", className)}
|
||||
>
|
||||
{(!isLoaded || loading) && <DefaultLoader />}
|
||||
{/* SSR-safe: children render only when map is loaded on client */}
|
||||
@@ -508,7 +508,7 @@ function MarkerContent({ children, className }: MarkerContentProps) {
|
||||
<div className={cn("relative cursor-pointer", className)}>
|
||||
{children || <DefaultMarkerIcon />}
|
||||
</div>,
|
||||
marker.getElement()
|
||||
marker.getElement(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -580,15 +580,15 @@ function MarkerPopup({
|
||||
return createPortal(
|
||||
<div
|
||||
className={cn(
|
||||
"relative rounded-md border bg-popover p-3 text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95",
|
||||
className
|
||||
"bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 relative rounded-md border p-3 shadow-md",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{closeButton && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
className="absolute top-1 right-1 z-10 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
||||
className="ring-offset-background focus:ring-ring absolute top-1 right-1 z-10 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none"
|
||||
aria-label="Close popup"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
@@ -597,7 +597,7 @@ function MarkerPopup({
|
||||
)}
|
||||
{children}
|
||||
</div>,
|
||||
container
|
||||
container,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -666,13 +666,13 @@ function MarkerTooltip({
|
||||
return createPortal(
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-md bg-foreground px-2 py-1 text-xs text-background shadow-md animate-in fade-in-0 zoom-in-95",
|
||||
className
|
||||
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 rounded-md px-2 py-1 text-xs shadow-md",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>,
|
||||
container
|
||||
container,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -699,9 +699,9 @@ function MarkerLabel({
|
||||
<div
|
||||
className={cn(
|
||||
"absolute left-1/2 -translate-x-1/2 whitespace-nowrap",
|
||||
"text-[10px] font-medium text-foreground",
|
||||
"text-foreground text-[10px] font-medium",
|
||||
positionClasses[position],
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@@ -735,7 +735,7 @@ const positionClasses = {
|
||||
|
||||
function ControlGroup({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex flex-col rounded-md border border-border bg-background shadow-sm overflow-hidden [&>button:not(:last-child)]:border-b [&>button:not(:last-child)]:border-border">
|
||||
<div className="border-border bg-background [&>button:not(:last-child)]:border-border flex flex-col overflow-hidden rounded-md border shadow-sm [&>button:not(:last-child)]:border-b">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@@ -758,8 +758,8 @@ function ControlButton({
|
||||
aria-label={label}
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex items-center justify-center size-8 hover:bg-accent dark:hover:bg-accent/40 transition-colors",
|
||||
disabled && "opacity-50 pointer-events-none cursor-not-allowed"
|
||||
"hover:bg-accent dark:hover:bg-accent/40 flex size-8 items-center justify-center transition-colors",
|
||||
disabled && "pointer-events-none cursor-not-allowed opacity-50",
|
||||
)}
|
||||
disabled={disabled}
|
||||
>
|
||||
@@ -812,7 +812,7 @@ function MapControls({
|
||||
(error) => {
|
||||
console.error("Error getting location:", error);
|
||||
setWaitingForLocation(false);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}, [map, onLocate]);
|
||||
@@ -832,7 +832,7 @@ function MapControls({
|
||||
className={cn(
|
||||
"absolute z-10 flex flex-col gap-1.5",
|
||||
positionClasses[position],
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{showZoom && (
|
||||
@@ -1006,15 +1006,15 @@ function MapPopup({
|
||||
return createPortal(
|
||||
<div
|
||||
className={cn(
|
||||
"relative rounded-md border bg-popover p-3 text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95",
|
||||
className
|
||||
"bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 relative rounded-md border p-3 shadow-md",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{closeButton && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
className="absolute top-1 right-1 z-10 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
||||
className="ring-offset-background focus:ring-ring absolute top-1 right-1 z-10 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none"
|
||||
aria-label="Close popup"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
@@ -1023,7 +1023,7 @@ function MapPopup({
|
||||
)}
|
||||
{children}
|
||||
</div>,
|
||||
container
|
||||
container,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1169,7 +1169,7 @@ function MapRoute({
|
||||
}
|
||||
|
||||
type MapClusterLayerProps<
|
||||
P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties
|
||||
P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties,
|
||||
> = {
|
||||
/** GeoJSON FeatureCollection data or URL to fetch GeoJSON from */
|
||||
data: string | GeoJSON.FeatureCollection<GeoJSON.Point, P>;
|
||||
@@ -1186,18 +1186,18 @@ type MapClusterLayerProps<
|
||||
/** Callback when an unclustered point is clicked */
|
||||
onPointClick?: (
|
||||
feature: GeoJSON.Feature<GeoJSON.Point, P>,
|
||||
coordinates: [number, number]
|
||||
coordinates: [number, number],
|
||||
) => void;
|
||||
/** Callback when a cluster is clicked. If not provided, zooms into the cluster */
|
||||
onClusterClick?: (
|
||||
clusterId: number,
|
||||
coordinates: [number, number],
|
||||
pointCount: number
|
||||
pointCount: number,
|
||||
) => void;
|
||||
};
|
||||
|
||||
function MapClusterLayer<
|
||||
P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties
|
||||
P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties,
|
||||
>({
|
||||
data,
|
||||
clusterMaxZoom = 14,
|
||||
@@ -1375,7 +1375,7 @@ function MapClusterLayer<
|
||||
const handleClusterClick = async (
|
||||
e: MapLibreGL.MapMouseEvent & {
|
||||
features?: MapLibreGL.MapGeoJSONFeature[];
|
||||
}
|
||||
},
|
||||
) => {
|
||||
const features = map.queryRenderedFeatures(e.point, {
|
||||
layers: [clusterLayerId],
|
||||
@@ -1387,7 +1387,7 @@ function MapClusterLayer<
|
||||
const pointCount = feature.properties?.point_count as number;
|
||||
const coordinates = (feature.geometry as GeoJSON.Point).coordinates as [
|
||||
number,
|
||||
number
|
||||
number,
|
||||
];
|
||||
|
||||
if (onClusterClick) {
|
||||
@@ -1407,7 +1407,7 @@ function MapClusterLayer<
|
||||
const handlePointClick = (
|
||||
e: MapLibreGL.MapMouseEvent & {
|
||||
features?: MapLibreGL.MapGeoJSONFeature[];
|
||||
}
|
||||
},
|
||||
) => {
|
||||
if (!onPointClick || !e.features?.length) return;
|
||||
|
||||
@@ -1423,7 +1423,7 @@ function MapClusterLayer<
|
||||
|
||||
onPointClick(
|
||||
feature as unknown as GeoJSON.Feature<GeoJSON.Point, P>,
|
||||
coordinates
|
||||
coordinates,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -123,6 +123,9 @@
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
html {
|
||||
@apply overscroll-y-none;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground font-sans;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user