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 (
+
+ );
+};