- {/* Replace with your actual logo/branding */}
-
Welcome to Borbann
-
Your data integration and analysis platform.
+
+ {/* Hero Section */}
+
+
+
+ Property Analytics Platform
+
+
Data-Driven Property Intelligence
+
+ Leverage AI and machine learning to analyze property data, predict prices, and gain valuable insights for
+ better decision making.
+
+
+
+
+ Explore Map
+
+
+
+ Manage Data Pipelines
+
+
+
+
-
-
-
Go to Map
-
-
- {" "}
- {/* Example link */}
-
- Read Docs
+ {/* Features Section */}
+
+
+
+
Powerful Analytics Features
+
+ Our platform provides comprehensive tools to analyze property data from multiple angles
+
+
+
+
+
+
+
+
+
+ Geospatial Visualization
+
+ Interactive maps with property data overlays and environmental factors
+
+
+
+
+ Visualize properties on an interactive map with customizable overlays for flood risk, air quality, and
+ more. Analyze properties within a specific radius.
+
+
+
+
+
+ Explore Maps
+
+
+
+
+
+
+
+
+
+
+ Price Prediction
+ AI-powered price prediction with explainable features
+
+
+
+ Understand how different factors affect property prices with our explainable AI models. Adjust
+ parameters to see how they impact predictions.
+
+
+
+
+
+ View Predictions
+
+
+
+
+
+
+
+
+
+
+ Data Pipelines
+ Automated data collection and processing
+
+
+
+ Set up automated data collection from multiple sources. Our AI-powered pipelines clean, normalize, and
+ prepare data for analysis.
+
+
+
+
+
+ Manage Pipelines
+
+
+
+
+
+
+
+
+
+
+ Property Analytics
+ Comprehensive property data analysis
+
+
+
+ Analyze property details, historical price trends, and environmental factors. Generate reports and
+ export data for further analysis.
+
+
+
+
+
+ Browse Properties
+
+
+
+
+
+
+
+
+
+
+ Market Trends
+ Real-time market analysis and trends
+
+
+
+ Track property market trends over time. Identify emerging patterns and make data-driven investment
+ decisions.
+
+
+
+
+
+ View Trends
+
+
+
+
+
+
+
+
+
+
+ Custom ML Models
+ Train and deploy custom machine learning models
+
+
+
+ Create and train custom machine learning models using your own data. Deploy models for specific
+ analysis needs.
+
+
+
+
+
+ Manage Models
+
+
+
+
+
+
+
+
+ {/* CTA Section */}
+
+
+
Ready to Get Started?
+
+ Explore our platform and discover how data-driven property analytics can transform your decision making.
+
+
+
+ Start Exploring
+
-
-
-
- {/* Optional: Add more introductory content or links */}
-
- © {new Date().getFullYear()} Borbann Project.
-
+
+
);
}
diff --git a/frontend/components.json b/frontend/components.json
index fda3491..6479878 100644
--- a/frontend/components.json
+++ b/frontend/components.json
@@ -1,24 +1,26 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
- "style": "new-york",
+ "style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
- "css": "app/globals.css",
+ "css": "src/styles/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
+ "@": "@/src",
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks",
"features": "@/features",
- "types": "@/types",
- "services": "@/services"
+ "services": "@/services",
+ "store": "@/store",
+ "types": "@/types"
},
"iconLibrary": "lucide"
}
diff --git a/frontend/components/common/PageLayout.tsx b/frontend/components/common/PageLayout.tsx
deleted file mode 100644
index 7eeadf9..0000000
--- a/frontend/components/common/PageLayout.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
-========================================
-File: frontend/components/common/PageLayout.tsx (NEW - Example)
-========================================
-*/
-import React, { ReactNode } from "react";
-import { cn } from "@/lib/utils";
-// Example: Assuming you might have a common Header/Footer/Sidebar structure
-// import { AppHeader } from './AppHeader';
-// import { AppFooter } from './AppFooter';
-
-interface PageLayoutProps {
- children: ReactNode;
- className?: string;
-}
-
-/**
- * DUMMY: A basic layout component for pages.
- * Might include common headers, footers, or side navigation structures.
- */
-export function PageLayout({ children, className }: PageLayoutProps) {
- return (
-
- {/*
*/} {/* Example: Shared Header */}
-
{children}
- {/*
*/} {/* Example: Shared Footer */}
-
- );
-}
diff --git a/frontend/components/common/ThemeController.tsx b/frontend/components/common/ThemeController.tsx
deleted file mode 100644
index f3283b1..0000000
--- a/frontend/components/common/ThemeController.tsx
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
-========================================
-File: frontend/components/common/ThemeController.tsx
-========================================
-*/
-"use client";
-
-import { useState, useEffect, useRef, type ReactNode } from "react";
-import { useTheme } from "next-themes";
-import { Sun, Moon, Laptop, Palette, Check } from "lucide-react";
-import { Button } from "@/components/ui/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
- DropdownMenuSeparator,
- DropdownMenuLabel,
-} from "@/components/ui/dropdown-menu";
-import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
-
-// Define available color schemes (these affect CSS variables)
-const colorSchemes = [
- { name: "Blue", primary: "221.2 83.2% 53.3%" }, // Default blue
- { name: "Green", primary: "142.1 76.2% 36.3%" },
- { name: "Purple", primary: "262.1 83.3% 57.8%" },
- { name: "Orange", primary: "24.6 95% 53.1%" },
- { name: "Teal", primary: "173 80.4% 40%" },
-];
-
-interface ThemeControllerProps {
- children: ReactNode;
- defaultColorScheme?: string;
-}
-
-export function ThemeController({ children, defaultColorScheme = "Blue" }: ThemeControllerProps) {
- const { setTheme, theme } = useTheme();
- const [colorScheme, setColorScheme] = useState(defaultColorScheme);
- // State for overlay boundaries removed, as overlay context now manages positioning/constraints.
- // const [overlayBoundaries, setOverlayBoundaries] = useState({ width: 0, height: 0 });
- const containerRef = useRef
(null);
-
- // Update overlay boundaries - This logic might be better placed within the OverlayProvider
- // or removed if not strictly necessary for the controller's function.
- // Kept here for now as per original code structure, but consider moving it.
- // useEffect(() => {
- // const updateBoundaries = () => {
- // if (containerRef.current) {
- // const width = containerRef.current.clientWidth;
- // const height = containerRef.current.clientHeight;
- // document.documentElement.style.setProperty("--max-overlay-width", `${width - 32}px`);
- // document.documentElement.style.setProperty("--max-overlay-height", `${height - 32}px`);
- // }
- // };
- // updateBoundaries();
- // window.addEventListener("resize", updateBoundaries);
- // return () => window.removeEventListener("resize", updateBoundaries);
- // }, []);
-
- // Apply color scheme by setting the '--primary' CSS variable
- useEffect(() => {
- const scheme = colorSchemes.find((s) => s.name === colorScheme);
- if (scheme) {
- document.documentElement.style.setProperty("--primary", scheme.primary);
- // You might need to set --ring as well if it depends on primary
- // document.documentElement.style.setProperty("--ring", scheme.primary);
- }
- }, [colorScheme]);
-
- return (
-
- {children}
-
- {/* Theme Controller UI */}
-
- {" "}
- {/* Ensure high z-index */}
-
-
-
-
-
-
-
-
-
-
- Theme Options
-
-
- setTheme("light")} className="flex items-center justify-between">
-
-
- Light
-
- {theme === "light" && }
-
-
- setTheme("dark")} className="flex items-center justify-between">
-
-
- Dark
-
- {theme === "dark" && }
-
-
- setTheme("system")} className="flex items-center justify-between">
-
-
- System
-
- {theme === "system" && }
-
-
-
- Color Scheme
-
- {colorSchemes.map((scheme) => (
- setColorScheme(scheme.name)}
- className="flex items-center justify-between">
-
- {colorScheme === scheme.name && }
-
- ))}
-
-
-
-
- Theme Settings
-
-
-
-
-
- );
-}
diff --git a/frontend/components/common/ThemeProvider.tsx b/frontend/components/common/ThemeProvider.tsx
deleted file mode 100644
index b64248b..0000000
--- a/frontend/components/common/ThemeProvider.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
-========================================
-File: frontend/components/common/ThemeProvider.tsx
-========================================
-*/
-"use client";
-
-import { ThemeProvider as NextThemesProvider } from "next-themes";
-import type { ThemeProviderProps } from "next-themes";
-
-export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
- return (
-
- {children}
-
- );
-}
diff --git a/frontend/components/common/ThemeToggle.tsx b/frontend/components/common/ThemeToggle.tsx
deleted file mode 100644
index c1df06b..0000000
--- a/frontend/components/common/ThemeToggle.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
-========================================
-File: frontend/components/common/ThemeToggle.tsx
-========================================
-*/
-"use client";
-
-import React from "react";
-import { Moon, Sun, Laptop } from "lucide-react";
-import { useTheme } from "next-themes";
-import { Button } from "@/components/ui/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
-
-export function ThemeToggle() {
- const { setTheme, theme } = useTheme();
-
- return (
-
-
-
-
-
-
-
-
- Toggle theme
-
-
-
- setTheme("light")}>
-
- Light
-
- setTheme("dark")}>
-
- Dark
-
- setTheme("system")}>
-
- System
-
-
-
-
-
- Change theme
-
-
-
- );
-}
diff --git a/frontend/components/mode-toggle.tsx b/frontend/components/mode-toggle.tsx
new file mode 100644
index 0000000..28dc868
--- /dev/null
+++ b/frontend/components/mode-toggle.tsx
@@ -0,0 +1,28 @@
+"use client"
+import { Moon, Sun } from "lucide-react"
+import { useTheme } from "next-themes"
+
+import { Button } from "@/components/ui/button"
+import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
+
+export function ModeToggle() {
+ const { setTheme } = useTheme()
+
+ return (
+
+
+
+
+
+ Toggle theme
+
+
+
+ setTheme("light")}>Light
+ setTheme("dark")}>Dark
+ setTheme("system")}>System
+
+
+ )
+}
+
diff --git a/frontend/components/page-header.tsx b/frontend/components/page-header.tsx
new file mode 100644
index 0000000..3973597
--- /dev/null
+++ b/frontend/components/page-header.tsx
@@ -0,0 +1,51 @@
+"use client"
+
+import { ChevronRight } from "lucide-react"
+import Link from "next/link"
+import { ModeToggle } from "./mode-toggle"
+
+interface BreadcrumbItem {
+ title: string
+ href: string
+}
+
+interface PageHeaderProps {
+ title: string
+ description?: string
+ breadcrumb?: BreadcrumbItem[]
+}
+
+export default function PageHeader({ title, description, breadcrumb = [] }: PageHeaderProps) {
+ return (
+
+ {breadcrumb.length > 0 && (
+
+ {breadcrumb.map((item, index) => (
+
+ {index > 0 && }
+
+ {item.title}
+
+
+ ))}
+ {title && (
+ <>
+
+ {title}
+ >
+ )}
+
+
+
+
+
+ )}
+
+
+
{title}
+ {description &&
{description}
}
+
+
+ )
+}
+
diff --git a/frontend/components/sidebar.tsx b/frontend/components/sidebar.tsx
new file mode 100644
index 0000000..f22e45d
--- /dev/null
+++ b/frontend/components/sidebar.tsx
@@ -0,0 +1,114 @@
+"use client"
+
+import type React from "react"
+
+import { cn } from "@/lib/utils"
+import { Map, Database, FileText, Users, ChevronDown, ChevronUp, BrainCircuit } from "lucide-react"
+import Link from "next/link"
+import { usePathname } from "next/navigation"
+import { useState } from "react"
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
+import { Button } from "@/components/ui/button"
+
+interface SidebarItemProps {
+ icon: React.ReactNode
+ label: string
+ href: string
+ active?: boolean
+ badge?: React.ReactNode
+}
+
+const SidebarItem = ({ icon, label, href, active, badge }: SidebarItemProps) => {
+ return (
+
+ {icon}
+ {label}
+ {badge}
+
+ )
+}
+
+export default function Sidebar() {
+ const pathname = usePathname()
+ const [expanded, setExpanded] = useState(false)
+
+ return (
+
+
+
+
+ } label="Maps" href="/maps" active={pathname.startsWith("/maps")} />
+
+ }
+ label="Data Pipeline"
+ href="/data-pipeline"
+ active={pathname.startsWith("/data-pipeline")}
+ />
+
+ }
+ label="Models"
+ href="/models"
+ active={pathname.startsWith("/models")}
+ />
+
+ }
+ label="Documentation"
+ href="/documentation"
+ active={pathname.startsWith("/documentation")}
+ badge={NEW }
+ />
+
+
+
+
+
+
+ GG
+
+
+
GG_WPX
+
garfield.wpx@gmail.com
+
+
+
+
setExpanded(!expanded)}
+ >
+
+ Users
+ {expanded ? : }
+
+
+ {expanded && (
+
+
+ Manage Users
+
+
+ Roles & Permissions
+
+
+ )}
+
+
+ )
+}
+
diff --git a/frontend/components/theme-provider.tsx b/frontend/components/theme-provider.tsx
new file mode 100644
index 0000000..55c2f6e
--- /dev/null
+++ b/frontend/components/theme-provider.tsx
@@ -0,0 +1,11 @@
+'use client'
+
+import * as React from 'react'
+import {
+ ThemeProvider as NextThemesProvider,
+ type ThemeProviderProps,
+} from 'next-themes'
+
+export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
+ return {children}
+}
diff --git a/frontend/features/map/api/mapApi.ts b/frontend/features/map/api/mapApi.ts
deleted file mode 100644
index c4e1a72..0000000
--- a/frontend/features/map/api/mapApi.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
-========================================
-File: frontend/features/map/api/mapApi.ts
-========================================
-*/
-import apiClient from "@/services/apiClient";
-import type { APIResponse, PointOfInterest } from "@/types/api"; // Shared types
-import type { MapBounds } from "../types"; // Feature-specific types
-
-interface FetchPOIsParams {
- bounds: MapBounds;
- filters?: Record;
-}
-
-/**
- * DUMMY: Fetches Points of Interest based on map bounds and filters.
- */
-export async function fetchPointsOfInterest(
- params: FetchPOIsParams
-): Promise> {
- console.log("DUMMY API: Fetching POIs with params:", params);
-
- // Simulate building query parameters
- const queryParams = new URLSearchParams({
- north: params.bounds.north.toString(),
- south: params.bounds.south.toString(),
- east: params.bounds.east.toString(),
- west: params.bounds.west.toString(),
- });
- if (params.filters) {
- Object.entries(params.filters).forEach(([key, value]) => {
- if (value !== undefined && value !== null) {
- queryParams.set(key, String(value));
- }
- });
- }
-
- // Use the dummy apiClient
- const response = await apiClient.get(`/map/pois?${queryParams.toString()}`);
-
- if (response.success) {
- // Simulate adding more data if needed for testing
- const dummyData: PointOfInterest[] = [
- { id: "poi1", lat: params.bounds.north - 0.01, lng: params.bounds.west + 0.01, name: "Dummy Cafe", type: "cafe" },
- { id: "poi2", lat: params.bounds.south + 0.01, lng: params.bounds.east - 0.01, name: "Dummy Park", type: "park" },
- ...(response.data || []) // Include data if apiClient simulation provides it
- ];
- return { success: true, data: dummyData };
- } else {
- return response; // Forward the error response
- }
-}
-
-// Add other map-related API functions here
-export async function fetchMapAnalytics(bounds: MapBounds): Promise> {
- console.log("DUMMY API: Fetching Map Analytics with params:", bounds);
- // Simulate building query parameters
- const queryParams = new URLSearchParams({
- north: bounds.north.toString(),
- south: bounds.south.toString(),
- east: bounds.east.toString(),
- west: bounds.west.toString(),
- });
- return apiClient.get(`/map/analytics?${queryParams.toString()}`);
-}
\ No newline at end of file
diff --git a/frontend/features/map/components/analytics-overlay.tsx b/frontend/features/map/components/analytics-overlay.tsx
deleted file mode 100644
index ac88c10..0000000
--- a/frontend/features/map/components/analytics-overlay.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
-========================================
-File: frontend/features/map/components/analytics-overlay.tsx
-========================================
-*/
-"use client";
-
-import { LineChart, Wind, Droplets, Sparkles, Bot } from "lucide-react";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
-import { AreaChart } from "./area-chart";
-import { Overlay } from "./overlay-system/overlay"; // Import local overlay system
-import { useOverlay } from "./overlay-system/overlay-context";
-
-export function AnalyticsOverlay() {
- const { toggleOverlay } = useOverlay();
-
- const handleChatClick = () => {
- toggleOverlay("chat");
- };
-
- return (
- }
- initialPosition="top-right"
- initialIsOpen={true}
- width="350px">
-
-
-
-
Information in radius will be analyzed
-
- {/* Area Price History Card */}
-
-
-
-
- Area Price History
-
-
- Overall Price History of this area
-
-
-
-
-
-
- {/* Price Prediction Card */}
-
-
-
-
- Price Prediction
-
-
- The estimated price based on various factors.
-
-
-
-
-
-
- {/* Environmental Factors Cards */}
-
-
-
-
-
- Flood Factor
-
-
-
-
-
-
-
-
-
- Air Factor
-
-
-
-
-
-
- {/* Chat With AI Card */}
-
-
-
-
- Chat With AI
-
- Want to ask specific question?
-
-
-
-
-
-
- );
-}
diff --git a/frontend/features/map/components/analytics-panel.tsx b/frontend/features/map/components/analytics-panel.tsx
deleted file mode 100644
index eab45c5..0000000
--- a/frontend/features/map/components/analytics-panel.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-/*
-========================================
-File: frontend/features/map/components/analytics-panel.tsx
-========================================
-*/
-// This component seems redundant with analytics-overlay.tsx in the new structure.
-// If it has unique logic or display variation, keep it and refactor its imports.
-// Otherwise, it can likely be removed, and its functionality merged into analytics-overlay.tsx.
-// For this rewrite, assuming it's removed or its distinct logic is integrated elsewhere.
-// If needed, its structure would be similar to analytics-overlay.tsx but maybe without the wrapper.
diff --git a/frontend/features/map/components/area-chart.tsx b/frontend/features/map/components/area-chart.tsx
deleted file mode 100644
index 110a774..0000000
--- a/frontend/features/map/components/area-chart.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
-========================================
-File: frontend/features/map/components/area-chart.tsx
-========================================
-*/
-"use client";
-
-import { LineChart, Line, ResponsiveContainer, XAxis, YAxis, Tooltip } from "@/components/ui/chart"; // Using shared ui chart components
-import { useTheme } from "next-themes";
-
-interface AreaChartProps {
- data: number[];
- color: string;
-}
-
-export function AreaChart({ data, color }: AreaChartProps) {
- const { theme } = useTheme();
- const isDark = theme === "dark";
-
- // Generate labels (e.g., months or simple indices)
- const labels = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"]; // Example labels
-
- // Format the data for the chart
- const chartData = data.map((value, index) => ({
- name: labels[index % labels.length] || `Point ${index + 1}`, // Use labels or fallback
- value: value,
- }));
-
- // Format the price for display in tooltip
- const formatPrice = (value: number) => {
- return new Intl.NumberFormat("th-TH", {
- style: "currency",
- currency: "THB",
- minimumFractionDigits: 0,
- maximumFractionDigits: 0,
- }).format(value);
- };
-
- return (
-
- {" "}
- {/* Adjust height as needed */}
-
-
-
- dataMin * 0.95, (dataMax: number) => dataMax * 1.05]} />
- [formatPrice(value), "Price"]}
- contentStyle={{
- backgroundColor: isDark ? "#1f2937" : "white",
- borderRadius: "0.375rem",
- border: isDark ? "1px solid #374151" : "1px solid #e2e8f0",
- fontSize: "0.75rem",
- color: isDark ? "#e5e7eb" : "#1f2937",
- }}
- />
- , but keeping based on original code
- // If area fill is desired:
- // fill={color}
- // fillOpacity={0.5}
- />
-
-
-
- );
-}
diff --git a/frontend/features/map/components/chat-bot.tsx b/frontend/features/map/components/chat-bot.tsx
deleted file mode 100644
index 15e7563..0000000
--- a/frontend/features/map/components/chat-bot.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-/*
-========================================
-File: frontend/features/map/components/chat-bot.tsx
-========================================
-*/
-// This component seems redundant with chat-overlay.tsx.
-// Assuming it's removed or its logic is integrated into chat-overlay.tsx.
-// If needed, its structure would be similar to chat-overlay.tsx but maybe without the wrapper.
diff --git a/frontend/features/map/components/filters-overlay.tsx b/frontend/features/map/components/filters-overlay.tsx
deleted file mode 100644
index ee234f7..0000000
--- a/frontend/features/map/components/filters-overlay.tsx
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
-========================================
-File: frontend/features/map/components/filters-overlay.tsx
-========================================
-*/
-"use client";
-
-import { useState } from "react";
-import { Filter } from "lucide-react";
-import { Button } from "@/components/ui/button";
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
-import { Slider } from "@/components/ui/slider";
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import { Switch } from "@/components/ui/switch";
-import { Label } from "@/components/ui/label";
-import { ScrollArea } from "@/components/ui/scroll-area";
-import { Overlay } from "./overlay-system/overlay"; // Import local overlay system
-
-export function FiltersOverlay() {
- const [area, setArea] = useState("< 30 km");
- const [timePeriod, setTimePeriod] = useState("All Time");
- const [propertyType, setPropertyType] = useState("House");
- const [priceRange, setPriceRange] = useState([5_000_000, 20_000_000]);
- const [activeTab, setActiveTab] = useState("basic");
-
- const handleApplyFilters = () => {
- console.log("DUMMY: Applying filters:", {
- area,
- timePeriod,
- propertyType,
- priceRange, // Include advanced filters state here
- });
- // In real app: trigger data refetch with these filters
- };
-
- return (
- }
- initialPosition="bottom-left"
- initialIsOpen={true}
- width="350px">
-
- {" "}
- {/* Scrollable content */}
-
-
-
- Basic
- Advanced
-
-
-
-
-
-
- Area Radius
-
-
-
-
-
-
- {"< 10 km"}
- {"< 20 km"}
- {"< 30 km"}
- {"< 50 km"}
-
-
-
-
-
- Time Period
-
-
-
-
-
-
- Last Month
- Last 3 Months
- Last Year
- All Time
-
-
-
-
-
-
- Property Type
-
-
-
-
-
-
- House
- Condo
- Townhouse
- Land
- Commercial
-
-
-
-
-
-
-
-
-
-
- Price Range
-
-
- {new Intl.NumberFormat("th-TH", { notation: "compact" }).format(priceRange[0])} -{" "}
- {new Intl.NumberFormat("th-TH", { notation: "compact" }).format(priceRange[1])} ฿
-
-
-
-
-
-
-
Environmental Factors
-
-
-
- Low Flood Risk
-
-
-
-
-
- Good Air Quality
-
-
-
-
-
- Low Noise Pollution
-
-
-
-
-
-
-
-
-
-
- Apply Filters
-
-
-
-
- );
-}
diff --git a/frontend/features/map/components/map-container.tsx b/frontend/features/map/components/map-container.tsx
deleted file mode 100644
index 6466243..0000000
--- a/frontend/features/map/components/map-container.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
-========================================
-File: frontend/features/map/components/map-container.tsx
-========================================
-*/
-"use client";
-
-import React, { useEffect, useRef } from "react";
-import type { MapLocation } from "../types"; // Feature-specific type
-
-interface MapContainerProps {
- selectedLocation: MapLocation;
-}
-
-export function MapContainer({ selectedLocation }: MapContainerProps) {
- const mapRef = useRef(null);
-
- useEffect(() => {
- const mapElement = mapRef.current;
- console.log("DUMMY MAP: Initializing/updating for:", selectedLocation);
-
- if (mapElement) {
- // Placeholder for actual map library integration (e.g., Leaflet, Mapbox GL JS, Google Maps API)
- mapElement.innerHTML = `
-
- Map Placeholder: Centered on ${selectedLocation.name || "location"} (${selectedLocation.lat.toFixed(
- 4
- )}, ${selectedLocation.lng.toFixed(4)})
-
-
-
- `;
- // In a real app, you'd initialize the map library here, set view, add layers/markers.
- }
-
- // Cleanup function
- return () => {
- console.log("DUMMY MAP: Cleaning up map instance");
- if (mapElement) {
- mapElement.innerHTML = ""; // Clear placeholder
- // In a real app, you'd properly destroy the map instance here.
- }
- };
- }, [selectedLocation]); // Re-run effect if location changes
-
- return (
-
- {/* The map library will render into this div */}
-
- );
-}
diff --git a/frontend/features/map/components/map-header.tsx b/frontend/features/map/components/map-header.tsx
deleted file mode 100644
index 5cf61a4..0000000
--- a/frontend/features/map/components/map-header.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
-========================================
-File: frontend/features/map/components/map-header.tsx
-========================================
-*/
-"use client";
-
-import { ChevronRight } from "lucide-react";
-import Link from "next/link";
-import { Button } from "@/components/ui/button";
-import { ThemeToggle } from "@/components/common/ThemeToggle"; // Import from common
-
-export function MapHeader() {
- // Add any map-specific header logic here if needed
- return (
-
- );
-}
diff --git a/frontend/features/map/components/map-sidebar.tsx b/frontend/features/map/components/map-sidebar.tsx
deleted file mode 100644
index b5d8cc4..0000000
--- a/frontend/features/map/components/map-sidebar.tsx
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
-========================================
-File: frontend/features/map/components/map-sidebar.tsx
-========================================
-*/
-"use client";
-
-import React from "react";
-import Link from "next/link";
-import { usePathname } from "next/navigation";
-import {
- Home,
- Map, // Changed from Clock
- BarChart3, // Changed from Map
- Layers, // Changed from FileText
- Settings,
- SlidersHorizontal, // Changed from PenTool
- MessageCircle, // Changed from BarChart3
- Info, // Changed from Plane
- LineChart,
- DollarSign,
- MoreHorizontal,
- Gift, // Added Gift icon component below
-} from "lucide-react";
-import { cn } from "@/lib/utils";
-import {
- Sidebar,
- SidebarContent,
- SidebarFooter,
- SidebarHeader,
- SidebarMenu,
- SidebarMenuItem,
- SidebarMenuButton,
-} from "@/components/ui/sidebar"; // Assuming sidebar is a shared UI component structure
-
-export function MapSidebar() {
- const pathname = usePathname();
-
- // Define navigation items relevant to the map context or general app navigation shown here
- const mainNavItems = [
- { name: "Map View", icon: Map, href: "/map" },
- { name: "Analytics", icon: BarChart3, href: "/map/analytics" }, // Example sub-route
- { name: "Filters", icon: SlidersHorizontal, href: "/map/filters" }, // Example sub-route
- { name: "Data Layers", icon: Layers, href: "/map/layers" }, // Example sub-route
- { name: "Chat", icon: MessageCircle, href: "/map/chat" }, // Example sub-route
- { name: "Model Info", icon: Info, href: "/model-explanation" }, // Link to other feature
- { name: "Settings", icon: Settings, href: "/settings" }, // Example general setting
- { name: "More", icon: MoreHorizontal, href: "/more" }, // Example general setting
- ];
-
- // Example project-specific items (if sidebar is shared)
- const projectNavItems = [
- { name: "Market Trends", icon: LineChart, href: "/projects/trends" },
- { name: "Investment", icon: DollarSign, href: "/projects/investment" },
- ];
-
- return (
- // Using the shared Sidebar component structure
-
-
-
-
- B
-
- {/* Hide text when collapsed */}
- BorBann
-
-
-
-
-
- {mainNavItems.map((item) => (
-
-
-
-
- {/* Hide text when collapsed */}
- {item.name}
-
-
-
- ))}
-
-
- {/* Optional Project Section */}
- {/*
-
- Projects
-
-
- {projectNavItems.map((item) => (
-
- ...
-
- ))}
-
-
- */}
-
-
-
- {/* Footer content like user profile, settings shortcut etc. */}
-
-
- GG
-
-
-
GG_WPX
-
gg@example.com
-
-
-
-
- );
-}
-
-// Example Gift Icon (if not using lucide-react)
-function GiftIcon(props: React.SVGProps) {
- return (
-
-
-
-
-
-
-
- );
-}
diff --git a/frontend/features/map/components/overlay-system/overlay-context.tsx b/frontend/features/map/components/overlay-system/overlay-context.tsx
deleted file mode 100644
index d024455..0000000
--- a/frontend/features/map/components/overlay-system/overlay-context.tsx
+++ /dev/null
@@ -1,230 +0,0 @@
-/*
-========================================
-File: frontend/features/map/components/overlay-system/overlay-context.tsx
-========================================
-*/
-"use client";
-
-import React, { createContext, useContext, useState, useCallback, useRef, type ReactNode } from "react";
-
-// Define overlay types and positions
-export type OverlayId = string;
-export type OverlayPosition = "top-left" | "top-right" | "bottom-left" | "bottom-right" | "center";
-
-// Interface for overlay state
-export interface OverlayState {
- id: OverlayId;
- isOpen: boolean;
- isMinimized: boolean;
- position: OverlayPosition;
- zIndex: number;
- title: string;
- icon?: React.ReactNode;
-}
-
-// Interface for the overlay context
-interface OverlayContextType {
- overlays: Record;
- registerOverlay: (id: OverlayId, initialState: Partial>) => void;
- unregisterOverlay: (id: OverlayId) => void;
- openOverlay: (id: OverlayId) => void;
- closeOverlay: (id: OverlayId) => void;
- toggleOverlay: (id: OverlayId) => void;
- minimizeOverlay: (id: OverlayId) => void;
- maximizeOverlay: (id: OverlayId) => void;
- setPosition: (id: OverlayId, position: OverlayPosition) => void;
- bringToFront: (id: OverlayId) => void;
- getNextZIndex: () => number;
-}
-
-// Create the context
-const OverlayContext = createContext(undefined);
-
-// Default values for overlay state
-const defaultOverlayState: Omit = {
- isOpen: false,
- isMinimized: false,
- position: "bottom-right", // Default position
- zIndex: 10, // Starting z-index
-};
-
-export function OverlayProvider({ children }: { children: ReactNode }) {
- const [overlays, setOverlays] = useState>({});
- const maxZIndexRef = useRef(10); // Start z-index from 10
-
- // Get the next z-index value
- const getNextZIndex = useCallback(() => {
- maxZIndexRef.current += 1;
- return maxZIndexRef.current;
- }, []);
-
- // Register a new overlay
- const registerOverlay = useCallback(
- (id: OverlayId, initialState: Partial>) => {
- setOverlays((prev) => {
- if (prev[id]) {
- console.warn(`Overlay with id "${id}" already registered.`);
- return prev;
- }
- const newZIndex = initialState.isOpen ? getNextZIndex() : defaultOverlayState.zIndex;
- return {
- ...prev,
- [id]: {
- ...defaultOverlayState,
- id,
- title: id, // Default title to id
- ...initialState,
- zIndex: newZIndex, // Set initial z-index
- },
- };
- });
- },
- [getNextZIndex]
- );
-
- // Unregister an overlay
- const unregisterOverlay = useCallback((id: OverlayId) => {
- setOverlays((prev) => {
- const { [id]: _, ...rest } = prev; // Use destructuring to remove the key
- return rest;
- });
- }, []);
-
- // Open an overlay
- const openOverlay = useCallback(
- (id: OverlayId) => {
- setOverlays((prev) => {
- if (!prev[id] || prev[id].isOpen) return prev;
- return {
- ...prev,
- [id]: {
- ...prev[id],
- isOpen: true,
- isMinimized: false, // Ensure not minimized when opened
- zIndex: getNextZIndex(), // Bring to front
- },
- };
- });
- },
- [getNextZIndex]
- );
-
- // Close an overlay
- const closeOverlay = useCallback((id: OverlayId) => {
- setOverlays((prev) => {
- if (!prev[id] || !prev[id].isOpen) return prev;
- return {
- ...prev,
- [id]: { ...prev[id], isOpen: false },
- };
- });
- }, []);
-
- // Toggle an overlay's open/closed state
- const toggleOverlay = useCallback(
- (id: OverlayId) => {
- setOverlays((prev) => {
- if (!prev[id]) return prev; // Don't toggle non-existent overlays
-
- const willBeOpen = !prev[id].isOpen;
- const newZIndex = willBeOpen ? getNextZIndex() : prev[id].zIndex; // Bring to front only if opening
-
- return {
- ...prev,
- [id]: {
- ...prev[id],
- isOpen: willBeOpen,
- isMinimized: willBeOpen ? false : prev[id].isMinimized, // Maximize when toggling open
- zIndex: newZIndex,
- },
- };
- });
- },
- [getNextZIndex]
- );
-
- // Minimize an overlay
- const minimizeOverlay = useCallback((id: OverlayId) => {
- setOverlays((prev) => {
- if (!prev[id] || !prev[id].isOpen || prev[id].isMinimized) return prev; // Only minimize open, non-minimized overlays
- return {
- ...prev,
- [id]: {
- ...prev[id],
- isMinimized: true,
- // Optionally send to back when minimized: zIndex: defaultOverlayState.zIndex
- },
- };
- });
- }, []);
-
- // Maximize an overlay
- const maximizeOverlay = useCallback(
- (id: OverlayId) => {
- setOverlays((prev) => {
- if (!prev[id] || !prev[id].isOpen || !prev[id].isMinimized) return prev; // Only maximize minimized overlays
- return {
- ...prev,
- [id]: {
- ...prev[id],
- isMinimized: false,
- zIndex: getNextZIndex(), // Bring to front when maximized
- },
- };
- });
- },
- [getNextZIndex]
- );
-
- // Set the position of an overlay
- const setPosition = useCallback((id: OverlayId, position: OverlayPosition) => {
- setOverlays((prev) => {
- if (!prev[id]) return prev;
- return {
- ...prev,
- [id]: { ...prev[id], position },
- };
- });
- }, []);
-
- // Bring an overlay to the front
- const bringToFront = useCallback(
- (id: OverlayId) => {
- setOverlays((prev) => {
- if (!prev[id] || !prev[id].isOpen) return prev; // Only bring open overlays to front
- // Avoid getting new zIndex if already on top
- if (prev[id].zIndex === maxZIndexRef.current) return prev;
- return {
- ...prev,
- [id]: { ...prev[id], zIndex: getNextZIndex() },
- };
- });
- },
- [getNextZIndex]
- );
-
- const value = {
- overlays,
- registerOverlay,
- unregisterOverlay,
- openOverlay,
- closeOverlay,
- toggleOverlay,
- minimizeOverlay,
- maximizeOverlay,
- setPosition,
- bringToFront,
- getNextZIndex,
- };
-
- return {children} ;
-}
-
-// Hook to use the overlay context
-export function useOverlay() {
- const context = useContext(OverlayContext);
- if (context === undefined) {
- throw new Error("useOverlay must be used within an OverlayProvider");
- }
- return context;
-}
diff --git a/frontend/features/map/components/overlay-system/overlay-dock.tsx b/frontend/features/map/components/overlay-system/overlay-dock.tsx
deleted file mode 100644
index c39a0ff..0000000
--- a/frontend/features/map/components/overlay-system/overlay-dock.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
-========================================
-File: frontend/features/map/components/overlay-system/overlay-dock.tsx
-========================================
-*/
-"use client";
-
-import React from "react";
-import { Button } from "@/components/ui/button";
-import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
-import { useOverlay } from "./overlay-context";
-import { cn } from "@/lib/utils";
-
-interface OverlayDockProps {
- position?: "bottom" | "right" | "left" | "top"; // Added more positions
- className?: string;
-}
-
-export function OverlayDock({ position = "bottom", className }: OverlayDockProps) {
- const { overlays, toggleOverlay } = useOverlay();
-
- // Filter overlays that have icons defined (and potentially are registered)
- const dockableOverlays = Object.values(overlays).filter((overlay) => overlay.icon);
-
- // No need to render if there are no dockable overlays
- if (dockableOverlays.length === 0) return null;
-
- // Define CSS classes for different positions
- const positionClasses = {
- bottom: "fixed bottom-4 left-1/2 -translate-x-1/2 flex flex-row gap-2 z-50",
- right: "fixed right-4 top-1/2 -translate-y-1/2 flex flex-col gap-2 z-50",
- left: "fixed left-4 top-1/2 -translate-y-1/2 flex flex-col gap-2 z-50",
- top: "fixed top-4 left-1/2 -translate-x-1/2 flex flex-row gap-2 z-50",
- };
-
- const tooltipSide = {
- bottom: "top",
- top: "bottom",
- left: "right",
- right: "left",
- } as const;
-
- return (
-
-
- {dockableOverlays.map((overlay) => (
-
-
-
- toggleOverlay(overlay.id)}>
- {overlay.icon}
-
-
-
- {overlay.isOpen ? `Hide ${overlay.title}` : `Show ${overlay.title}`}
-
-
-
- ))}
-
-
- );
-}
diff --git a/frontend/features/map/components/overlay-system/overlay.tsx b/frontend/features/map/components/overlay-system/overlay.tsx
deleted file mode 100644
index 661b3cc..0000000
--- a/frontend/features/map/components/overlay-system/overlay.tsx
+++ /dev/null
@@ -1,219 +0,0 @@
-/*
-========================================
-File: frontend/features/map/components/overlay-system/overlay.tsx
-========================================
-*/
-"use client";
-
-import React, { useEffect, useState, useRef, useCallback } from "react";
-import { X, Minimize2, Maximize2, Move } from "lucide-react";
-import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
-import { Button } from "@/components/ui/button";
-import { cn } from "@/lib/utils";
-import { useOverlay, type OverlayId, type OverlayPosition } from "./overlay-context";
-
-interface OverlayProps {
- id: OverlayId;
- title: string;
- icon?: React.ReactNode;
- initialPosition?: OverlayPosition;
- initialIsOpen?: boolean;
- className?: string;
- children: React.ReactNode;
- onClose?: () => void;
- showMinimize?: boolean;
- width?: string;
- height?: string; // Can be 'auto' or specific value like '400px'
- maxHeight?: string; // e.g., '80vh'
-}
-
-export function Overlay({
- id,
- title,
- icon,
- initialPosition = "bottom-right",
- initialIsOpen = false,
- className,
- children,
- onClose,
- showMinimize = true,
- width = "350px",
- height = "auto",
- maxHeight = "80vh", // Default max height
-}: OverlayProps) {
- const {
- overlays,
- registerOverlay,
- unregisterOverlay,
- closeOverlay,
- minimizeOverlay,
- maximizeOverlay,
- bringToFront,
- // Add setPosition if dragging is implemented
- } = useOverlay();
-
- const overlayRef = useRef(null);
- // State for dragging logic (Optional, basic example commented out)
- // const [isDragging, setIsDragging] = useState(false);
- // const [offset, setOffset] = useState({ x: 0, y: 0 });
-
- // Register overlay on mount
- useEffect(() => {
- registerOverlay(id, {
- title,
- icon,
- position: initialPosition,
- isOpen: initialIsOpen,
- // Add initial zIndex if needed, otherwise context handles it
- });
-
- // Unregister on unmount
- return () => unregisterOverlay(id);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [id, registerOverlay, unregisterOverlay]); // Only run once on mount/unmount
-
- // Get the current state of this overlay
- const overlay = overlays[id];
-
- // --- Optional Dragging Logic ---
- // const handleMouseDown = useCallback((e: React.MouseEvent) => {
- // if (!overlayRef.current) return;
- // bringToFront(id);
- // setIsDragging(true);
- // const rect = overlayRef.current.getBoundingClientRect();
- // setOffset({
- // x: e.clientX - rect.left,
- // y: e.clientY - rect.top,
- // });
- // // Prevent text selection during drag
- // e.preventDefault();
- // }, [bringToFront, id]);
-
- // const handleMouseMove = useCallback((e: MouseEvent) => {
- // if (!isDragging || !overlayRef.current) return;
- // overlayRef.current.style.left = `${e.clientX - offset.x}px`;
- // overlayRef.current.style.top = `${e.clientY - offset.y}px`;
- // // Remove fixed positioning classes if dragging manually
- // overlayRef.current.classList.remove(...Object.values(positionClasses));
- // }, [isDragging, offset]);
-
- // const handleMouseUp = useCallback(() => {
- // if (isDragging) {
- // setIsDragging(false);
- // // Optional: Snap to edge or update position state in context
- // }
- // }, [isDragging]);
-
- // useEffect(() => {
- // if (isDragging) {
- // window.addEventListener("mousemove", handleMouseMove);
- // window.addEventListener("mouseup", handleMouseUp);
- // } else {
- // window.removeEventListener("mousemove", handleMouseMove);
- // window.removeEventListener("mouseup", handleMouseUp);
- // }
- // return () => {
- // window.removeEventListener("mousemove", handleMouseMove);
- // window.removeEventListener("mouseup", handleMouseUp);
- // };
- // }, [isDragging, handleMouseMove, handleMouseUp]);
- // --- End Optional Dragging Logic ---
-
- // If the overlay isn't registered yet or isn't open, don't render anything
- if (!overlay || !overlay.isOpen) return null;
-
- const handleCloseClick = (e: React.MouseEvent) => {
- e.stopPropagation(); // Prevent triggering bringToFront if clicking close
- closeOverlay(id);
- if (onClose) onClose();
- };
-
- const handleMinimizeClick = (e: React.MouseEvent) => {
- e.stopPropagation();
- minimizeOverlay(id);
- };
-
- const handleMaximizeClick = (e: React.MouseEvent) => {
- e.stopPropagation();
- maximizeOverlay(id);
- };
-
- const handleHeaderMouseDown = (e: React.MouseEvent) => {
- bringToFront(id);
- // handleMouseDown(e); // Uncomment if implementing dragging
- };
-
- // Define position classes based on the current state
- const positionClasses = {
- "top-left": "top-4 left-4",
- "top-right": "top-4 right-4",
- "bottom-left": "bottom-4 left-4",
- "bottom-right": "bottom-4 right-4",
- center: "top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2",
- };
-
- // Render minimized state in the dock (handled by OverlayDock now)
- if (overlay.isMinimized) {
- // Minimized state is now handled by the OverlayDock component based on context state
- // This component only renders the full overlay or nothing.
- return null;
- }
-
- // Render full overlay
- return (
- bringToFront(id)} // Bring to front on any click within the overlay
- aria-labelledby={`${id}-title`}
- role="dialog" // Use appropriate role
- >
-
- {/* Make header draggable */}
-
-
- {icon && {icon} }
- {title}
- {/* */} {/* Optional move icon */}
-
-
- {showMinimize && (
-
-
-
- )}
-
-
-
-
-
- {/* Ensure content area takes remaining space and scrolls if needed */}
- {children}
-
-
- );
-}
diff --git a/frontend/features/map/components/property-filters.tsx b/frontend/features/map/components/property-filters.tsx
deleted file mode 100644
index 2c9118c..0000000
--- a/frontend/features/map/components/property-filters.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-/*
-========================================
-File: frontend/features/map/components/property-filters.tsx
-========================================
-*/
-// This component seems redundant with filters-overlay.tsx.
-// Assuming it's removed or its logic is integrated into filters-overlay.tsx.
-// If needed, its structure would be similar to filters-overlay.tsx but maybe without the wrapper.
diff --git a/frontend/features/map/hooks/useMapInteractions.ts b/frontend/features/map/hooks/useMapInteractions.ts
deleted file mode 100644
index 71d29b7..0000000
--- a/frontend/features/map/hooks/useMapInteractions.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
-========================================
-File: frontend/features/map/hooks/useMapInteractions.ts (NEW - Dummy)
-========================================
-*/
-import { useState, useCallback } from "react";
-import type { MapLocation } from "../types";
-
-/**
- * DUMMY Hook: Manages map interaction state like selected markers, zoom level etc.
- */
-export function useMapInteractions(initialLocation: MapLocation) {
- const [currentLocation, setCurrentLocation] = useState(initialLocation);
- const [selectedMarkerId, setSelectedMarkerId] = useState(null);
-
- const handleMapMove = useCallback((newLocation: MapLocation) => {
- console.log("DUMMY Hook: Map moved to", newLocation);
- setCurrentLocation(newLocation);
- }, []);
-
- const handleMarkerClick = useCallback((markerId: string) => {
- console.log("DUMMY Hook: Marker clicked", markerId);
- setSelectedMarkerId(markerId);
- // Potentially fetch details for this marker via API
- }, []);
-
- const clearSelection = useCallback(() => {
- setSelectedMarkerId(null);
- }, []);
-
- return {
- currentLocation,
- selectedMarkerId,
- handleMapMove,
- handleMarkerClick,
- clearSelection,
- };
-}
diff --git a/frontend/features/map/types/index.ts b/frontend/features/map/types/index.ts
deleted file mode 100644
index eba6684..0000000
--- a/frontend/features/map/types/index.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
-========================================
-File: frontend/features/map/types/index.ts (NEW - Dummy)
-========================================
-*/
-// Types specific only to the Map feature
-
-export interface MapBounds {
- north: number;
- south: number;
- east: number;
- west: number;
-}
-
-export interface MapLocation {
- lat: number;
- lng: number;
- name?: string;
- zoom?: number;
-}
-
-// Example type for map layer configuration
-export interface MapLayerConfig {
- id: string;
- name: string;
- url: string; // e.g., Tile server URL template
- type: "raster" | "vector" | "geojson";
- visible: boolean;
- opacity?: number;
-}
-
-// Example type for data displayed on the map
-export interface MapPropertyData {
- id: string;
- coordinates: [number, number]; // [lng, lat]
- price: number;
- type: string;
-}
-
-// Re-export relevant shared types if needed for convenience
-// export type { PointOfInterest } from '@/types/api';
diff --git a/frontend/features/map/utils/mapHelpers.ts b/frontend/features/map/utils/mapHelpers.ts
deleted file mode 100644
index c1bca06..0000000
--- a/frontend/features/map/utils/mapHelpers.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
-========================================
-File: frontend/features/map/utils/mapHelpers.ts (NEW - Dummy)
-========================================
-*/
-
-import type { MapBounds, MapLocation } from "../types";
-
-/**
- * DUMMY Utility: Calculates the center of given map bounds.
- */
-export function calculateBoundsCenter(bounds: MapBounds): MapLocation {
- const centerLat = (bounds.north + bounds.south) / 2;
- const centerLng = (bounds.east + bounds.west) / 2;
- console.log("DUMMY Util: Calculating center for bounds:", bounds);
- return { lat: centerLat, lng: centerLng };
-}
-
-/**
- * DUMMY Utility: Formats coordinates for display.
- */
-export function formatCoords(location: MapLocation): string {
- return `${location.lat.toFixed(4)}, ${location.lng.toFixed(4)}`;
-}
-
-// Add other map-specific utility functions here
diff --git a/frontend/features/model-explanation/api/explanationApi.ts b/frontend/features/model-explanation/api/explanationApi.ts
deleted file mode 100644
index 5afefed..0000000
--- a/frontend/features/model-explanation/api/explanationApi.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
-========================================
-File: frontend/features/model-explanation/api/explanationApi.ts (NEW - Dummy)
-========================================
-*/
-import apiClient from "@/services/apiClient";
-import type { APIResponse } from "@/types/api";
-import type { ModelExplanationData, FeatureImportance } from "../types";
-
-/**
- * DUMMY: Fetches data needed for the model explanation page.
- */
-export async function fetchModelExplanation(propertyId: string): Promise> {
- console.log(`DUMMY API: Fetching model explanation for property ID: ${propertyId}`);
- // return apiClient.get(`/properties/${propertyId}/explanation`);
-
- // Simulate response
- await new Promise((resolve) => setTimeout(resolve, 700));
- const dummyExplanation: ModelExplanationData = {
- propertyDetails: {
- address: `Dummy Property ${propertyId}`,
- type: "Condo",
- size: 120,
- bedrooms: 2,
- bathrooms: 2,
- age: 3,
- floor: 10,
- amenities: ["Pool", "Gym"],
- predictedPrice: 12500000,
- },
- similarProperties: [
- { address: "Comp 1", price: 12000000, size: 115, age: 4 },
- { address: "Comp 2", price: 13500000, size: 130, age: 2 },
- ],
- features: [
- { name: "Location", importance: 40, value: "Near BTS", impact: "positive" },
- { name: "Size", importance: 30, value: "120 sqm", impact: "positive" },
- { name: "Age", importance: 15, value: "3 years", impact: "neutral" },
- { name: "Amenities", importance: 10, value: "Pool, Gym", impact: "positive" },
- { name: "Floor", importance: 5, value: "10th", impact: "positive" },
- ],
- environmentalFactors: {
- floodRisk: "low",
- airQuality: "moderate",
- noiseLevel: "low",
- },
- confidence: 0.91,
- priceRange: { lower: 11800000, upper: 13200000 },
- };
- return { success: true, data: dummyExplanation };
-}
diff --git a/frontend/features/model-explanation/components/feature-importance-chart.tsx b/frontend/features/model-explanation/components/feature-importance-chart.tsx
deleted file mode 100644
index db845a8..0000000
--- a/frontend/features/model-explanation/components/feature-importance-chart.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
-========================================
-File: frontend/features/model-explanation/components/feature-importance-chart.tsx
-========================================
-*/
-"use client";
-
-import { useTheme } from "next-themes";
-import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell } from "@/components/ui/chart"; // Using shared ui chart components
-import type { FeatureImportance } from "../types"; // Feature specific type
-
-interface FeatureImportanceChartProps {
- features: FeatureImportance[];
-}
-
-export function FeatureImportanceChart({ features }: FeatureImportanceChartProps) {
- const { theme } = useTheme();
- const isDark = theme === "dark";
-
- // Sort features by importance for consistent display
- const sortedFeatures = [...features].sort((a, b) => b.importance - a.importance);
-
- // Define colors based on impact
- const getBarColor = (impact: "positive" | "negative" | "neutral") => {
- if (impact === "positive") return "#10b981"; // Green
- if (impact === "negative") return "#ef4444"; // Red
- return "#f59e0b"; // Amber/Yellow for neutral
- };
-
- return (
-
-
-
- `${value}%`}
- stroke={isDark ? "#9ca3af" : "#6b7280"}
- />
-
- [`${value.toFixed(1)}%`, "Importance"]}
- labelFormatter={(label: string) => `Feature: ${label}`} // Show feature name in tooltip
- contentStyle={{
- backgroundColor: isDark ? "#1f2937" : "white",
- borderRadius: "0.375rem",
- border: isDark ? "1px solid #374151" : "1px solid #e2e8f0",
- fontSize: "0.75rem",
- color: isDark ? "#e5e7eb" : "#1f2937",
- }}
- />
-
- {sortedFeatures.map((entry, index) => (
- |
- ))}
-
-
-
- );
-}
diff --git a/frontend/features/model-explanation/components/price-comparison-chart.tsx b/frontend/features/model-explanation/components/price-comparison-chart.tsx
deleted file mode 100644
index 8771f4d..0000000
--- a/frontend/features/model-explanation/components/price-comparison-chart.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
-========================================
-File: frontend/features/model-explanation/components/price-comparison-chart.tsx
-========================================
-*/
-"use client";
-
-import { useTheme } from "next-themes";
-import {
- BarChart,
- Bar,
- XAxis,
- YAxis,
- CartesianGrid,
- Tooltip,
- ResponsiveContainer,
- Legend,
- Cell,
-} from "@/components/ui/chart"; // Using shared ui chart components
-import type { ComparableProperty, PropertyBaseDetails } from "../types"; // Feature specific types
-
-interface PriceComparisonChartProps {
- property: PropertyBaseDetails & { name: string }; // Add name for the primary property
- comparisons: ComparableProperty[];
-}
-
-export function PriceComparisonChart({ property, comparisons }: PriceComparisonChartProps) {
- const { theme } = useTheme();
- const isDark = theme === "dark";
-
- // Combine property and comparisons for chart data
- // Ensure the property being explained is included and identifiable
- const data = [
- { ...property }, // Keep all details for tooltip if needed
- ...comparisons.map((c) => ({ ...c, name: c.address })), // Use address as name for comparisons
- ];
-
- // Format the price for display
- const formatPrice = (value: number) => {
- return new Intl.NumberFormat("th-TH", {
- style: "currency",
- currency: "THB",
- notation: "compact", // Use compact notation like 15M
- minimumFractionDigits: 0,
- maximumFractionDigits: 1,
- }).format(value);
- };
-
- // Custom tooltip content
- const CustomTooltip = ({ active, payload, label }: any) => {
- if (active && payload && payload.length) {
- const data = payload[0].payload; // Get the data point for this bar
- return (
-
-
{label}
-
Price: {formatPrice(data.price)}
-
Size: {data.size} sqm
-
Age: {data.age} years
-
- );
- }
- return null;
- };
-
- return (
-
-
-
-
- formatPrice(value)}
- stroke={isDark ? "#9ca3af" : "#6b7280"}
- fontSize={10}
- width={40}
- />
- } // Use custom tooltip
- wrapperStyle={{ zIndex: 100 }} // Ensure tooltip is on top
- />
- {/* // Legend might be redundant if XAxis labels are clear */}
-
- {data.map((entry, index) => (
- |
- ))}
-
-
-
- );
-}
diff --git a/frontend/features/model-explanation/types/index.ts b/frontend/features/model-explanation/types/index.ts
deleted file mode 100644
index 9dd42cd..0000000
--- a/frontend/features/model-explanation/types/index.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
-========================================
-File: frontend/features/model-explanation/types/index.ts (NEW - Dummy)
-========================================
-*/
-
-// Types specific to the Model Explanation feature
-
-export interface FeatureImportance {
- name: string;
- importance: number; // e.g., percentage 0-100
- value: string | number; // The actual value for the property being explained
- impact: "positive" | "negative" | "neutral";
-}
-
-export interface ComparableProperty {
- address: string;
- price: number;
- size: number;
- age: number;
- // Add other relevant comparison fields if needed
-}
-
-export interface PropertyBaseDetails {
- address: string;
- type: string;
- size: number;
- bedrooms?: number;
- bathrooms?: number;
- age: number;
- floor?: number;
- amenities?: string[];
- predictedPrice: number;
-}
-
-export interface EnvironmentalFactors {
- floodRisk: "low" | "moderate" | "high" | "unknown";
- airQuality: "good" | "moderate" | "poor" | "unknown";
- noiseLevel: "low" | "moderate" | "high" | "unknown";
- // Add other factors like proximity scores etc.
-}
-
-export interface ModelExplanationData {
- propertyDetails: PropertyBaseDetails;
- similarProperties: ComparableProperty[];
- features: FeatureImportance[];
- environmentalFactors: EnvironmentalFactors;
- confidence: number; // e.g., 0.92 for 92%
- priceRange: { lower: number; upper: number };
-}
diff --git a/frontend/hooks/use-mobile.tsx b/frontend/hooks/use-mobile.tsx
index 0d6b1fe..2b0fe1d 100644
--- a/frontend/hooks/use-mobile.tsx
+++ b/frontend/hooks/use-mobile.tsx
@@ -1,37 +1,19 @@
-/*
-========================================
-File: frontend/hooks/use-mobile.tsx
-========================================
-*/
-import * as React from "react";
+import * as React from "react"
-const MOBILE_BREAKPOINT = 768; // Standard md breakpoint
+const MOBILE_BREAKPOINT = 768
-export function useIsMobile(): boolean {
- // Initialize state based on current window size (or undefined if SSR)
- const [isMobile, setIsMobile] = React.useState(
- typeof window !== "undefined" ? window.innerWidth < MOBILE_BREAKPOINT : undefined
- );
+export function useIsMobile() {
+ const [isMobile, setIsMobile] = React.useState(undefined)
React.useEffect(() => {
- // Ensure this runs only client-side
- if (typeof window === "undefined") {
- return;
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
+ const onChange = () => {
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
+ mql.addEventListener("change", onChange)
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+ return () => mql.removeEventListener("change", onChange)
+ }, [])
- const handleResize = () => {
- setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
- };
-
- // Set initial state correctly after mount
- handleResize();
-
- window.addEventListener("resize", handleResize);
-
- // Cleanup listener on unmount
- return () => window.removeEventListener("resize", handleResize);
- }, []); // Empty dependency array ensures this runs only once on mount and cleanup on unmount
-
- // Return false during SSR or initial client render before effect runs
- return isMobile ?? false;
+ return !!isMobile
}
diff --git a/frontend/hooks/use-toast.ts b/frontend/hooks/use-toast.ts
index eebb31c..02e111d 100644
--- a/frontend/hooks/use-toast.ts
+++ b/frontend/hooks/use-toast.ts
@@ -1,106 +1,106 @@
-/*
-========================================
-File: frontend/hooks/use-toast.ts
-========================================
-*/
-"use client";
+"use client"
// Inspired by react-hot-toast library
-import * as React from "react";
-// Import types from the actual Toast component location
-import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
+import * as React from "react"
-const TOAST_LIMIT = 1; // Show only one toast at a time
-const TOAST_REMOVE_DELAY = 1000000; // A very long time (effectively manual dismiss only)
+import type {
+ ToastActionElement,
+ ToastProps,
+} from "@/components/ui/toast"
+
+const TOAST_LIMIT = 1
+const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
- id: string;
- title?: React.ReactNode;
- description?: React.ReactNode;
- action?: ToastActionElement;
-};
+ id: string
+ title?: React.ReactNode
+ description?: React.ReactNode
+ action?: ToastActionElement
+}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
-} as const;
+} as const
-let count = 0;
+let count = 0
function genId() {
- count = (count + 1) % Number.MAX_SAFE_INTEGER;
- return count.toString();
+ count = (count + 1) % Number.MAX_SAFE_INTEGER
+ return count.toString()
}
-type ActionType = typeof actionTypes;
+type ActionType = typeof actionTypes
type Action =
| {
- type: ActionType["ADD_TOAST"];
- toast: ToasterToast;
+ type: ActionType["ADD_TOAST"]
+ toast: ToasterToast
}
| {
- type: ActionType["UPDATE_TOAST"];
- toast: Partial;
+ type: ActionType["UPDATE_TOAST"]
+ toast: Partial
}
| {
- type: ActionType["DISMISS_TOAST"];
- toastId?: ToasterToast["id"];
+ type: ActionType["DISMISS_TOAST"]
+ toastId?: ToasterToast["id"]
}
| {
- type: ActionType["REMOVE_TOAST"];
- toastId?: ToasterToast["id"];
- };
+ type: ActionType["REMOVE_TOAST"]
+ toastId?: ToasterToast["id"]
+ }
interface State {
- toasts: ToasterToast[];
+ toasts: ToasterToast[]
}
-const toastTimeouts = new Map>();
+const toastTimeouts = new Map>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
- return;
+ return
}
const timeout = setTimeout(() => {
- toastTimeouts.delete(toastId);
+ toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
- });
- }, TOAST_REMOVE_DELAY);
+ })
+ }, TOAST_REMOVE_DELAY)
- toastTimeouts.set(toastId, timeout);
-};
+ toastTimeouts.set(toastId, timeout)
+}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
- // Slice ensures the limit is enforced
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
- };
+ }
case "UPDATE_TOAST":
return {
...state,
- toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
- };
+ toasts: state.toasts.map((t) =>
+ t.id === action.toast.id ? { ...t, ...action.toast } : t
+ ),
+ }
case "DISMISS_TOAST": {
- const { toastId } = action;
+ const { toastId } = action
- // Side effect: schedule removal for dismissed toasts
+ // ! Side effects ! - This could be extracted into a dismissToast() action,
+ // but I'll keep it here for simplicity
if (toastId) {
- addToRemoveQueue(toastId);
+ addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
- addToRemoveQueue(toast.id);
- });
+ addToRemoveQueue(toast.id)
+ })
}
return {
@@ -109,48 +109,48 @@ export const reducer = (state: State, action: Action): State => {
t.id === toastId || toastId === undefined
? {
...t,
- open: false, // Trigger the close animation
+ open: false,
}
: t
),
- };
+ }
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
- toasts: [], // Remove all toasts
- };
+ toasts: [],
+ }
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
- };
+ }
}
-};
-
-const listeners: Array<(state: State) => void> = [];
-
-let memoryState: State = { toasts: [] }; // In-memory state
-
-function dispatch(action: Action) {
- memoryState = reducer(memoryState, action);
- listeners.forEach((listener) => {
- listener(memoryState);
- });
}
-type Toast = Omit;
+const listeners: Array<(state: State) => void> = []
+
+let memoryState: State = { toasts: [] }
+
+function dispatch(action: Action) {
+ memoryState = reducer(memoryState, action)
+ listeners.forEach((listener) => {
+ listener(memoryState)
+ })
+}
+
+type Toast = Omit
function toast({ ...props }: Toast) {
- const id = genId();
+ const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
- });
- const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
+ })
+ const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
@@ -159,37 +159,36 @@ function toast({ ...props }: Toast) {
id,
open: true,
onOpenChange: (open) => {
- if (!open) dismiss(); // Ensure dismiss is called when the toast closes itself
+ if (!open) dismiss()
},
},
- });
+ })
return {
id: id,
dismiss,
update,
- };
+ }
}
function useToast() {
- const [state, setState] = React.useState(memoryState);
+ const [state, setState] = React.useState(memoryState)
React.useEffect(() => {
- listeners.push(setState);
+ listeners.push(setState)
return () => {
- // Clean up listener
- const index = listeners.indexOf(setState);
+ const index = listeners.indexOf(setState)
if (index > -1) {
- listeners.splice(index, 1);
+ listeners.splice(index, 1)
}
- };
- }, [state]); // Only re-subscribe if state instance changes (it shouldn't)
+ }
+ }, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
- };
+ }
}
-export { useToast, toast };
+export { useToast, toast }
diff --git a/frontend/public/map.png b/frontend/public/map.png
new file mode 100644
index 0000000..a90e444
Binary files /dev/null and b/frontend/public/map.png differ
diff --git a/frontend/services/apiClient.ts b/frontend/services/apiClient.ts
index 9088254..d048b17 100644
--- a/frontend/services/apiClient.ts
+++ b/frontend/services/apiClient.ts
@@ -1,106 +1,135 @@
-/*
-========================================
-File: frontend/services/apiClient.ts (NEW - Dummy)
-========================================
-*/
-import type { APIResponse } from "@/types/api"; // Import shared response type
+/* === src/services/apiClient.ts === */
+/**
+ * API Client - Dummy Implementation
+ *
+ * This provides a basic structure for making API calls.
+ * - It includes an Authorization header in each request (assuming token-based auth).
+ * - Replace `getAuthToken` with your actual token retrieval logic.
+ * - Consider using libraries like axios or ky for more robust features in a real app.
+ */
-// --- Dummy Auth Token ---
-// In a real app, this would come from localStorage, context, or a state manager after login
-const DUMMY_AUTH_TOKEN = "Bearer dummy-jwt-token-12345";
+const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "/api/v1"; // Example API base URL
/**
- * Base function for making API requests.
- * Includes dummy authorization header.
+ * Retrieves the authentication token.
+ * Replace this with your actual implementation (e.g., from localStorage, context, state management).
+ * @returns {string | null} The auth token or null if not found.
*/
-async function fetchApi(endpoint: string, options: RequestInit = {}): Promise> {
- const url = `${process.env.NEXT_PUBLIC_API_URL || "/api/v1"}${endpoint}`;
+const getAuthToken = (): string | null => {
+ // Dummy implementation: Replace with your actual logic
+ if (typeof window !== "undefined") {
+ // Example: return localStorage.getItem("authToken");
+ // For dummy purposes, returning a placeholder token
+ return "dummy-auth-token-12345";
+ }
+ return null;
+};
- const defaultHeaders: HeadersInit = {
- "Content-Type": "application/json",
- Authorization: DUMMY_AUTH_TOKEN, // Add dummy token here
- };
+interface FetchOptions extends RequestInit {
+ params?: Record; // For query parameters
+ /** If true, Content-Type header will not be set (e.g., for FormData) */
+ skipContentType?: boolean;
+}
- const config: RequestInit = {
- ...options,
- headers: {
- ...defaultHeaders,
- ...options.headers,
- },
- };
+/**
+ * Generic fetch function for API calls.
+ * @template T The expected response type.
+ * @param {string} endpoint The API endpoint (e.g., '/users').
+ * @param {FetchOptions} options Fetch options (method, body, headers, params).
+ * @returns {Promise} The response data.
+ */
+async function apiClient(endpoint: string, options: FetchOptions = {}): Promise {
+ const { params, headers: customHeaders, skipContentType, body, ...fetchOptions } = options;
+ const token = getAuthToken();
- console.log(`DUMMY API Client: Requesting ${config.method || "GET"} ${url}`);
+ // Construct URL
+ let url = `${API_BASE_URL}${endpoint}`;
+ if (params) {
+ const queryParams = new URLSearchParams();
+ Object.entries(params).forEach(([key, value]) => {
+ if (value !== undefined && value !== null) {
+ // Ensure value exists
+ queryParams.append(key, String(value));
+ }
+ });
+ const queryString = queryParams.toString();
+ if (queryString) {
+ url += `?${queryString}`;
+ }
+ }
- // Simulate network delay
- await new Promise((resolve) => setTimeout(resolve, Math.random() * 300 + 100));
+ // Prepare headers
+ const headers = new Headers(customHeaders);
+
+ if (!skipContentType && body && !(body instanceof FormData)) {
+ if (!headers.has("Content-Type")) {
+ headers.set("Content-Type", "application/json");
+ }
+ }
+
+ if (token && !headers.has("Authorization")) {
+ headers.set("Authorization", `Bearer ${token}`);
+ }
+
+ // Prepare body - Stringify JSON unless it's FormData
+ let processedBody = body;
+ if (body && headers.get("Content-Type") === "application/json" && typeof body !== "string") {
+ processedBody = JSON.stringify(body);
+ }
try {
- // --- Simulate API Responses ---
- // You can add more sophisticated simulation based on the endpoint
- if (endpoint.includes("error")) {
- console.warn(`DUMMY API Client: Simulating error for ${url}`);
- return { success: false, error: "Simulated server error" };
+ const response = await fetch(url, {
+ ...fetchOptions,
+ headers,
+ body: processedBody,
+ });
+
+ // Check for successful response
+ if (!response.ok) {
+ let errorData = { message: `HTTP error! status: ${response.status} ${response.statusText}` };
+ try {
+ // Try to parse specific error details from the API response
+ const jsonError = await response.json();
+ errorData = { ...errorData, ...jsonError };
+ } catch (e) {
+ // Ignore if the error response is not JSON
+ }
+ console.error("API Error:", errorData);
+ throw new Error(errorData.message);
}
- if (config.method === "POST" || config.method === "PUT" || config.method === "DELETE") {
- // Simulate simple success for modification requests
- console.log(`DUMMY API Client: Simulating success for ${config.method} ${url}`);
- return { success: true, data: { message: "Operation successful (simulated)" } as T };
+ // Handle responses with no content (e.g., 204 No Content)
+ if (response.status === 204) {
+ // For 204, there's no body, return undefined or null as appropriate for T
+ // Using 'as T' assumes the caller expects undefined/null for no content
+ return undefined as T;
}
- // Simulate success for GET requests (return empty array or specific data based on endpoint)
- console.log(`DUMMY API Client: Simulating success for GET ${url}`);
- let responseData: any = [];
- if (endpoint.includes("/map/pois")) responseData = []; // Let feature api add dummy data
- if (endpoint.includes("/properties")) responseData = []; // Example
- // Add more specific endpoint data simulation if needed
-
- return { success: true, data: responseData as T };
-
- // --- Real Fetch Logic (keep commented for dummy) ---
- // const response = await fetch(url, config);
- //
- // if (!response.ok) {
- // let errorData;
- // try {
- // errorData = await response.json();
- // } catch (e) {
- // errorData = { detail: response.statusText || "Unknown error" };
- // }
- // const errorMessage = errorData?.detail || `HTTP error ${response.status}`;
- // console.error(`API Error (${response.status}) for ${url}:`, errorMessage);
- // return { success: false, error: errorMessage, details: errorData };
- // }
- //
- // // Handle cases with no content
- // if (response.status === 204) {
- // return { success: true, data: null as T };
- // }
- //
- // const data: T = await response.json();
- // return { success: true, data };
- // --- End Real Fetch Logic ---
+ // Parse the JSON response body for other successful responses
+ const data: T = await response.json();
+ return data;
} catch (error) {
- console.error(`Network or other error for ${url}:`, error);
- const errorMessage = error instanceof Error ? error.message : "Network error or invalid response";
- return { success: false, error: errorMessage };
+ console.error("API Client Fetch Error:", error);
+ // Re-throw the error for handling by the calling code (e.g., React Query, component)
+ throw error;
}
}
-// --- Convenience Methods ---
-const apiClient = {
- get: (endpoint: string, options?: RequestInit) => fetchApi(endpoint, { ...options, method: "GET" }),
+// --- Specific HTTP Method Helpers ---
- post: (endpoint: string, body: any, options?: RequestInit) =>
- fetchApi(endpoint, { ...options, method: "POST", body: JSON.stringify(body) }),
+export const api = {
+ get: (endpoint: string, options?: FetchOptions) => apiClient(endpoint, { ...options, method: "GET" }),
- put: (endpoint: string, body: any, options?: RequestInit) =>
- fetchApi(endpoint, { ...options, method: "PUT", body: JSON.stringify(body) }),
+ post: (endpoint: string, body: any, options?: FetchOptions) =>
+ apiClient(endpoint, { ...options, method: "POST", body }),
- delete: (endpoint: string, options?: RequestInit) => fetchApi(endpoint, { ...options, method: "DELETE" }),
+ put: (endpoint: string, body: any, options?: FetchOptions) =>
+ apiClient(endpoint, { ...options, method: "PUT", body }),
- patch: (endpoint: string, body: any, options?: RequestInit) =>
- fetchApi(endpoint, { ...options, method: "PATCH", body: JSON.stringify(body) }),
+ patch: (endpoint: string, body: any, options?: FetchOptions) =>
+ apiClient(endpoint, { ...options, method: "PATCH", body }),
+
+ delete: (endpoint: string, options?: FetchOptions) => apiClient(endpoint, { ...options, method: "DELETE" }),
};
-export default apiClient;
+export default api;
diff --git a/frontend/types/api.ts b/frontend/types/api.ts
index a756412..8c46cf5 100644
--- a/frontend/types/api.ts
+++ b/frontend/types/api.ts
@@ -1,41 +1,175 @@
-/*
-========================================
-File: frontend/types/api.ts (NEW - Dummy Shared Types)
-========================================
-*/
-
-/** Generic API Response Structure */
-export type APIResponse = { success: true; data: T } | { success: false; error: string; details?: any };
-
-/** Represents a Point of Interest (can be property, cafe, etc.) */
-export interface PointOfInterest {
- id: string;
- lat: number;
- lng: number;
- name: string;
- type: string; // e.g., 'property', 'cafe', 'park', 'station'
- price?: number; // Optional: for properties
- rating?: number; // Optional: for amenities
- // Add other relevant shared fields
+/* === src/types/api.ts === */
+/**
+ * General API Response Types
+ */
+export interface PaginatedResponse {
+ count: number;
+ next: string | null;
+ previous: string | null;
+ results: T[];
}
-/** Basic Property Information */
-export interface PropertySummary {
- id: string;
- address: string;
- lat: number;
- lng: number;
+export interface ApiErrorResponse {
+ message: string;
+ details?: Record | string[]; // Can be an object or array of strings
+ code?: string; // Optional error code
+}
+
+/**
+ * Represents a generic successful API operation, possibly with no specific data.
+ */
+export interface SuccessResponse {
+ success: boolean;
+ message?: string;
+}
+
+/**
+ * Property Data Types
+ */
+export type PropertyType = "Condominium" | "House" | "Townhouse" | "Land" | "Apartment" | "Other";
+export type OwnershipType = "Freehold" | "Leasehold";
+export type FurnishingStatus = "Unfurnished" | "Partly Furnished" | "Fully Furnished";
+
+export interface PriceHistoryEntry {
+ date: string; // Consider using Date object after parsing
price: number;
- type: string; // e.g., 'Condo', 'House'
- size?: number; // sqm
}
-/** User representation */
-export interface User {
+export interface MarketTrendData {
+ areaGrowthPercentage: number;
+ similarPropertiesCount: number;
+ averagePrice: number;
+ averagePricePerSqm: number;
+ priceTrend?: "Rising" | "Falling" | "Stable"; // Optional trend indicator
+}
+
+export interface EnvironmentalFactor {
+ type: "Flood Risk" | "Air Quality" | "Noise Level";
+ level: "Low" | "Moderate" | "High" | "Good" | "Fair" | "Poor"; // Adjust levels as needed
+ details?: string;
+}
+
+export interface NearbyFacility {
+ name: string;
+ type: "Transport" | "Shopping" | "Park" | "Hospital" | "School" | "Other";
+ distanceMeters: number;
+}
+
+export interface Property {
id: string;
- username: string;
- email: string;
- // Add roles or other relevant fields
+ title: string;
+ description: string;
+ price: number;
+ currency?: string; // e.g., 'THB', 'USD'
+ location: {
+ address: string;
+ city: string;
+ district?: string;
+ postalCode?: string;
+ latitude?: number;
+ longitude?: number;
+ };
+ bedrooms: number;
+ bathrooms: number;
+ areaSqm: number;
+ propertyType: PropertyType;
+ images: string[]; // Array of image URLs
+ yearBuilt?: number;
+ floorLevel?: number;
+ totalFloors?: number;
+ parkingSpaces?: number;
+ furnishing: FurnishingStatus;
+ ownership: OwnershipType;
+ availabilityDate?: string; // Consider using Date object
+ isPremium: boolean;
+ features: string[];
+ amenities: string[];
+ priceHistory?: PriceHistoryEntry[];
+ marketTrends?: MarketTrendData;
+ environmentalFactors?: EnvironmentalFactor[];
+ nearbyFacilities?: NearbyFacility[];
+ agent?: {
+ // Optional agent info
+ id: string;
+ name: string;
+ contact: string;
+ };
+ dataSource?: string; // Origin of the data
+ createdAt: string; // ISO 8601 date string
+ updatedAt: string; // ISO 8601 date string
}
-// Add other globally shared types (e.g., PipelineStatus, DataSourceType if needed FE side)
+/**
+ * Data Pipeline Types
+ */
+export type PipelineStatus = "active" | "paused" | "error" | "idle" | "running";
+export type SourceType = "Website" | "API" | "File Upload" | "Database";
+
+export interface DataSource {
+ id: string;
+ name: string;
+ type: SourceType;
+ url?: string; // For Website/API
+ lastUpdated: string;
+ recordCount: number;
+ status: "connected" | "error" | "pending";
+}
+
+export interface DataPipeline {
+ id: string;
+ name: string;
+ description: string;
+ status: PipelineStatus;
+ lastRunAt: string | null;
+ nextRunAt: string | null;
+ runFrequency: string; // e.g., "Daily", "Hourly", "Manual"
+ sources: DataSource[];
+ totalRecords: number;
+ aiPowered: boolean;
+ errorDetails?: string;
+ createdAt: string;
+ updatedAt: string;
+}
+
+/**
+ * Model Types
+ */
+export type ModelType = "Regression" | "Neural Network" | "Geospatial" | "Time Series" | "Ensemble" | "Classification";
+export type ModelStatus = "active" | "inactive" | "training" | "error" | "pending";
+
+export interface Model {
+ id: string;
+ name: string;
+ description?: string;
+ type: ModelType;
+ version: string;
+ status: ModelStatus;
+ isSystemModel: boolean; // Distinguishes system models from custom ones
+ dataSourceId?: string; // ID of the DataPipeline used for training (if custom)
+ dataSourceName?: string; // Name of the source for display
+ hyperparameters: Record;
+ performanceMetrics?: Record; // e.g., { accuracy: 0.92, mae: 150000 }
+ lastTrainedAt?: string;
+ createdAt: string;
+ updatedAt: string;
+}
+
+/**
+ * Map Related Types
+ */
+export interface MapLayer {
+ id: string;
+ name: string;
+ type: "property" | "heatmap" | "environmental";
+ isVisible: boolean;
+ dataUrl?: string; // URL to fetch layer data
+ style?: Record; // Mapbox style properties
+}
+
+export interface MapViewState {
+ longitude: number;
+ latitude: number;
+ zoom: number;
+ pitch?: number;
+ bearing?: number;
+}
diff --git a/frontend/types/index.ts b/frontend/types/index.ts
index 01638fe..4f032c4 100644
--- a/frontend/types/index.ts
+++ b/frontend/types/index.ts
@@ -1,12 +1,22 @@
-/*
-========================================
-File: frontend/types/index.ts (NEW - Barrel File)
-========================================
-*/
+/* === src/types/index.ts === */
+// Barrel file for exporting shared types
-// Re-export shared types for easier importing
export * from "./api";
+export * from "./user";
-// You can add other shared types here or export from other files in this directory
-// export * from './user';
+// Example of another shared type definition
+export interface SelectOption {
+ value: T;
+ label: string;
+ icon?: React.ComponentType<{ className?: string }>; // Optional icon component
+ disabled?: boolean;
+}
+
+// Generic type for UI components requiring an icon
+export interface IconProps {
+ className?: string;
+}
+
+// Add other shared types as the application grows
// export * from './settings';
+// export * from './notifications';
diff --git a/frontend/types/user.ts b/frontend/types/user.ts
new file mode 100644
index 0000000..427e6e8
--- /dev/null
+++ b/frontend/types/user.ts
@@ -0,0 +1,56 @@
+/* === src/types/user.ts === */
+/**
+ * User Profile and Authentication Types
+ */
+
+export interface UserProfile {
+ id: string;
+ username: string;
+ email: string;
+ firstName?: string;
+ lastName?: string;
+ avatarUrl?: string; // URL to the user's avatar image
+ roles: UserRole[]; // Array of roles assigned to the user
+ preferences?: UserPreferences; // User-specific settings
+ isActive: boolean;
+ lastLogin?: string; // ISO 8601 date string
+ createdAt: string; // ISO 8601 date string
+}
+
+export type UserRole = "admin" | "analyst" | "viewer" | "data_manager"; // Example roles
+
+export interface UserPreferences {
+ theme?: "light" | "dark" | "system";
+ defaultMapLocation?: {
+ latitude: number;
+ longitude: number;
+ zoom: number;
+ };
+ notifications?: {
+ pipelineSuccess?: boolean;
+ pipelineError?: boolean;
+ newReports?: boolean;
+ };
+ // Add other user-specific preferences
+}
+
+export interface AuthState {
+ user: UserProfile | null;
+ token: string | null; // The authentication token (e.g., JWT)
+ isAuthenticated: boolean;
+ isLoading: boolean; // Tracks loading state during auth checks/login/logout
+ error: string | null; // Stores any authentication errors
+}
+
+// Type for login credentials
+export interface LoginCredentials {
+ emailOrUsername: string;
+ password?: string; // Password might not be needed for SSO flows
+}
+
+// Type for the response after successful login
+export interface LoginResponse {
+ user: UserProfile;
+ token: string;
+ refreshToken?: string; // Optional refresh token
+}