diff --git a/webapp/src/app/app-seed.tsx b/webapp/src/app/app-seed.tsx new file mode 100644 index 00000000..dc0f67b7 --- /dev/null +++ b/webapp/src/app/app-seed.tsx @@ -0,0 +1,15 @@ +import { AppWrapper } from "./app-wrapper"; +import { AppRouterSeed } from "./routes/app-router-seed"; + +import "./styles/globals.css"; +import "./styles/seed.css"; + +const AppSeed = () => { + return ( + + + + ); +}; + +export default AppSeed; diff --git a/webapp/src/app/global-router.tsx b/webapp/src/app/global-router.tsx index e7140c5b..d23d87b9 100644 --- a/webapp/src/app/global-router.tsx +++ b/webapp/src/app/global-router.tsx @@ -2,7 +2,7 @@ import { lazy } from "react"; import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; const AppAll4Trees = lazy(() => import("./app-all4trees")); -// const AppSeed = lazy(() => import("./app-seed")); #uncomment next PR +const AppSeed = lazy(() => import("./app-seed")); export default function GlobalRouter() { return ( @@ -12,10 +12,10 @@ export default function GlobalRouter() { element={} path="/all4trees/*" /> - {/* } path="/seed/*" - /> */} + /> } index diff --git a/webapp/src/app/providers/map-provider-seed.tsx b/webapp/src/app/providers/map-provider-seed.tsx new file mode 100644 index 00000000..dfb70566 --- /dev/null +++ b/webapp/src/app/providers/map-provider-seed.tsx @@ -0,0 +1,77 @@ +import { createMap, EVENTS } from "coordo"; +import { type ReactNode, useCallback, useRef, useState } from "react"; + +import { API_URL } from "@shared/api/client"; +import { MapContext } from "@shared/contexts/map-context-seed"; +import { useLocalStorage } from "@shared/hooks/use-local-storage"; + +const STYLE_URL = `${API_URL}/maps/style.json`; + +type MapSettings = { + zoom: number; + center: [number, number]; +}; + +const DEFAULT_MAP_SETTINGS: MapSettings = { + center: [34.1246, -23.0758], + zoom: 3.8, +}; + +type MapProviderSeedProps = { + children: ReactNode; +}; + +export function MapProviderSeed({ children }: MapProviderSeedProps) { + const [isReady, setIsReady] = useState(false); + const mapApiRef = useRef | null>(null); + const [mapSettings, setMapSettings] = useLocalStorage( + "d4g:map-settings:seed", + DEFAULT_MAP_SETTINGS, + ); + + // Callback ref pattern — called by React when the DOM node mounts/unmounts. + // See: https://dev.to/gilfink/quick-tip-using-callback-refs-in-react-4gef + // biome-ignore lint/correctness/useExhaustiveDependencies: map init should run only once on mount + const mapContainerRef = useCallback((node: HTMLElement | null) => { + if (!node || mapApiRef.current) return; + + const handleReady = () => { + setIsReady(true); + }; + + node.addEventListener(EVENTS.MAP_READY, handleReady); + + try { + mapApiRef.current = createMap(`#${node.id}`, STYLE_URL, { + center: mapSettings.center, + zoom: mapSettings.zoom, + }); + if (import.meta.env.DEV) { + // biome-ignore lint/suspicious/noExplicitAny : debug only + (window as any).__map__ = mapApiRef.current?.mapInstance; + } + // biome-ignore lint/suspicious/noExplicitAny : + mapApiRef.current.addEventListener("move", (event: any) => { + setMapSettings({ + center: event.target.getCenter().toArray(), + zoom: event.target.getZoom(), + }); + }); + } catch (error) { + console.error("Error when initializing the map:", error); + } + }, []); + + return ( + + {children} + + ); +} diff --git a/webapp/src/app/routes/app-router-seed.tsx b/webapp/src/app/routes/app-router-seed.tsx new file mode 100644 index 00000000..2668f989 --- /dev/null +++ b/webapp/src/app/routes/app-router-seed.tsx @@ -0,0 +1,20 @@ +import { lazy } from "react"; + +import { MapProviderSeed } from "@app/providers/map-provider-seed"; + +import { AppRouterBase } from "./app-router-base"; + +const MainPage = lazy(() => import("@pages/seed/main")); + +export const AppRouterSeed = () => { + return ( + + ); +}; diff --git a/webapp/src/app/styles/seed.css b/webapp/src/app/styles/seed.css new file mode 100644 index 00000000..9a485b10 --- /dev/null +++ b/webapp/src/app/styles/seed.css @@ -0,0 +1,161 @@ +/* ========================================================================== + Charte Graphique Seed - Variables CSS & Classes Utilitaires + -------------------------------------------------------------------------- + -> Utiliser le préfix seed pour les variables de la charte graphique. + -> Utiliser le préfix custom pour les variables du Figma. + -> Pour utiliser les niveaux d'alpha, il faut utiliser la notation rgb + --seed-rgb-my-color: 123, 123, 123; + --seed-color-my-color: rgba(var(--seed-rgb-my-color), 1); + --other-usage: rgba(var(--seed-rgb-my-color), 0.3); + ========================================================================== */ + +@import "tailwindcss"; +@import "./index.css"; + +:root { + /* ======================================================================== + COULEURS PRINCIPALES - CHARTE SEED + + /!\ WRONG VALUES AND TOKENS => THESE ARE FOR TESTING NOW + ======================================================================== */ + + --seed-color-nuit: #0f0f0f; + --seed-color-onyx: #424242; + --seed-color-brunswick-green: #224d41; + --seed-rgb-vert-kelly: 153, 207, 22; + --seed-color-vert-kelly: rgba(var(--seed-rgb-vert-kelly), 1); + --seed-color-jaune-vert: #99cf16; + --seed-color-citrouille: #e40000; + --seed-color-alabaster: #e1cd16; + + /* ======================================================================== + COULEURS LA FORET - CHARTE SEED + ======================================================================== */ + --seed-color-bleu: #123762; + --seed-color-vert-de-gris: #7dabae; + + /* ======================================================================== + COULEURS AJUSTEES - POUR UNE MEILLEURE UI + ======================================================================== */ + --custom-color-green-dark: #358055; + --custom-color-green-main: #0d1d18; + --custom-color-green-pastel: #31d181; + --custom-color-grey-light: #e5e7eb; + --custom-color-blue-dark: #207ee2; + --custom-color-blue-light: #52a4ff; + + --custom-color-socio-eco: #1447e6; /* Same as in socio-eco-icon */ + --custom-color-inventaire: #5e1a1a; /* Same as in inventaire-icon */ + + /* ======================================================================== + TYPOGRAPHIE - CHARTE SEED + ======================================================================== */ + + --font-title-primary: + "Phosphate", "Phosphate Solid", Impact, "Arial Black", system-ui, sans-serif; + --font-body-primary: Arial, system-ui, -apple-system, "Segoe UI", sans-serif; + + --font-title-secondary: + "Century Gothic", "Century Gothic Black", "Arial Black", system-ui, + sans-serif; + --font-body-secondary: + "Open Sans", Arial, system-ui, -apple-system, "Segoe UI", sans-serif; +} + +/* ======================================================================== + THEME TOKEN CONFIGURATIONS ALL 4 TREES: light (root) and dark + Docs: https://ui.shadcn.com/docs/theming + ======================================================================== */ +:root { + /* LIGHT MODE SHADCN */ + --background: var(--seed-color-alabaster); + --foreground: var(--seed-color-nuit); + + --card: var(--custom-color-grey-light); + --card-foreground: var(--seed-color-nuit); + + --popover: var(--seed-color-alabaster); + --popover-foreground: var(--seed-color-nuit); + + --primary: var(--seed-color-vert-kelly); + --primary-foreground: var(--seed-color-nuit); + --secondary: var(--seed-color-jaune-vert); + --secondary-foreground: var(--seed-color-nuit); + + --muted: #e5e7eb; + --muted-foreground: var(--seed-color-onyx); + --accent: var(--seed-color-jaune-vert); + --accent-foreground: var(--seed-color-nuit); + --destructive: #dc2626; + --info: var(--custom-color-blue-light); + --info-foreground: var(--custom-color-blue-dark); + + --border: rgba(0, 0, 0, 0.15); + --input: #ffffff; + --ring: var(--seed-color-vert-kelly); + + --chart-1: var(--seed-color-vert-kelly); + --chart-2: var(--seed-color-citrouille); + --chart-3: var(--seed-color-bleu); + --chart-4: #895bf5; + --chart-5: #f04646; + --chart-6: var(--seed-color-onyx); + + --sidebar: var(--seed-color-alabaster); + --sidebar-foreground: var(--seed-color-alabaster); + --sidebar-primary: var(--seed-color-brunswick-green); + --sidebar-primary-foreground: var(--seed-color-alabaster); + --sidebar-accent: var(--seed-color-citrouille); + --sidebar-accent-foreground: #ffffff; + --sidebar-border: #e5e7eb; + --sidebar-ring: var(--seed-color-vert-kelly); +} + +.dark { + --background: var(--custom-color-green-dark); + --foreground: var(--seed-color-alabaster); + + --card: var(--custom-color-blue-dark); + --card-foreground: var(--seed-color-alabaster); + + --popover: var(--seed-color-nuit); + --popover-foreground: var(--seed-color-alabaster); + + --primary: var(--seed-color-brunswick-green); + --primary-foreground: var(--custom-color-green-pastel); + --secondary: var(--seed-color-vert-de-gris); + --secondary-foreground: var(--seed-color-nuit); + + --muted: var(--seed-color-onyx); + --muted-foreground: #9ca3af; + --accent: var(--custom-color-green-main); + --accent-foreground: var(--custom-color-green-pastel); + --destructive: #ef4444; + --info: var(--custom-color-blue-dark); + --info-foreground: var(--custom-color-blue-light); + + --border: rgba(255, 255, 255, 0.15); + --input: rgba(255, 255, 255, 0.05); + --ring: var(--seed-color-jaune-vert); + + --sidebar: var(--seed-color-onyx); + --sidebar-foreground: var(--seed-color-alabaster); + --sidebar-primary: var(--seed-color-onyx); + --sidebar-primary-foreground: var(--seed-color-vert-kelly); + --sidebar-accent: var(--seed-color-citrouille); + --sidebar-accent-foreground: var(--seed-color-nuit); + --sidebar-border: rgba(255, 255, 255, 0.15); + --sidebar-ring: var(--seed-color-jaune-vert); +} + +@theme inline { + --color-socio-eco: var(--custom-color-socio-eco); + --color-inventaire: var(--custom-color-inventaire); +} + +@layer components { + * { + outline-color: rgba(#a21111, 0.2); + border-color: var(--border); + } +} diff --git a/webapp/src/pages/seed/main.tsx b/webapp/src/pages/seed/main.tsx new file mode 100644 index 00000000..9bedc613 --- /dev/null +++ b/webapp/src/pages/seed/main.tsx @@ -0,0 +1,43 @@ +import { MapSeed } from "@widgets/map/map-seed"; + +import { SidebarProvider } from "@shared/ui/sidebar"; + +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@ui/resizable"; + +export interface MainPageProps { + userData?: unknown; +} + +function MainPage() { + return ( + <> + {/* TODO: Integrate Sidebar with Resizable Panels smoothly: https://github.com/huntabyte/shadcn-svelte/discussions/1657 */} + + + +

This is seed !

+
+ + + + + + +
+
+ + ); +} + +// Default export for lazy loading import +export default MainPage; diff --git a/webapp/src/shared/contexts/map-context-seed.ts b/webapp/src/shared/contexts/map-context-seed.ts new file mode 100644 index 00000000..d8ebad7f --- /dev/null +++ b/webapp/src/shared/contexts/map-context-seed.ts @@ -0,0 +1,26 @@ +import type { createMap } from "coordo"; +import { + createContext, + type RefCallback, + type RefObject, + useContext, +} from "react"; + +export type Category = { value: string; label: string }; + +export interface MapContextType { + isReady: boolean; + mapApiRef: RefObject | null>; + mapContainerRef: RefCallback; + setIsReady: (isReady: boolean) => void; +} + +export const MapContext = createContext(undefined); + +export function useMapContext() { + const context = useContext(MapContext); + if (!context) { + throw new Error("useMapContext must be used within a MapProvider"); + } + return context; +} diff --git a/webapp/src/widgets/map/assets/seed-icon.svg b/webapp/src/widgets/map/assets/seed-icon.svg new file mode 100644 index 00000000..ab9683f3 --- /dev/null +++ b/webapp/src/widgets/map/assets/seed-icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/webapp/src/widgets/map/map-seed.tsx b/webapp/src/widgets/map/map-seed.tsx new file mode 100644 index 00000000..ee6aa3aa --- /dev/null +++ b/webapp/src/widgets/map/map-seed.tsx @@ -0,0 +1,50 @@ +import { type FC, useEffect, useState } from "react"; + +import { type SeedData, SeedIndicator } from "@features/indicators/seed"; +import { + DEFAULT_POPUP_CONFIG, + getRenderPopupLayer, +} from "@features/popup/renderPopup"; + +import { LAYERS } from "@shared/api/layers"; +import { useMapContext } from "@shared/contexts/map-context-seed"; + +import pictoSeed from "./assets/seed-icon.svg"; +import { MapBase } from "./map-base"; +import { getIconSize } from "./utils"; + +export const MapSeed: FC = () => { + const { isReady, mapContainerRef, mapApiRef } = useMapContext(); + const [isMaximizedPopupSize, setIsMaximizedPopupSize] = useState(false); + + useEffect(() => { + if (!isReady || !mapApiRef.current) return; + + const toggleShiftSize = () => setIsMaximizedPopupSize((prev) => !prev); + + mapApiRef.current.setLayerSymbol({ + iconSize: getIconSize({}), + layerId: LAYERS.SEED, + svg: pictoSeed, + }); + + mapApiRef.current.setLayerPopup({ + centerOnClick: true, + layerId: LAYERS.SEED, + popupConfig: DEFAULT_POPUP_CONFIG, + renderCallback: getRenderPopupLayer({ + Element: SeedIndicator, + toggleShiftSize, + }), + trigger: "click", + }); + }, [isReady, mapApiRef]); + + return ( + + ); +};