From 7ab14fad02eb5397e1221f5f1968d93a2e3d4902 Mon Sep 17 00:00:00 2001 From: Sosokker Date: Mon, 7 Apr 2025 23:44:03 +0700 Subject: [PATCH] refactor: restructure the frontend --- frontend/README.md | 122 ++- .../app/(routes)/map-explanation/page.tsx | 703 ++++++++++++++++++ frontend/app/(routes)/map/layout.tsx | 21 + frontend/app/(routes)/map/page.tsx | 73 ++ frontend/app/globals.css | 208 ++++-- frontend/app/layout.tsx | 46 +- .../app/map/components/analytics-panel.tsx | 143 ---- frontend/app/map/components/chat-bot.tsx | 109 --- frontend/app/map/components/chat-overlay.tsx | 78 -- .../app/map/components/filters-overlay.tsx | 142 ---- frontend/app/map/components/map-container.tsx | 45 -- frontend/app/map/components/map-sidebar.tsx | 127 ---- .../app/map/components/overlay-context.tsx | 94 --- .../app/map/components/overlay-controls.tsx | 79 -- .../app/map/components/overlay-manager.tsx | 59 -- .../overlay-system/overlay-dock.tsx | 50 -- .../map/components/overlay-system/overlay.tsx | 156 ---- frontend/app/map/layout.tsx | 10 - frontend/app/map/page.tsx | 71 -- .../components/feature-importance-chart.tsx | 65 -- .../components/price-comparison-chart.tsx | 75 -- frontend/app/model-explanation/page.tsx | 640 ---------------- frontend/app/page.tsx | 121 +-- frontend/components.json | 9 +- frontend/components/common/PageLayout.tsx | 29 + .../ThemeController.tsx} | 105 ++- frontend/components/common/ThemeProvider.tsx | 23 + .../ThemeToggle.tsx} | 28 +- frontend/components/theme-provider.tsx | 12 - frontend/eslint.config.mjs | 58 +- frontend/features/map/api/mapApi.ts | 65 ++ .../map/components/analytics-overlay.tsx | 36 +- .../map/components/analytics-panel.tsx | 10 + .../map/components/area-chart.tsx | 46 +- frontend/features/map/components/chat-bot.tsx | 8 + .../map/components/filters-overlay.tsx} | 139 ++-- .../features/map/components/map-container.tsx | 75 ++ .../map/components/map-header.tsx | 30 +- .../features/map/components/map-sidebar.tsx | 144 ++++ .../overlay-system/overlay-context.tsx | 119 +-- .../overlay-system/overlay-dock.tsx | 67 ++ .../map/components/overlay-system/overlay.tsx | 219 ++++++ .../map/components/property-filters.tsx | 8 + .../features/map/hooks/useMapInteractions.ts | 38 + frontend/features/map/types/index.ts | 41 + frontend/features/map/utils/mapHelpers.ts | 26 + .../model-explanation/api/explanationApi.ts | 51 ++ .../components/feature-importance-chart.tsx | 71 ++ .../components/price-comparison-chart.tsx | 94 +++ .../features/model-explanation/types/index.ts | 50 ++ frontend/hooks/use-mobile.tsx | 42 +- frontend/hooks/use-toast.ts | 147 ++-- frontend/lib/utils.ts | 14 +- frontend/next.config.ts | 17 +- frontend/package.json | 3 +- frontend/pnpm-lock.yaml | 27 + frontend/services/apiClient.ts | 106 +++ frontend/store/mapStore.ts | 43 ++ frontend/tailwind.config.ts | 38 +- frontend/tsconfig.json | 47 +- frontend/types/api.ts | 41 + frontend/types/index.ts | 12 + 62 files changed, 2865 insertions(+), 2510 deletions(-) create mode 100644 frontend/app/(routes)/map-explanation/page.tsx create mode 100644 frontend/app/(routes)/map/layout.tsx create mode 100644 frontend/app/(routes)/map/page.tsx delete mode 100644 frontend/app/map/components/analytics-panel.tsx delete mode 100644 frontend/app/map/components/chat-bot.tsx delete mode 100644 frontend/app/map/components/chat-overlay.tsx delete mode 100644 frontend/app/map/components/filters-overlay.tsx delete mode 100644 frontend/app/map/components/map-container.tsx delete mode 100644 frontend/app/map/components/map-sidebar.tsx delete mode 100644 frontend/app/map/components/overlay-context.tsx delete mode 100644 frontend/app/map/components/overlay-controls.tsx delete mode 100644 frontend/app/map/components/overlay-manager.tsx delete mode 100644 frontend/app/map/components/overlay-system/overlay-dock.tsx delete mode 100644 frontend/app/map/components/overlay-system/overlay.tsx delete mode 100644 frontend/app/map/layout.tsx delete mode 100644 frontend/app/map/page.tsx delete mode 100644 frontend/app/model-explanation/components/feature-importance-chart.tsx delete mode 100644 frontend/app/model-explanation/components/price-comparison-chart.tsx delete mode 100644 frontend/app/model-explanation/page.tsx create mode 100644 frontend/components/common/PageLayout.tsx rename frontend/components/{theme-controller.tsx => common/ThemeController.tsx} (65%) create mode 100644 frontend/components/common/ThemeProvider.tsx rename frontend/components/{theme-toggle.tsx => common/ThemeToggle.tsx} (73%) delete mode 100644 frontend/components/theme-provider.tsx create mode 100644 frontend/features/map/api/mapApi.ts rename frontend/{app => features}/map/components/analytics-overlay.tsx (86%) create mode 100644 frontend/features/map/components/analytics-panel.tsx rename frontend/{app => features}/map/components/area-chart.tsx (63%) create mode 100644 frontend/features/map/components/chat-bot.tsx rename frontend/{app/map/components/property-filters.tsx => features/map/components/filters-overlay.tsx} (55%) create mode 100644 frontend/features/map/components/map-container.tsx rename frontend/{app => features}/map/components/map-header.tsx (50%) create mode 100644 frontend/features/map/components/map-sidebar.tsx rename frontend/{app => features}/map/components/overlay-system/overlay-context.tsx (58%) create mode 100644 frontend/features/map/components/overlay-system/overlay-dock.tsx create mode 100644 frontend/features/map/components/overlay-system/overlay.tsx create mode 100644 frontend/features/map/components/property-filters.tsx create mode 100644 frontend/features/map/hooks/useMapInteractions.ts create mode 100644 frontend/features/map/types/index.ts create mode 100644 frontend/features/map/utils/mapHelpers.ts create mode 100644 frontend/features/model-explanation/api/explanationApi.ts create mode 100644 frontend/features/model-explanation/components/feature-importance-chart.tsx create mode 100644 frontend/features/model-explanation/components/price-comparison-chart.tsx create mode 100644 frontend/features/model-explanation/types/index.ts create mode 100644 frontend/services/apiClient.ts create mode 100644 frontend/store/mapStore.ts create mode 100644 frontend/types/api.ts create mode 100644 frontend/types/index.ts diff --git a/frontend/README.md b/frontend/README.md index e215bc4..2cc626b 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,36 +1,88 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev +```markdown +frontend/ +├── features/ # <= NEW: Feature-specific modules +│ ├── map/ +│ │ ├── api/ # API calls specific to the map +│ │ │ └── mapApi.ts # (Example) +│ │ ├── components/ # Map-specific UI components +│ │ │ ├── analytics-overlay.tsx +│ │ │ ├── analytics-panel.tsx +│ │ │ ├── area-chart.tsx +│ │ │ ├── chat-bot.tsx +│ │ │ ├── chat-overlay.tsx +│ │ │ ├── filters-overlay.tsx +│ │ │ ├── map-container.tsx +│ │ │ ├── map-header.tsx +│ │ │ ├── map-sidebar.tsx +│ │ │ ├── property-filters.tsx +│ │ │ └── overlay-system/ # Overlay specific components +│ │ │ ├── overlay-context.tsx +│ │ │ ├── overlay-dock.tsx +│ │ │ └── overlay.tsx +│ │ ├── hooks/ # Map-specific hooks +│ │ │ └── useMapInteractions.ts # (Example) +│ │ ├── types/ # Map-related types/interfaces +│ │ │ └── index.ts # (Example) +│ │ └── utils/ # Map-specific utilities +│ │ └── mapHelpers.ts # (Example) +│ └── model-explanation/ +│ ├── components/ # Model Explanation specific components +│ │ ├── feature-importance-chart.tsx +│ │ └── price-comparison-chart.tsx +│ ├── api/ +│ │ └── explanationApi.ts # (Example) +│ └── types/ +│ └── index.ts # (Example) +│ +├── components/ +│ ├── ui/ # Shadcn UI components (KEEP AS IS) +│ │ └── ... (accordion.tsx, button.tsx, etc.) +│ └── common/ # <= NEW: Shared, app-wide components +│ ├── ThemeProvider.tsx # Moved from root components +│ ├── ThemeToggle.tsx # Moved from root components +│ ├── ThemeController.tsx # Moved from root components +│ └── PageLayout.tsx # (Example new shared layout) +│ +├── lib/ # Core utilities, constants, config +│ └── utils.ts # (Keep cn function here) +│ +├── hooks/ # Shared, app-wide hooks +│ ├── use-toast.ts # (Keep useToast here) +│ └── use-mobile.tsx # (Keep useIsMobile here) +│ +├── services/ # <= OPTIONAL: Centralized API layer (alternative to features/\*/api) +│ └── apiClient.ts # (Example API client setup) +│ +├── store/ # <= OPTIONAL: Global state management (e.g., Zustand, Jotai) +│ └── mapStore.ts # (Example store) +│ +├── types/ # <= NEW: Shared, app-wide types/interfaces +│ ├── index.ts # Barrel file for types +│ └── api.ts # Example: General API response types +│ +├── app/ # Next.js app router (routing, layouts, pages) +│ ├── (routes)/ # Keep actual route structure here +│ │ ├── map/ +│ │ │ ├── layout.tsx # Imports common layout, feature components +│ │ │ └── page.tsx # Imports components from 'features/map/components' +│ │ ├── model-explanation/ +│ │ │ └── page.tsx # Imports components from 'features/model-explanation/components' +│ │ └── page.tsx # Root page +│ ├── layout.tsx # Root layout (includes ThemeProvider) +│ ├── globals.css +│ └── favicon.ico +│ +├── public/ # Static assets +│ └── ... +│ +├── .gitignore +├── components.json +├── eslint.config.mjs +├── next-env.d.ts +├── next.config.ts +├── package.json +├── pnpm-lock.yaml +├── postcss.config.mjs +├── tailwind.config.ts +└── tsconfig.json ``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/frontend/app/(routes)/map-explanation/page.tsx b/frontend/app/(routes)/map-explanation/page.tsx new file mode 100644 index 0000000..6512791 --- /dev/null +++ b/frontend/app/(routes)/map-explanation/page.tsx @@ -0,0 +1,703 @@ +/* +======================================== +File: frontend/app/(routes)/model-explanation/page.tsx +======================================== +*/ +"use client"; + +import React, { useState, useEffect } from "react"; +import Link from "next/link"; +import { + ChevronRight, + Info, + ArrowRight, + Home, + Building, + Ruler, + Calendar, + Coins, + Droplets, + Wind, + Sun, + Car, + School, + ShoppingBag, +} from "lucide-react"; + +// Common UI components +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; +import { Slider } from "@/components/ui/slider"; +import { Skeleton } from "@/components/ui/skeleton"; +// Removed SidebarProvider & ThemeProvider - should be in root layout +// Removed MapSidebar - assuming it's not needed here or use a common one + +// Feature-specific components +import { FeatureImportanceChart } from "@/features/model-explanation/components/feature-importance-chart"; +import { PriceComparisonChart } from "@/features/model-explanation/components/price-comparison-chart"; + +// Feature-specific API and types +import { fetchModelExplanation } from "@/features/model-explanation/api/explanationApi"; +import type { ModelExplanationData, PropertyBaseDetails } from "@/features/model-explanation/types"; + +export default function ModelExplanationPage() { + const [activeStep, setActiveStep] = useState(1); + const [isLoading, setIsLoading] = useState(true); + const [explanationData, setExplanationData] = useState(null); + + // State for interactive elements based on fetched data + const [propertySize, setPropertySize] = useState(0); + const [propertyAge, setPropertyAge] = useState(0); + + // Fetch data on mount + useEffect(() => { + async function loadExplanation() { + setIsLoading(true); + // TODO: Get actual property ID from route params or state + const propertyId = "dummy-prop-123"; + const response = await fetchModelExplanation(propertyId); + if (response.success && response.data) { + setExplanationData(response.data); + // Initialize sliders with fetched data + setPropertySize(response.data.propertyDetails.size); + setPropertyAge(response.data.propertyDetails.age); + } else { + console.error("Failed to load model explanation:", response.error); + // Handle error state in UI + } + setIsLoading(false); + } + loadExplanation(); + }, []); + + // Stepper configuration + const steps = [ + { id: 1, title: "Property Details", icon: Home }, + { id: 2, title: "Feature Analysis", icon: Ruler }, + { id: 3, title: "Market Comparison", icon: Building }, + { id: 4, title: "Environmental Factors", icon: Droplets }, + { id: 5, title: "Final Prediction", icon: Coins }, + ]; + + // Calculate adjusted price based on slider interaction + const calculateAdjustedPrice = () => { + if (!explanationData) return 0; + // Simple formula for demonstration - refine with actual logic if possible + const basePrice = explanationData.propertyDetails.predictedPrice; + const baseSize = explanationData.propertyDetails.size; + const baseAge = explanationData.propertyDetails.age; + + const sizeImpact = (propertySize - baseSize) * 50000; // 50,000 THB per sqm diff + const ageImpact = (baseAge - propertyAge) * 200000; // 200,000 THB per year newer + + return basePrice + sizeImpact + ageImpact; + }; + + const adjustedPrice = explanationData ? calculateAdjustedPrice() : 0; + + // Loading State UI + if (isLoading) { + return ( +
+
+ + + + +
+
+ + +
+
+ +
+
+
+
+ ); + } + + // Error State UI + if (!explanationData) { + return ( +
+

Failed to load model explanation.

+ + + +
+ ); + } + + // Main Content UI + const { propertyDetails, features, similarProperties, environmentalFactors, confidence, priceRange } = + explanationData; + + return ( + // Assuming ThemeProvider is in the root layout + // Assuming SidebarProvider and a common sidebar are in root layout or parent layout +
+ {" "} + {/* Adjusted for page content */} + {/* Header */} +
+
+ + Map + + + Price Prediction Model +
+ {/* Add any specific header actions if needed */} +
+ {/* Main content */} +
+ {" "} + {/* Make content area scrollable */} +
+
+

Explainable Price Prediction Model

+

+ Understand how our AI model predicts property prices and what factors influence the valuation. +

+
+ + {/* Steps navigation */} +
+
+ {steps.map((step) => ( + + ))} +
+
+ +
+
+ + {/* Step content */} +
+ {/* --- Left Column: Property Details & Interaction --- */} +
+ {" "} + {/* Make left column sticky */} + + + Property Details + {propertyDetails.address} + + + {/* Dynamically display details */} + + + {propertyDetails.bedrooms && } + {propertyDetails.bathrooms && } + + {propertyDetails.floor && } + + + + + Adjust Parameters + See how changes affect the prediction + + + {/* Size Slider */} +
+
+ + {propertySize} sqm +
+ setPropertySize(value[0])} + /> +
+ {/* Age Slider */} +
+
+ + {propertyAge} years +
+ setPropertyAge(value[0])} + /> +
+
+ +
+
+ Adjusted Price + {formatCurrency(adjustedPrice)} +
+ {/* Show difference */} + {propertyDetails.predictedPrice !== adjustedPrice && ( +
+ {adjustedPrice > propertyDetails.predictedPrice ? "↑" : "↓"} + {Math.abs(adjustedPrice - propertyDetails.predictedPrice).toLocaleString()} THB from original +
+ )} +
+
+
+
+ + {/* --- Right Column: Step Content --- */} +
+ {activeStep === 1 && } + {activeStep === 2 && } + {activeStep === 3 && ( + + )} + {activeStep === 4 && } + {activeStep === 5 && ( + + )} +
+
+
+
+
+ ); +} + +// --- Helper Components for Steps --- + +function DetailRow({ label, value }: { label: string; value: string | number }) { + return ( +
+ {label} + {value} +
+ ); +} + +function formatCurrency(value: number): string { + return new Intl.NumberFormat("th-TH", { + style: "currency", + currency: "THB", + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }).format(value); +} + +// Step 1 Component +function Step1Content({ + propertyDetails, + setActiveStep, +}: { + propertyDetails: PropertyBaseDetails; + setActiveStep: (step: number) => void; +}) { + return ( + + + Property Overview + Basic information used in our prediction model + + +
+

+ Our AI model begins by analyzing the core attributes of your property. These fundamental characteristics + form the baseline for our prediction. +

+
+ + + + {propertyDetails.floor && ( + + )} +
+
+
+ setActiveStep(2)} /> +
+ ); +} + +// Step 2 Component +function Step2Content({ + features, + setActiveStep, +}: { + features: ModelExplanationData["features"]; + setActiveStep: (step: number) => void; +}) { + return ( + + + Feature Analysis + How different features impact the predicted price + + +
+

+ Our model analyzes various features and determines how each contributes to the price prediction. Below is a + breakdown of the most important factors. +

+
+ +
+
+ {features.map((feature) => ( +
+
+ {feature.name} + + {feature.impact === "positive" + ? "↑ Positive" + : feature.impact === "negative" + ? "↓ Negative" + : "→ Neutral"} + +
+
+ + {feature.importance}% +
+

{feature.value}

+
+ ))} +
+
+
+ setActiveStep(1)} onNext={() => setActiveStep(3)} /> +
+ ); +} + +// Step 3 Component +function Step3Content({ + property, + comparisons, + setActiveStep, +}: { + property: PropertyBaseDetails; + comparisons: ModelExplanationData["similarProperties"]; + setActiveStep: (step: number) => void; +}) { + // Prepare data for the chart, ensuring the main property is clearly labeled + const chartProperty = { + name: "Your Property", + price: property.predictedPrice, + size: property.size, + age: property.age, + }; + const chartComparisons = comparisons.map((p, i) => ({ + name: `Comp ${i + 1}`, + address: p.address, + price: p.price, + size: p.size, + age: p.age, + })); + + return ( + + + Market Comparison + + How your property compares to similar properties recently analyzed or sold in the area + + + +
+

+ We analyze recent data from similar properties to establish a baseline. This ensures our prediction aligns + with current market conditions. +

+
+ +
+
+

Similar Properties Details

+
+ {comparisons.map((p, index) => ( +
+
+ {p.address} +
+
+ {p.size} sqm, {p.age} years old +
+
{formatCurrency(p.price)}
+
+ ))} +
+
+
+
+ setActiveStep(2)} onNext={() => setActiveStep(4)} /> +
+ ); +} + +// Step 4 Component +function Step4Content({ + factors, + setActiveStep, +}: { + factors: ModelExplanationData["environmentalFactors"]; + setActiveStep: (step: number) => void; +}) { + const factorDetails = { + floodRisk: { + icon: Droplets, + color: factors.floodRisk === "low" ? "green" : factors.floodRisk === "moderate" ? "yellow" : "red", + text: "Historical data suggests this level of risk.", + }, + airQuality: { + icon: Wind, + color: factors.airQuality === "good" ? "green" : factors.airQuality === "moderate" ? "yellow" : "red", + text: "Compared to city average.", + }, + noiseLevel: { + icon: Sun, + color: factors.noiseLevel === "low" ? "green" : factors.noiseLevel === "moderate" ? "yellow" : "red", + text: "Based on proximity to major roads/sources.", + }, + }; + + return ( + + + Environmental & Location Factors + How surrounding conditions and amenities affect value + + +
+

+ Environmental conditions and nearby amenities significantly impact desirability and value. Our model + considers these external factors. +

+
+ {/* Environmental Factors */} + + + +
+ {/* Proximity Example */} +
+

Proximity to Amenities

+
+ + + + +
+
+
+
+ setActiveStep(3)} onNext={() => setActiveStep(5)} /> +
+ ); +} + +// Step 5 Component +function Step5Content({ + predictedPrice, + confidence, + priceRange, + setActiveStep, +}: { + predictedPrice: number; + confidence: number; + priceRange: { lower: number; upper: number }; + setActiveStep: (step: number) => void; +}) { + return ( + + + Final Prediction + The AI-predicted price based on all analyzed factors + + +
+ {/* Price Box */} +
+

Predicted Price

+
{formatCurrency(predictedPrice)}
+
Confidence Level: {(confidence * 100).toFixed(0)}%
+
+ {/* Price Range Box */} +
+

+ Price Range +

+

+ Based on our model's confidence, the likely market range is: +

+
+
+
Lower Bound
+
{formatCurrency(priceRange.lower)}
+
+
+
Prediction
+
{formatCurrency(predictedPrice)}
+
+
+
Upper Bound
+
{formatCurrency(priceRange.upper)}
+
+
+
+ {/* Summary */} +
+

Summary of Factors

+

This prediction considers:

+
    +
  • Property characteristics (size, age, layout)
  • +
  • Location and neighborhood profile
  • +
  • Recent market trends and comparable sales
  • +
  • Environmental factors and amenity access
  • +
+
+
+
+ + + + + + +
+ ); +} + +// --- Sub-components for Steps --- +function InfoCard({ icon: Icon, title, description }: { icon: React.ElementType; title: string; description: string }) { + return ( +
+ +
+

{title}

+

{description}

+
+
+ ); +} + +function FactorCard({ + title, + factor, + details, +}: { + title: string; + factor: string; + details: { icon: React.ElementType; color: string; text: string }; +}) { + const Icon = details.icon; + const colorClass = `bg-${details.color}-500`; // Requires Tailwind JIT or safelisting + const textColorClass = `text-${details.color}-500`; + + return ( +
+ +

{title}

+
+ {/* Explicit colors might be safer than dynamic Tailwind classes */} +
+ {factor} +
+

{details.text}

+
+ ); +} + +function ProximityItem({ icon: Icon, text }: { icon: React.ElementType; text: string }) { + return ( +
+ +
{text}
+
+ ); +} + +function StepFooter({ onPrev, onNext }: { onPrev?: () => void; onNext?: () => void }) { + return ( + + {onPrev ? ( + + ) : ( +
+ )} + {onNext ? ( + + ) : ( +
+ )} +
+ ); +} diff --git a/frontend/app/(routes)/map/layout.tsx b/frontend/app/(routes)/map/layout.tsx new file mode 100644 index 0000000..95a3c54 --- /dev/null +++ b/frontend/app/(routes)/map/layout.tsx @@ -0,0 +1,21 @@ +/* +======================================== +File: frontend/app/(routes)/map/layout.tsx +======================================== +*/ +import type React from "react"; +// import { PageLayout } from "@/components/common/PageLayout"; // Example using a common layout + +// This layout is specific to the map feature's route group +export default function MapFeatureLayout({ children }: { children: React.ReactNode }) { + return ( + // {/* Example using common layout */} + // The MapSidebar might be rendered here if it's part of the layout +
+ {" "} + {/* Ensure content takes up space */} + {children} +
+ //
+ ); +} diff --git a/frontend/app/(routes)/map/page.tsx b/frontend/app/(routes)/map/page.tsx new file mode 100644 index 0000000..8fa0a97 --- /dev/null +++ b/frontend/app/(routes)/map/page.tsx @@ -0,0 +1,73 @@ +/* +======================================== +File: frontend/app/(routes)/map/page.tsx +======================================== +*/ +"use client"; + +import React, { useState } from "react"; +import Link from "next/link"; +import { ArrowRight } from "lucide-react"; + +// Import common components +import { Button } from "@/components/ui/button"; +// NOTE: ThemeProvider and ThemeController are in the root layout or a higher common layout now + +// Import feature-specific components/contexts/types +import { MapContainer } from "@/features/map/components/map-container"; +// MapSidebar might be part of the layout now, if shared, otherwise import here +// import { MapSidebar } from "@/features/map/components/map-sidebar"; +import { MapHeader } from "@/features/map/components/map-header"; // Map specific header +import { OverlayProvider } from "@/features/map/components/overlay-system/overlay-context"; +import { OverlayDock } from "@/features/map/components/overlay-system/overlay-dock"; +import { AnalyticsOverlay } from "@/features/map/components/analytics-overlay"; +import { FiltersOverlay } from "@/features/map/components/filters-overlay"; +import { ChatOverlay } from "@/features/map/components/chat-overlay"; +import type { MapLocation } from "@/features/map/types"; + +export default function MapPage() { + const [selectedLocation, setSelectedLocation] = useState({ + lat: 13.7563, + lng: 100.5018, + name: "Bangkok", + }); + + // Main page structure remains similar, but imports are updated + return ( + // ThemeProvider/Controller likely moved to root layout + // SidebarProvider might be moved too, depending on its scope + // Assuming OverlayProvider is specific to this map page context + + {/* The outer div with flex, h-screen etc. should be handled by the layout file or a common PageLayout */} +
+ {" "} + {/* Simplified for page content */} + +
+ + + {/* Prediction model banner */} +
+
+
+

Price Prediction: 15,000,000 ฿

+

Based on our AI model analysis

+
+ + + +
+
+ + {/* Overlay System */} + + + + +
+
+
+ ); +} diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 854c4f6..1186e16 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -1,31 +1,131 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import "tailwindcss" prefix(tw); + +@plugin "tailwindcss-animate"; + +@plugin 'tailwindcss-animate'; + +@custom-variant dark (&:is(.dark *)); + +@theme { + --color-background: hsl(var(--background)); + --color-foreground: hsl(var(--foreground)); + + --color-card: hsl(var(--card)); + --color-card-foreground: hsl(var(--card-foreground)); + + --color-popover: hsl(var(--popover)); + --color-popover-foreground: hsl(var(--popover-foreground)); + + --color-primary: hsl(var(--primary)); + --color-primary-foreground: hsl(var(--primary-foreground)); + + --color-secondary: hsl(var(--secondary)); + --color-secondary-foreground: hsl(var(--secondary-foreground)); + + --color-muted: hsl(var(--muted)); + --color-muted-foreground: hsl(var(--muted-foreground)); + + --color-accent: hsl(var(--accent)); + --color-accent-foreground: hsl(var(--accent-foreground)); + + --color-destructive: hsl(var(--destructive)); + --color-destructive-foreground: hsl(var(--destructive-foreground)); + + --color-border: hsl(var(--border)); + --color-input: hsl(var(--input)); + --color-ring: hsl(var(--ring)); + + --color-chart-1: hsl(var(--chart-1)); + --color-chart-2: hsl(var(--chart-2)); + --color-chart-3: hsl(var(--chart-3)); + --color-chart-4: hsl(var(--chart-4)); + --color-chart-5: hsl(var(--chart-5)); + + --color-sidebar: hsl(var(--sidebar-background)); + --color-sidebar-foreground: hsl(var(--sidebar-foreground)); + --color-sidebar-primary: hsl(var(--sidebar-primary)); + --color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground)); + --color-sidebar-accent: hsl(var(--sidebar-accent)); + --color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground)); + --color-sidebar-border: hsl(var(--sidebar-border)); + --color-sidebar-ring: hsl(var(--sidebar-ring)); + + --radius-lg: var(--radius); + --radius-md: calc(var(--radius) - 2px); + --radius-sm: calc(var(--radius) - 4px); + + --animate-accordion-down: accordion-down 0.2s ease-out; + --animate-accordion-up: accordion-up 0.2s ease-out; + + @keyframes accordion-down { + from { + height: 0; + } + to { + height: var(--radix-accordion-content-height); + } + } + @keyframes accordion-up { + from { + height: var(--radix-accordion-content-height); + } + to { + height: 0; + } + } +} + +/* + The default border color has changed to `currentColor` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. +*/ +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentColor); + } +} + +@layer utilities { + body { + font-family: Arial, Helvetica, sans-serif; + } +} @layer base { :root { --background: 0 0% 100%; - --foreground: 240 10% 3.9%; + --foreground: 0 0% 3.9%; --card: 0 0% 100%; - --card-foreground: 240 10% 3.9%; + --card-foreground: 0 0% 3.9%; --popover: 0 0% 100%; - --popover-foreground: 240 10% 3.9%; - --primary: 221.2 83.2% 53.3%; - --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 221.2 83.2% 53.3%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; --radius: 0.5rem; - - /* Sidebar specific colors */ --sidebar-background: 0 0% 98%; --sidebar-foreground: 240 5.3% 26.1%; --sidebar-primary: 240 5.9% 10%; @@ -34,38 +134,36 @@ --sidebar-accent-foreground: 240 5.9% 10%; --sidebar-border: 220 13% 91%; --sidebar-ring: 217.2 91.2% 59.8%; - - /* Overlay constraints */ - --max-overlay-width: calc(100vw - 32px); - --max-overlay-height: calc(100vh - 32px); } - .dark { - --background: 240 10% 3.9%; + --background: 0 0% 3.9%; --foreground: 0 0% 98%; - --card: 240 10% 3.9%; + --card: 0 0% 3.9%; --card-foreground: 0 0% 98%; - --popover: 240 10% 3.9%; + --popover: 0 0% 3.9%; --popover-foreground: 0 0% 98%; - --primary: 217.2 91.2% 59.8%; - --primary-foreground: 222.2 47.4% 11.2%; - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 224.3 76.3% 48%; - - /* Sidebar specific colors for dark mode */ + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; --sidebar-background: 240 5.9% 10%; --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 0 0% 98%; - --sidebar-primary-foreground: 240 5.9% 10%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; --sidebar-accent: 240 3.7% 15.9%; --sidebar-accent-foreground: 240 4.8% 95.9%; --sidebar-border: 240 3.7% 15.9%; @@ -81,25 +179,3 @@ @apply bg-background text-foreground; } } - -@layer components { - .overlay-container { - @apply absolute z-20; - max-width: var(--max-overlay-width); - max-height: var(--max-overlay-height); - } - - .overlay-container[data-minimized="true"] { - @apply z-10; - } - - .overlay-card { - @apply bg-card/95 backdrop-blur-sm border border-border/50 shadow-lg; - max-width: 100%; - max-height: 100%; - } - - .overlay-minimized { - @apply w-[200px] h-auto; - } -} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 17b2ce8..f745756 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -1,20 +1,46 @@ -import type { Metadata } from 'next' -import './globals.css' +/* +======================================== +File: frontend/app/layout.tsx +======================================== +*/ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; // Example using Inter font +import "./globals.css"; +import { ThemeProvider } from "@/components/common/ThemeProvider"; // Correct path +import { ThemeController } from "@/components/common/ThemeController"; // Correct path +import { Toaster as SonnerToaster } from "@/components/ui/sonner"; // Sonner for notifications +import { Toaster as RadixToaster } from "@/components/ui/toaster"; // Shadcn Toaster (if using useToast hook) + +// Setup font +const inter = Inter({ subsets: ["latin"], variable: "--font-sans" }); // Define CSS variable export const metadata: Metadata = { - title: 'v0 App', - description: 'Created with v0', - generator: 'v0.dev', -} + title: "Borbann - Data Platform", // More specific title + description: "Data integration, analysis, and visualization platform.", +}; export default function RootLayout({ children, }: Readonly<{ - children: React.ReactNode + children: React.ReactNode; }>) { return ( - - {children} + + {" "} + {/* suppressHydrationWarning needed for next-themes */} + + {" "} + {/* Apply font class */} + {/* ThemeProvider should wrap everything for theme context */} + + {/* ThemeController can wrap specific parts or the whole app */} + {/* If placed here, it controls the base theme and color scheme */} + {children} + {/* Include Toaster components for notifications */} + + {/* Include if using the useToast hook */} + + - ) + ); } diff --git a/frontend/app/map/components/analytics-panel.tsx b/frontend/app/map/components/analytics-panel.tsx deleted file mode 100644 index 7dbb51a..0000000 --- a/frontend/app/map/components/analytics-panel.tsx +++ /dev/null @@ -1,143 +0,0 @@ -"use client" - -import { Bot, LineChart, Wind, Droplets, Sparkles, Maximize2, Minimize2, ArrowLeftRight } from "lucide-react" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { AreaChart } from "./area-chart" -import { Button } from "@/components/ui/button" -import { useOverlayContext } from "./overlay-context" -import { ScrollArea } from "@/components/ui/scroll-area" - -interface AnalyticsPanelProps { - onChatClick: () => void -} - -export function AnalyticsPanel({ onChatClick }: AnalyticsPanelProps) { - const { overlays, minimizeOverlay, maximizeOverlay, changePosition } = useOverlayContext() - const isMinimized = overlays.analytics.minimized - const position = overlays.analytics.position - - if (isMinimized) { - return ( - - - Analytics - - - - ) - } - - return ( - - - - - Analytics - -
- - -
-
- - -
-

Information in radius will be analyzed

- - - - - - Area Price History - -
-
10,000,000 Baht
-
-

Overall Price History of this area

-
- - - -
- - - - - - Price Prediction - -
-
15,000,000 Baht
-
-

The estimated price based on various factors.

-
- - - -
- -
- - - - - Flood Factor - -
-
- Moderate -
-
-
- - - - - - Air Factor - -
-
- Bad -
-
-
-
- - - - - - Chat With AI - -

Want to ask specific question?

-
-
-
-
-
-
- ) -} - diff --git a/frontend/app/map/components/chat-bot.tsx b/frontend/app/map/components/chat-bot.tsx deleted file mode 100644 index 4feccea..0000000 --- a/frontend/app/map/components/chat-bot.tsx +++ /dev/null @@ -1,109 +0,0 @@ -"use client" - -import { useState } from "react" -import { Send, X, Minimize2, Maximize2 } from "lucide-react" -import { Button } from "@/components/ui/button" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Input } from "@/components/ui/input" -import { useOverlayContext } from "./overlay-context" -import { ScrollArea } from "@/components/ui/scroll-area" - -interface ChatBotProps { - onClose: () => void -} - -export function ChatBot({ onClose }: ChatBotProps) { - const { overlays, minimizeOverlay, maximizeOverlay } = useOverlayContext() - const isMinimized = overlays.chat.minimized - - const [message, setMessage] = useState("") - const [chatHistory, setChatHistory] = useState([{ role: "bot", content: "Hi! How can I help you today?" }]) - - const handleSendMessage = () => { - if (!message.trim()) return - - // Add user message to chat - setChatHistory([...chatHistory, { role: "user", content: message }]) - - // Simulate bot response (in a real app, this would call an API) - setTimeout(() => { - setChatHistory((prev) => [ - ...prev, - { - role: "bot", - content: "I can provide information about this area. What would you like to know?", - }, - ]) - }, 1000) - - setMessage("") - } - - if (isMinimized) { - return ( - - - ChatBot -
- - -
-
-
- ) - } - - return ( - - - ChatBot -
- - -
-
- - -
- {chatHistory.map((chat, index) => ( -
-
- {chat.content} -
-
- ))} -
-
-
- setMessage(e.target.value)} - placeholder="Type your message..." - className="flex-1" - onKeyDown={(e) => { - if (e.key === "Enter") { - handleSendMessage() - } - }} - /> - -
-
-
- ) -} - diff --git a/frontend/app/map/components/chat-overlay.tsx b/frontend/app/map/components/chat-overlay.tsx deleted file mode 100644 index 2178db7..0000000 --- a/frontend/app/map/components/chat-overlay.tsx +++ /dev/null @@ -1,78 +0,0 @@ -"use client" - -import { useState } from "react" -import { Send, MessageCircle } from "lucide-react" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Overlay } from "./overlay-system/overlay" - -export function ChatOverlay() { - const [message, setMessage] = useState("") - const [chatHistory, setChatHistory] = useState([{ role: "bot", content: "Hi! How can I help you today?" }]) - - const handleSendMessage = () => { - if (!message.trim()) return - - // Add user message to chat - setChatHistory([...chatHistory, { role: "user", content: message }]) - - // Simulate bot response (in a real app, this would call an API) - setTimeout(() => { - setChatHistory((prev) => [ - ...prev, - { - role: "bot", - content: "I can provide information about this area. What would you like to know?", - }, - ]) - }, 1000) - - setMessage("") - } - - return ( - } - initialPosition="bottom-right" - initialIsOpen={false} - width="400px" - > -
-
-
- {chatHistory.map((chat, index) => ( -
-
- {chat.content} -
-
- ))} -
-
-
- setMessage(e.target.value)} - placeholder="Type your message..." - className="flex-1" - onKeyDown={(e) => { - if (e.key === "Enter") { - handleSendMessage() - } - }} - /> - -
-
-
- ) -} - diff --git a/frontend/app/map/components/filters-overlay.tsx b/frontend/app/map/components/filters-overlay.tsx deleted file mode 100644 index 852de42..0000000 --- a/frontend/app/map/components/filters-overlay.tsx +++ /dev/null @@ -1,142 +0,0 @@ -"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 { Overlay } from "./overlay-system/overlay" - -export function FiltersOverlay() { - const [area, setArea] = useState("< 30 km") - const [timePeriod, setTimePeriod] = useState("All Time") - const [propertyType, setPropertyType] = useState("House") - const [priceRange, setPriceRange] = useState([5000000, 20000000]) - const [activeTab, setActiveTab] = useState("basic") - - return ( - } - initialPosition="bottom-left" - initialIsOpen={true} - width="350px" - > -
- - - Basic - Advanced - - - -
-
- - -
-
- - -
-
-
- - -
-
- - -
-
-
- - - {new Intl.NumberFormat("th-TH", { - style: "currency", - currency: "THB", - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }).format(priceRange[0])}{" "} - -{" "} - {new Intl.NumberFormat("th-TH", { - style: "currency", - currency: "THB", - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }).format(priceRange[1])} - -
- -
- -
- -
-
- - -
-
- - -
-
- - -
-
-
-
-
-
- - -
-
- ) -} - diff --git a/frontend/app/map/components/map-container.tsx b/frontend/app/map/components/map-container.tsx deleted file mode 100644 index 593a0bd..0000000 --- a/frontend/app/map/components/map-container.tsx +++ /dev/null @@ -1,45 +0,0 @@ -"use client" - -import { useEffect, useRef } from "react" - -interface MapContainerProps { - selectedLocation: { - lat: number - lng: number - name?: string - } -} - -export function MapContainer({ selectedLocation }: MapContainerProps) { - const mapRef = useRef(null) - - useEffect(() => { - // This is a placeholder for actual map integration - // In a real application, you would use a library like Google Maps, Mapbox, or Leaflet - const mapElement = mapRef.current - - if (mapElement) { - // Simulate map loading with a background image - mapElement.style.backgroundImage = "url('/placeholder.svg?height=800&width=1200')" - mapElement.style.backgroundSize = "cover" - mapElement.style.backgroundPosition = "center" - } - - // Clean up function - return () => { - if (mapElement) { - mapElement.style.backgroundImage = "" - } - } - }, [selectedLocation]) - - return ( -
- {/* Map markers would be rendered here in a real implementation */} -
-
-
-
- ) -} - diff --git a/frontend/app/map/components/map-sidebar.tsx b/frontend/app/map/components/map-sidebar.tsx deleted file mode 100644 index 7e938c0..0000000 --- a/frontend/app/map/components/map-sidebar.tsx +++ /dev/null @@ -1,127 +0,0 @@ -"use client" - -import type React from "react" - -import { - Home, - Clock, - Map, - FileText, - Settings, - PenTool, - BarChart3, - Plane, - LineChart, - DollarSign, - MoreHorizontal, -} from "lucide-react" -import Link from "next/link" -import { cn } from "@/lib/utils" -import { usePathname } from "next/navigation" - -export function MapSidebar() { - const pathname = usePathname() - - const mainNavItems = [ - { name: "Home", icon: Home, href: "/" }, - { name: "My assets", icon: Clock, href: "/assets" }, - { name: "Models", icon: Map, href: "/models" }, - { name: "Trade", icon: LineChart, href: "/trade" }, - { name: "Earn", icon: DollarSign, href: "/earn" }, - { name: "Documentation", icon: FileText, href: "/documentation", badge: "NEW" }, - { name: "Pay", icon: Settings, href: "/pay" }, - { name: "More", icon: MoreHorizontal, href: "/more" }, - ] - - const projectNavItems = [ - { name: "Design Engineering", icon: PenTool, href: "/projects/design" }, - { name: "Sales & Marketing", icon: BarChart3, href: "/projects/sales" }, - { name: "Travel", icon: Plane, href: "/projects/travel" }, - ] - - return ( - - ) -} - -function Gift(props: React.SVGProps) { - return ( - - - - - - - - ) -} - diff --git a/frontend/app/map/components/overlay-context.tsx b/frontend/app/map/components/overlay-context.tsx deleted file mode 100644 index 3383b70..0000000 --- a/frontend/app/map/components/overlay-context.tsx +++ /dev/null @@ -1,94 +0,0 @@ -"use client" - -import { createContext, useContext, useState, type ReactNode } from "react" - -type OverlayType = "analytics" | "filters" | "chat" -type OverlayPosition = "left" | "right" - -interface OverlayState { - visible: boolean - minimized: boolean - position: OverlayPosition -} - -interface OverlayContextType { - overlays: Record - toggleOverlay: (type: OverlayType) => void - minimizeOverlay: (type: OverlayType) => void - maximizeOverlay: (type: OverlayType) => void - changePosition: (type: OverlayType, position: OverlayPosition) => void -} - -const OverlayContext = createContext(undefined) - -export function OverlayProvider({ children }: { children: ReactNode }) { - const [overlays, setOverlays] = useState>({ - analytics: { visible: true, minimized: false, position: "right" }, - filters: { visible: true, minimized: false, position: "left" }, - chat: { visible: false, minimized: false, position: "right" }, - }) - - const toggleOverlay = (type: OverlayType) => { - setOverlays((prev) => ({ - ...prev, - [type]: { - ...prev[type], - visible: !prev[type].visible, - minimized: false, - }, - })) - } - - const minimizeOverlay = (type: OverlayType) => { - setOverlays((prev) => ({ - ...prev, - [type]: { - ...prev[type], - minimized: true, - }, - })) - } - - const maximizeOverlay = (type: OverlayType) => { - setOverlays((prev) => ({ - ...prev, - [type]: { - ...prev[type], - minimized: false, - }, - })) - } - - const changePosition = (type: OverlayType, position: OverlayPosition) => { - setOverlays((prev) => ({ - ...prev, - [type]: { - ...prev[type], - position, - }, - })) - } - - return ( - - {children} - - ) -} - -export function useOverlayContext() { - const context = useContext(OverlayContext) - if (context === undefined) { - throw new Error("useOverlayContext must be used within an OverlayProvider") - } - return context -} - diff --git a/frontend/app/map/components/overlay-controls.tsx b/frontend/app/map/components/overlay-controls.tsx deleted file mode 100644 index 2d546ec..0000000 --- a/frontend/app/map/components/overlay-controls.tsx +++ /dev/null @@ -1,79 +0,0 @@ -"use client" - -import { MessageCircle, Filter, Layers, ArrowLeftRight } from "lucide-react" -import { Button } from "@/components/ui/button" -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" -import { useOverlayContext } from "./overlay-context" - -export function OverlayControls() { - const { overlays, toggleOverlay, changePosition } = useOverlayContext() - - return ( - -
- - - - - - {overlays.analytics.visible ? "Hide analytics" : "Show analytics"} - - - - - - - - {overlays.filters.visible ? "Hide filters" : "Show filters"} - - - - - - - - Move analytics to the {overlays.analytics.position === "right" ? "left" : "right"} - - - - {!overlays.chat.visible && ( - - - - - Open chat - - )} -
-
- ) -} - diff --git a/frontend/app/map/components/overlay-manager.tsx b/frontend/app/map/components/overlay-manager.tsx deleted file mode 100644 index 5f96e22..0000000 --- a/frontend/app/map/components/overlay-manager.tsx +++ /dev/null @@ -1,59 +0,0 @@ -"use client" - -import { useOverlayContext } from "./overlay-context" -import { AnalyticsPanel } from "./analytics-panel" -import { PropertyFilters } from "./property-filters" -import { ChatBot } from "./chat-bot" -import { OverlayControls } from "./overlay-controls" - -export function OverlayManager() { - const { overlays, toggleOverlay } = useOverlayContext() - - // Function to ensure overlays stay within viewport bounds - const getPositionClasses = (position: string, type: string) => { - if (position === "right") { - return "right-4" - } else if (position === "left") { - return "left-4" - } - return type === "analytics" ? "right-4" : "left-4" - } - - return ( - <> - {/* Analytics Panel */} - {overlays.analytics.visible && ( -
- toggleOverlay("chat")} /> -
- )} - - {/* Property Filters */} - {overlays.filters.visible && ( -
- -
- )} - - {/* Chat Bot */} - {overlays.chat.visible && ( -
- toggleOverlay("chat")} /> -
- )} - - {/* Overlay Controls */} - - - ) -} - diff --git a/frontend/app/map/components/overlay-system/overlay-dock.tsx b/frontend/app/map/components/overlay-system/overlay-dock.tsx deleted file mode 100644 index 281862d..0000000 --- a/frontend/app/map/components/overlay-system/overlay-dock.tsx +++ /dev/null @@ -1,50 +0,0 @@ -"use client" -import { Button } from "@/components/ui/button" -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" -import { useOverlay } from "./overlay-context" - -interface OverlayDockProps { - position?: "bottom" | "right" - className?: string -} - -export function OverlayDock({ position = "bottom", className }: OverlayDockProps) { - const { overlays, toggleOverlay } = useOverlay() - - // Filter overlays that have icons - const overlaysWithIcons = Object.values(overlays).filter((overlay) => overlay.icon) - - if (overlaysWithIcons.length === 0) return null - - 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", - } - - return ( - -
- {overlaysWithIcons.map((overlay) => ( -
- - - - - - {overlay.isOpen ? `Hide ${overlay.title}` : `Show ${overlay.title}`} - - -
- ))} -
-
- ) -} - diff --git a/frontend/app/map/components/overlay-system/overlay.tsx b/frontend/app/map/components/overlay-system/overlay.tsx deleted file mode 100644 index 80ee07e..0000000 --- a/frontend/app/map/components/overlay-system/overlay.tsx +++ /dev/null @@ -1,156 +0,0 @@ -"use client" - -import type React from "react" -import { useEffect, useState, useRef } 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 - maxHeight?: string -} - -export function Overlay({ - id, - title, - icon, - initialPosition = "bottom-right", - initialIsOpen = false, - className, - children, - onClose, - showMinimize = true, - width = "350px", - height = "auto", - maxHeight = "80vh", -}: OverlayProps) { - const { overlays, registerOverlay, unregisterOverlay, closeOverlay, minimizeOverlay, maximizeOverlay, bringToFront } = - useOverlay() - - const [isDragging, setIsDragging] = useState(false) - const overlayRef = useRef(null) - - // Register overlay on mount - useEffect(() => { - registerOverlay(id, { - title, - icon, - position: initialPosition, - isOpen: initialIsOpen, - }) - - // Unregister on unmount - return () => unregisterOverlay(id) - }, [id]) - - // Get overlay state - const overlay = overlays[id] - if (!overlay) return null - - const handleClose = () => { - closeOverlay(id) - if (onClose) onClose() - } - - const handleMinimize = () => { - minimizeOverlay(id) - } - - const handleMaximize = () => { - maximizeOverlay(id) - } - - const handleHeaderClick = () => { - if (!isDragging) { - bringToFront(id) - } - } - - // Position classes based on position - 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", - } - - // If not open, don't render - if (!overlay.isOpen) return null - - // Render minimized state - if (overlay.isMinimized) { - return ( -
- - - - {icon && {icon}} - {title} - -
- - -
-
-
-
- ) - } - - // Render full overlay - return ( -
- - - - {icon && {icon}} - {title} - - -
- {showMinimize && ( - - )} - -
-
- {children} -
-
- ) -} - diff --git a/frontend/app/map/layout.tsx b/frontend/app/map/layout.tsx deleted file mode 100644 index 84e2591..0000000 --- a/frontend/app/map/layout.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import type React from "react" - -export default function MapLayout({ children }: { children: React.ReactNode }) { - return ( -
- {children} -
- ) -} - diff --git a/frontend/app/map/page.tsx b/frontend/app/map/page.tsx deleted file mode 100644 index 97ea8f9..0000000 --- a/frontend/app/map/page.tsx +++ /dev/null @@ -1,71 +0,0 @@ -"use client" - -import { useState } from "react" -import { MapContainer } from "./components/map-container" -import { MapSidebar } from "./components/map-sidebar" -import { MapHeader } from "./components/map-header" -import { SidebarProvider } from "@/components/ui/sidebar" -import { ThemeProvider } from "@/components/theme-provider" -import { Button } from "@/components/ui/button" -import { ArrowRight } from "lucide-react" -import Link from "next/link" -import { OverlayProvider } from "./components/overlay-system/overlay-context" -import { OverlayDock } from "./components/overlay-system/overlay-dock" -import { AnalyticsOverlay } from "./components/analytics-overlay" -import { FiltersOverlay } from "./components/filters-overlay" -import { ChatOverlay } from "./components/chat-overlay" -import { ThemeController } from "@/components/theme-controller" - -export default function MapPage() { - const [selectedLocation, setSelectedLocation] = useState<{ - lat: number - lng: number - name?: string - }>({ - lat: 13.7563, - lng: 100.5018, - name: "Bangkok", - }) - - return ( - - - - -
- -
- -
- - - {/* Prediction model banner */} -
-
-
-

Price Prediction: 15,000,000 ฿

-

Based on our AI model analysis

-
- - - -
-
- - {/* Overlay System */} - - - - -
-
-
-
-
-
-
- ) -} - diff --git a/frontend/app/model-explanation/components/feature-importance-chart.tsx b/frontend/app/model-explanation/components/feature-importance-chart.tsx deleted file mode 100644 index 3496371..0000000 --- a/frontend/app/model-explanation/components/feature-importance-chart.tsx +++ /dev/null @@ -1,65 +0,0 @@ -"use client" - -import { useTheme } from "next-themes" -import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell } from "@/components/ui/chart" - -interface Feature { - name: string - importance: number - value: string - impact: "positive" | "negative" | "neutral" -} - -interface FeatureImportanceChartProps { - features: Feature[] -} - -export function FeatureImportanceChart({ features }: FeatureImportanceChartProps) { - const { theme } = useTheme() - const isDark = theme === "dark" - - // Sort features by importance - const sortedFeatures = [...features].sort((a, b) => b.importance - a.importance) - - const getBarColor = (impact: string) => { - if (impact === "positive") return "#10b981" - if (impact === "negative") return "#ef4444" - return "#f59e0b" - } - - return ( - - - - `${value}%`} - stroke={isDark ? "#9ca3af" : "#6b7280"} - /> - - [`${value}%`, "Importance"]} - 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/app/model-explanation/components/price-comparison-chart.tsx b/frontend/app/model-explanation/components/price-comparison-chart.tsx deleted file mode 100644 index f5b4bdd..0000000 --- a/frontend/app/model-explanation/components/price-comparison-chart.tsx +++ /dev/null @@ -1,75 +0,0 @@ -"use client" - -import { useTheme } from "next-themes" -import { - BarChart, - Bar, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ResponsiveContainer, - Legend, - Cell, -} from "@/components/ui/chart" - -interface PropertyData { - name: string - price: number - size: number - age: number -} - -interface PriceComparisonChartProps { - property: PropertyData - comparisons: PropertyData[] -} - -export function PriceComparisonChart({ property, comparisons }: PriceComparisonChartProps) { - const { theme } = useTheme() - const isDark = theme === "dark" - - // Combine property and comparisons - const data = [property, ...comparisons] - - // Format the price for display - const formatPrice = (value: number) => { - return new Intl.NumberFormat("th-TH", { - style: "currency", - currency: "THB", - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }).format(value) - } - - return ( - - - - - `${(value / 1000000).toFixed(1)}M`} stroke={isDark ? "#9ca3af" : "#6b7280"} /> - [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", - }} - /> - - - {data.map((entry, index) => ( - - ))} - - - - ) -} - diff --git a/frontend/app/model-explanation/page.tsx b/frontend/app/model-explanation/page.tsx deleted file mode 100644 index 471b06b..0000000 --- a/frontend/app/model-explanation/page.tsx +++ /dev/null @@ -1,640 +0,0 @@ -"use client" - -import { useState } from "react" -import { - ChevronRight, - Info, - ArrowRight, - Home, - Building, - Ruler, - Calendar, - Coins, - Droplets, - Wind, - Sun, - Car, - School, - ShoppingBag, -} from "lucide-react" -import Link from "next/link" -import { Button } from "@/components/ui/button" -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" -import { Progress } from "@/components/ui/progress" -import { Slider } from "@/components/ui/slider" -import { FeatureImportanceChart } from "./components/feature-importance-chart" -import { PriceComparisonChart } from "./components/price-comparison-chart" -import { MapSidebar } from "../map/components/map-sidebar" -import { SidebarProvider } from "@/components/ui/sidebar" -import { ThemeProvider } from "@/components/theme-provider" - -export default function ModelExplanationPage() { - const [activeStep, setActiveStep] = useState(1) - const [propertySize, setPropertySize] = useState(150) - const [propertyAge, setPropertyAge] = useState(5) - - // Sample data for the model explanation - const propertyDetails = { - address: "123 Sukhumvit Road, Bangkok", - type: "Condominium", - size: 150, // sqm - bedrooms: 3, - bathrooms: 2, - age: 5, // years - floor: 15, - amenities: ["Swimming Pool", "Gym", "Security", "Parking"], - predictedPrice: 15000000, // THB - similarProperties: [ - { address: "125 Sukhumvit Road", price: 14500000, size: 145, age: 6 }, - { address: "130 Sukhumvit Road", price: 16200000, size: 160, age: 3 }, - { address: "118 Sukhumvit Road", price: 13800000, size: 140, age: 7 }, - ], - features: [ - { name: "Location", importance: 35, value: "Prime Area", impact: "positive" }, - { name: "Size", importance: 25, value: "150 sqm", impact: "positive" }, - { name: "Age", importance: 15, value: "5 years", impact: "neutral" }, - { name: "Amenities", importance: 10, value: "4 amenities", impact: "positive" }, - { name: "Floor", importance: 8, value: "15th floor", impact: "positive" }, - { name: "Environmental Factors", importance: 7, value: "Low flood risk", impact: "positive" }, - ], - } - - const steps = [ - { - id: 1, - title: "Property Details", - description: "Basic information about the property", - icon: Home, - }, - { - id: 2, - title: "Feature Analysis", - description: "How each feature affects the price", - icon: Ruler, - }, - { - id: 3, - title: "Market Comparison", - description: "Comparison with similar properties", - icon: Building, - }, - { - id: 4, - title: "Environmental Factors", - description: "Impact of environmental conditions", - icon: Droplets, - }, - { - id: 5, - title: "Final Prediction", - description: "The predicted price and confidence level", - icon: Coins, - }, - ] - - // Calculate a new price based on slider changes - const calculateAdjustedPrice = () => { - // Simple formula for demonstration - const sizeImpact = (propertySize - 150) * 50000 // 50,000 THB per sqm - const ageImpact = (5 - propertyAge) * 200000 // 200,000 THB per year newer - - return propertyDetails.predictedPrice + sizeImpact + ageImpact - } - - const adjustedPrice = calculateAdjustedPrice() - - return ( - - -
- -
- {/* Header */} -
-
- - Map - - - Price Prediction Model -
-
- - {/* Main content */} -
-
-
-

Explainable Price Prediction Model

-

- Understand how our AI model predicts property prices and what factors influence the valuation. -

-
- - {/* Steps navigation */} -
-
- {steps.map((step) => ( - - ))} -
-
- -
-
- - {/* Step content */} -
- {/* Left column - Property details */} -
- - - Property Details - {propertyDetails.address} - - -
- Type - {propertyDetails.type} -
-
- Size - {propertySize} sqm -
-
- Bedrooms - {propertyDetails.bedrooms} -
-
- Bathrooms - {propertyDetails.bathrooms} -
-
- Age - {propertyAge} years -
-
- Floor - {propertyDetails.floor} -
-
-
- - - - Adjust Parameters - See how changes affect the prediction - - -
-
- - {propertySize} sqm -
- setPropertySize(value[0])} - /> -
- -
-
- - {propertyAge} years -
- setPropertyAge(value[0])} - /> -
-
- -
-
- Adjusted Price - - {new Intl.NumberFormat("th-TH", { - style: "currency", - currency: "THB", - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }).format(adjustedPrice)} - -
-
- {adjustedPrice > propertyDetails.predictedPrice ? "↑" : "↓"} - {Math.abs(adjustedPrice - propertyDetails.predictedPrice).toLocaleString()} THB from - original prediction -
-
-
-
-
- - {/* Middle column - Step content */} -
- {activeStep === 1 && ( - - - Property Overview - Basic information used in our prediction model - - -
-

- Our AI model begins by analyzing the core attributes of your property. These fundamental - characteristics form the baseline for our prediction. -

- -
-
- -
-

Property Type

-

- {propertyDetails.type} properties in this area have specific market dynamics -

-
-
- -
- -
-

Size & Layout

-

- {propertyDetails.size} sqm with {propertyDetails.bedrooms} bedrooms and{" "} - {propertyDetails.bathrooms} bathrooms -

-
-
- -
- -
-

Property Age

-

- Built {propertyDetails.age} years ago, affecting depreciation calculations -

-
-
- -
- -
-

Floor & View

-

- Located on floor {propertyDetails.floor}, impacting value and desirability -

-
-
-
-
-
- - - -
- )} - - {activeStep === 2 && ( - - - Feature Analysis - How different features impact the predicted price - - -
-

- Our model analyzes various features of your property and determines how each one - contributes to the final price prediction. Below is a breakdown of the most important - factors. -

- -
- -
- -
- {propertyDetails.features.map((feature) => ( -
-
- {feature.name} - - {feature.impact === "positive" - ? "↑ Positive" - : feature.impact === "negative" - ? "↓ Negative" - : "→ Neutral"}{" "} - Impact - -
-
- - {feature.importance}% -
-

{feature.value}

-
- ))} -
-
-
- - - - -
- )} - - {activeStep === 3 && ( - - - Market Comparison - - How your property compares to similar properties in the area - - - -
-

- Our model analyzes recent sales data from similar properties in your area to establish a - baseline for comparison. This helps ensure our prediction is aligned with current market - conditions. -

- -
- ({ - name: p.address.split(" ")[0], - price: p.price, - size: p.size, - age: p.age, - }))} - /> -
- -
-

Similar Properties

-
- {propertyDetails.similarProperties.map((property, index) => ( -
-
{property.address}
-
- {property.size} sqm, {property.age} years old -
-
- {new Intl.NumberFormat("th-TH", { - style: "currency", - currency: "THB", - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }).format(property.price)} -
-
- ))} -
-
-
-
- - - - -
- )} - - {activeStep === 4 && ( - - - Environmental Factors - How environmental conditions affect the property value - - -
-

- Environmental factors can significantly impact property values. Our model considers - various environmental conditions to provide a more accurate prediction. -

- -
-
- -

Flood Risk

-
-
- Moderate -
-

- Historical data shows moderate flood risk in this area -

-
- -
- -

Air Quality

-
-
- Poor -
-

- Air quality is below average, affecting property value -

-
- -
- -

Noise Level

-
-
- Low -
-

- The area has relatively low noise pollution -

-
-
- -
-

Proximity to Amenities

-
-
- -
Public Transport: 300m
-
-
- -
Schools: 1.2km
-
-
- -
Shopping: 500m
-
-
- -
Hospitals: 2.5km
-
-
-
-
-
- - - - -
- )} - - {activeStep === 5 && ( - - - Final Prediction - The predicted price and confidence level - - -
-
-

Predicted Price

-
- {new Intl.NumberFormat("th-TH", { - style: "currency", - currency: "THB", - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }).format(adjustedPrice)} -
-
Confidence Level: 92%
-
- -
-

- - Price Range -

-

- Based on our model's confidence level, the price could range between: -

-
-
-
Lower Bound
-
- {new Intl.NumberFormat("th-TH", { - style: "currency", - currency: "THB", - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }).format(adjustedPrice * 0.95)} -
-
-
-
Prediction
-
- {new Intl.NumberFormat("th-TH", { - style: "currency", - currency: "THB", - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }).format(adjustedPrice)} -
-
-
-
Upper Bound
-
- {new Intl.NumberFormat("th-TH", { - style: "currency", - currency: "THB", - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }).format(adjustedPrice * 1.05)} -
-
-
-
- -
-

Summary of Factors

-

- The final prediction is based on a combination of all factors analyzed in previous - steps: -

-
    -
  • -
    - Property characteristics (size, age, layout) -
  • -
  • -
    - Location and neighborhood analysis -
  • -
  • -
    - Market trends and comparable properties -
  • -
  • -
    - Environmental factors and amenities -
  • -
-
-
-
- - - - -
- )} -
-
-
-
-
-
-
-
- ) -} - diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 88f0cc9..a7779fd 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,102 +1,35 @@ +/* +======================================== +File: frontend/app/page.tsx +======================================== +*/ import Image from "next/image"; +import Link from "next/link"; // Import Link +import { Button } from "@/components/ui/button"; // Import common UI export default function Home() { return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - app/page.tsx - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
+
+ {/* Replace with your actual logo/branding */} +

Welcome to Borbann

+

Your data integration and analysis platform.

- -
-
); diff --git a/frontend/components.json b/frontend/components.json index 335484f..fda3491 100644 --- a/frontend/components.json +++ b/frontend/components.json @@ -4,7 +4,7 @@ "rsc": true, "tsx": true, "tailwind": { - "config": "", + "config": "tailwind.config.ts", "css": "app/globals.css", "baseColor": "neutral", "cssVariables": true, @@ -15,7 +15,10 @@ "utils": "@/lib/utils", "ui": "@/components/ui", "lib": "@/lib", - "hooks": "@/hooks" + "hooks": "@/hooks", + "features": "@/features", + "types": "@/types", + "services": "@/services" }, "iconLibrary": "lucide" -} \ No newline at end of file +} diff --git a/frontend/components/common/PageLayout.tsx b/frontend/components/common/PageLayout.tsx new file mode 100644 index 0000000..7eeadf9 --- /dev/null +++ b/frontend/components/common/PageLayout.tsx @@ -0,0 +1,29 @@ +/* +======================================== +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/theme-controller.tsx b/frontend/components/common/ThemeController.tsx similarity index 65% rename from frontend/components/theme-controller.tsx rename to frontend/components/common/ThemeController.tsx index 2ffe13d..39fb275 100644 --- a/frontend/components/theme-controller.tsx +++ b/frontend/components/common/ThemeController.tsx @@ -1,9 +1,14 @@ -"use client" +/* +======================================== +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 { 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, @@ -11,70 +16,65 @@ import { DropdownMenuTrigger, DropdownMenuSeparator, DropdownMenuLabel, -} from "@/components/ui/dropdown-menu" -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" +} from "@/components/ui/dropdown-menu"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -// Define available color schemes +// Define available color schemes (these affect CSS variables) const colorSchemes = [ - { name: "Blue", primary: "221.2 83.2% 53.3%" }, + { 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 + children: ReactNode; + defaultColorScheme?: string; } export function ThemeController({ children, defaultColorScheme = "Blue" }: ThemeControllerProps) { - const { setTheme, theme } = useTheme() - const [colorScheme, setColorScheme] = useState(defaultColorScheme) - const [overlayBoundaries, setOverlayBoundaries] = useState({ width: 0, height: 0 }) - const containerRef = useRef(null) + 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 when window resizes + // 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 updateBoundaries = () => { - if (containerRef.current) { - setOverlayBoundaries({ - width: containerRef.current.clientWidth, - height: containerRef.current.clientHeight, - }) - } - } - - // Initial update - updateBoundaries() - - // Add resize listener - window.addEventListener("resize", updateBoundaries) - - // Cleanup - return () => window.removeEventListener("resize", updateBoundaries) - }, []) - - // Apply color scheme - useEffect(() => { - const scheme = colorSchemes.find((s) => s.name === colorScheme) + const scheme = colorSchemes.find((s) => s.name === colorScheme); if (scheme) { - document.documentElement.style.setProperty("--primary", scheme.primary) + 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]) - - // Apply CSS variables for overlay constraints - useEffect(() => { - document.documentElement.style.setProperty("--max-overlay-width", `${overlayBoundaries.width - 32}px`) - document.documentElement.style.setProperty("--max-overlay-height", `${overlayBoundaries.height - 32}px`) - }, [overlayBoundaries]) + }, [colorScheme]); return (
{children} {/* Theme Controller UI */} -
+
+ {" "} + {/* Ensure high z-index */} @@ -83,8 +83,7 @@ export function ThemeController({ children, defaultColorScheme = "Blue" }: Theme @@ -123,8 +122,7 @@ export function ThemeController({ children, defaultColorScheme = "Blue" }: Theme setColorScheme(scheme.name)} - className="flex items-center justify-between" - > + className="flex items-center justify-between">
{scheme.name} @@ -142,6 +140,5 @@ export function ThemeController({ children, defaultColorScheme = "Blue" }: Theme
- ) + ); } - diff --git a/frontend/components/common/ThemeProvider.tsx b/frontend/components/common/ThemeProvider.tsx new file mode 100644 index 0000000..b64248b --- /dev/null +++ b/frontend/components/common/ThemeProvider.tsx @@ -0,0 +1,23 @@ +/* +======================================== +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/theme-toggle.tsx b/frontend/components/common/ThemeToggle.tsx similarity index 73% rename from frontend/components/theme-toggle.tsx rename to frontend/components/common/ThemeToggle.tsx index 47408b2..c1df06b 100644 --- a/frontend/components/theme-toggle.tsx +++ b/frontend/components/common/ThemeToggle.tsx @@ -1,13 +1,24 @@ -"use client" +/* +======================================== +File: frontend/components/common/ThemeToggle.tsx +======================================== +*/ +"use client"; -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" +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() + const { setTheme, theme } = useTheme(); return ( @@ -42,6 +53,5 @@ export function ThemeToggle() {
- ) + ); } - diff --git a/frontend/components/theme-provider.tsx b/frontend/components/theme-provider.tsx deleted file mode 100644 index 77cb61d..0000000 --- a/frontend/components/theme-provider.tsx +++ /dev/null @@ -1,12 +0,0 @@ -"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/eslint.config.mjs b/frontend/eslint.config.mjs index c85fb67..c69b8b9 100644 --- a/frontend/eslint.config.mjs +++ b/frontend/eslint.config.mjs @@ -1,16 +1,72 @@ import { dirname } from "path"; import { fileURLToPath } from "url"; import { FlatCompat } from "@eslint/eslintrc"; +import eslintJs from "@eslint/js"; // Import recommended rules +import tseslint from "typescript-eslint"; // Import TS plugin/parser +import eslintPluginReact from "eslint-plugin-react"; // Import React plugin +import eslintConfigNext from "eslint-config-next"; // Import Next.js specific config const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); +// FlatCompat for extending older eslintrc-style configs if needed (like next/core-web-vitals) const compat = new FlatCompat({ baseDirectory: __dirname, + // resolvePluginsRelativeTo: __dirname, // might be needed depending on setup }); const eslintConfig = [ - ...compat.extends("next/core-web-vitals", "next/typescript"), + // Base ESLint recommended rules + eslintJs.configs.recommended, + + // TypeScript specific rules using the new flat config structure + ...tseslint.configs.recommended, // Or recommendedTypeChecked if using type info + + // React specific rules (new flat config structure) + { + plugins: { + react: eslintPluginReact, + }, + rules: { + ...eslintPluginReact.configs.recommended.rules, + ...eslintPluginReact.configs["jsx-runtime"].rules, // Recommended for React 17+ JSX transform + "react/prop-types": "off", // Not needed with TypeScript + // Add other React specific rules here + }, + settings: { + react: { + version: "detect", // Automatically detect React version + }, + }, + }, + + // Next.js specific rules using FlatCompat for `eslint-config-next` + // This often includes rules for react-hooks, jsx-a11y etc. + ...compat.extends("next/core-web-vitals"), + + // Custom rules or overrides + { + rules: { + // Example: Enforce no console logs in production (modify as needed) + // 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', + "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], // Warn on unused vars except those starting with _ + "@typescript-eslint/no-explicit-any": "warn", // Warn against using 'any' + // Add more custom rules here + }, + // Target specific files if necessary + // files: ["src/**/*.{ts,tsx}"], + }, + + // Ignore files if needed (e.g., generated files) + { + ignores: [ + ".next/", + "node_modules/", + "out/", + "build/", + // Add other ignored paths + ], + }, ]; export default eslintConfig; diff --git a/frontend/features/map/api/mapApi.ts b/frontend/features/map/api/mapApi.ts new file mode 100644 index 0000000..c4e1a72 --- /dev/null +++ b/frontend/features/map/api/mapApi.ts @@ -0,0 +1,65 @@ +/* +======================================== +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/app/map/components/analytics-overlay.tsx b/frontend/features/map/components/analytics-overlay.tsx similarity index 86% rename from frontend/app/map/components/analytics-overlay.tsx rename to frontend/features/map/components/analytics-overlay.tsx index f5abbbc..1e4e57b 100644 --- a/frontend/app/map/components/analytics-overlay.tsx +++ b/frontend/features/map/components/analytics-overlay.tsx @@ -1,17 +1,22 @@ -"use client" +/* +======================================== +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 { useOverlay } from "./overlay-system/overlay-context" +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 { toggleOverlay } = useOverlay(); const handleChatClick = () => { - toggleOverlay("chat") - } + toggleOverlay("chat"); + }; return ( } initialPosition="top-right" initialIsOpen={true} - width="350px" - > + width="350px">

Information in radius will be analyzed

+ {/* Area Price History Card */} @@ -46,6 +51,7 @@ export function AnalyticsOverlay() { + {/* Price Prediction Card */} @@ -65,6 +71,7 @@ export function AnalyticsOverlay() { + {/* Environmental Factors Cards */}
@@ -93,10 +100,10 @@ export function AnalyticsOverlay() {
+ {/* Chat With AI Card */} + onClick={handleChatClick}> @@ -109,6 +116,5 @@ export function AnalyticsOverlay() {
- ) + ); } - diff --git a/frontend/features/map/components/analytics-panel.tsx b/frontend/features/map/components/analytics-panel.tsx new file mode 100644 index 0000000..eab45c5 --- /dev/null +++ b/frontend/features/map/components/analytics-panel.tsx @@ -0,0 +1,10 @@ +/* +======================================== +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/app/map/components/area-chart.tsx b/frontend/features/map/components/area-chart.tsx similarity index 63% rename from frontend/app/map/components/area-chart.tsx rename to frontend/features/map/components/area-chart.tsx index ee04e2a..110a774 100644 --- a/frontend/app/map/components/area-chart.tsx +++ b/frontend/features/map/components/area-chart.tsx @@ -1,38 +1,45 @@ -"use client" +/* +======================================== +File: frontend/features/map/components/area-chart.tsx +======================================== +*/ +"use client"; -import { LineChart, Line, ResponsiveContainer, XAxis, YAxis, Tooltip } from "@/components/ui/chart" -import { useTheme } from "next-themes" +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 + data: number[]; + color: string; } export function AreaChart({ data, color }: AreaChartProps) { - const { theme } = useTheme() - const isDark = theme === "dark" + const { theme } = useTheme(); + const isDark = theme === "dark"; - // Generate labels (months) - const labels = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"] + // 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], + name: labels[index % labels.length] || `Point ${index + 1}`, // Use labels or fallback value: value, - })) + })); - // Format the price for display + // 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) - } + }).format(value); + }; return (
+ {" "} + {/* Adjust height as needed */} @@ -50,16 +57,17 @@ export function AreaChart({ data, color }: AreaChartProps) { , 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 new file mode 100644 index 0000000..15e7563 --- /dev/null +++ b/frontend/features/map/components/chat-bot.tsx @@ -0,0 +1,8 @@ +/* +======================================== +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/app/map/components/property-filters.tsx b/frontend/features/map/components/filters-overlay.tsx similarity index 55% rename from frontend/app/map/components/property-filters.tsx rename to frontend/features/map/components/filters-overlay.tsx index 633e065..ee234f7 100644 --- a/frontend/app/map/components/property-filters.tsx +++ b/frontend/features/map/components/filters-overlay.tsx @@ -1,50 +1,50 @@ -"use client" +/* +======================================== +File: frontend/features/map/components/filters-overlay.tsx +======================================== +*/ +"use client"; -import { useState } from "react" -import { Button } from "@/components/ui/button" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -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 { Minimize2, Maximize2 } from "lucide-react" -import { useOverlayContext } from "./overlay-context" -import { ScrollArea } from "@/components/ui/scroll-area" +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 PropertyFilters() { - const { overlays, minimizeOverlay, maximizeOverlay } = useOverlayContext() - const isMinimized = overlays.filters.minimized +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 [area, setArea] = useState("< 30 km") - const [timePeriod, setTimePeriod] = useState("All Time") - const [propertyType, setPropertyType] = useState("House") - const [priceRange, setPriceRange] = useState([5000000, 20000000]) - const [activeTab, setActiveTab] = useState("basic") - - if (isMinimized) { - return ( - - - Filters - - - - ) - } + 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 ( - - - Property Filters - - - - + } + initialPosition="bottom-left" + initialIsOpen={true} + width="350px"> + + {" "} + {/* Scrollable content */} +
Basic @@ -54,9 +54,11 @@ export function PropertyFilters() {
- +
- + - + @@ -103,34 +109,26 @@ export function PropertyFilters() {
- + - {new Intl.NumberFormat("th-TH", { - style: "currency", - currency: "THB", - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }).format(priceRange[0])}{" "} - -{" "} - {new Intl.NumberFormat("th-TH", { - style: "currency", - currency: "THB", - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }).format(priceRange[1])} + {new Intl.NumberFormat("th-TH", { notation: "compact" }).format(priceRange[0])} -{" "} + {new Intl.NumberFormat("th-TH", { notation: "compact" }).format(priceRange[1])} ฿
- +
- - ) + + ); } - diff --git a/frontend/features/map/components/map-container.tsx b/frontend/features/map/components/map-container.tsx new file mode 100644 index 0000000..6466243 --- /dev/null +++ b/frontend/features/map/components/map-container.tsx @@ -0,0 +1,75 @@ +/* +======================================== +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/app/map/components/map-header.tsx b/frontend/features/map/components/map-header.tsx similarity index 50% rename from frontend/app/map/components/map-header.tsx rename to frontend/features/map/components/map-header.tsx index ab0254c..89d96ee 100644 --- a/frontend/app/map/components/map-header.tsx +++ b/frontend/features/map/components/map-header.tsx @@ -1,32 +1,40 @@ -"use client" +/* +======================================== +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/theme-toggle" +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 ( -
+
+ {/* Breadcrumbs or Title */}
+ {" "} + {/* Example link */} Tools Map
+ {/* Header Actions */}
-
- ) + ); } - diff --git a/frontend/features/map/components/map-sidebar.tsx b/frontend/features/map/components/map-sidebar.tsx new file mode 100644 index 0000000..1b34576 --- /dev/null +++ b/frontend/features/map/components/map-sidebar.tsx @@ -0,0 +1,144 @@ +/* +======================================== +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/app/map/components/overlay-system/overlay-context.tsx b/frontend/features/map/components/overlay-system/overlay-context.tsx similarity index 58% rename from frontend/app/map/components/overlay-system/overlay-context.tsx rename to frontend/features/map/components/overlay-system/overlay-context.tsx index 0503f5a..d024455 100644 --- a/frontend/app/map/components/overlay-system/overlay-context.tsx +++ b/frontend/features/map/components/overlay-system/overlay-context.tsx @@ -1,3 +1,8 @@ +/* +======================================== +File: frontend/features/map/components/overlay-system/overlay-context.tsx +======================================== +*/ "use client"; import React, { createContext, useContext, useState, useCallback, useRef, type ReactNode } from "react"; @@ -20,7 +25,7 @@ export interface OverlayState { // Interface for the overlay context interface OverlayContextType { overlays: Record; - registerOverlay: (id: OverlayId, initialState: Partial) => void; + registerOverlay: (id: OverlayId, initialState: Partial>) => void; unregisterOverlay: (id: OverlayId) => void; openOverlay: (id: OverlayId) => void; closeOverlay: (id: OverlayId) => void; @@ -36,45 +41,52 @@ interface OverlayContextType { const OverlayContext = createContext(undefined); // Default values for overlay state -const defaultOverlayState: Omit = { +const defaultOverlayState: Omit = { isOpen: false, isMinimized: false, - position: "bottom-right", - zIndex: 10, + position: "bottom-right", // Default position + zIndex: 10, // Starting z-index }; export function OverlayProvider({ children }: { children: ReactNode }) { const [overlays, setOverlays] = useState>({}); - const maxZIndexRef = useRef(10); + const maxZIndexRef = useRef(10); // Start z-index from 10 - // Get the next z-index value using a ref so it doesn't trigger re-renders + // Get the next z-index value const getNextZIndex = useCallback(() => { - maxZIndexRef.current++; + maxZIndexRef.current += 1; return maxZIndexRef.current; }, []); // Register a new overlay - const registerOverlay = useCallback((id: OverlayId, initialState: Partial) => { - setOverlays((prev) => { - if (prev[id]) return prev; - return { - ...prev, - [id]: { - ...defaultOverlayState, - id, - title: id, - ...initialState, - }, - }; - }); - }, []); + 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 newOverlays = { ...prev }; - delete newOverlays[id]; - return newOverlays; + const { [id]: _, ...rest } = prev; // Use destructuring to remove the key + return rest; }); }, []); @@ -82,14 +94,14 @@ export function OverlayProvider({ children }: { children: ReactNode }) { const openOverlay = useCallback( (id: OverlayId) => { setOverlays((prev) => { - if (!prev[id]) return prev; + if (!prev[id] || prev[id].isOpen) return prev; return { ...prev, [id]: { ...prev[id], isOpen: true, - isMinimized: false, - zIndex: getNextZIndex(), + isMinimized: false, // Ensure not minimized when opened + zIndex: getNextZIndex(), // Bring to front }, }; }); @@ -100,33 +112,31 @@ export function OverlayProvider({ children }: { children: ReactNode }) { // Close an overlay const closeOverlay = useCallback((id: OverlayId) => { setOverlays((prev) => { - if (!prev[id]) return prev; + if (!prev[id] || !prev[id].isOpen) return prev; return { ...prev, - [id]: { - ...prev[id], - isOpen: false, - }, + [id]: { ...prev[id], isOpen: false }, }; }); }, []); - // Toggle an overlay + // Toggle an overlay's open/closed state const toggleOverlay = useCallback( (id: OverlayId) => { setOverlays((prev) => { - if (!prev[id]) return prev; - const newState = { - ...prev[id], - isOpen: !prev[id].isOpen, - }; - if (newState.isOpen) { - newState.isMinimized = false; - newState.zIndex = getNextZIndex(); - } + 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]: newState, + [id]: { + ...prev[id], + isOpen: willBeOpen, + isMinimized: willBeOpen ? false : prev[id].isMinimized, // Maximize when toggling open + zIndex: newZIndex, + }, }; }); }, @@ -136,12 +146,13 @@ export function OverlayProvider({ children }: { children: ReactNode }) { // Minimize an overlay const minimizeOverlay = useCallback((id: OverlayId) => { setOverlays((prev) => { - if (!prev[id]) return 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 }, }; }); @@ -151,13 +162,13 @@ export function OverlayProvider({ children }: { children: ReactNode }) { const maximizeOverlay = useCallback( (id: OverlayId) => { setOverlays((prev) => { - if (!prev[id]) return 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(), + zIndex: getNextZIndex(), // Bring to front when maximized }, }; }); @@ -171,10 +182,7 @@ export function OverlayProvider({ children }: { children: ReactNode }) { if (!prev[id]) return prev; return { ...prev, - [id]: { - ...prev[id], - position, - }, + [id]: { ...prev[id], position }, }; }); }, []); @@ -183,13 +191,12 @@ export function OverlayProvider({ children }: { children: ReactNode }) { const bringToFront = useCallback( (id: OverlayId) => { setOverlays((prev) => { - if (!prev[id]) return 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(), - }, + [id]: { ...prev[id], zIndex: getNextZIndex() }, }; }); }, @@ -213,6 +220,7 @@ export function OverlayProvider({ children }: { children: ReactNode }) { return {children}; } +// Hook to use the overlay context export function useOverlay() { const context = useContext(OverlayContext); if (context === undefined) { @@ -220,4 +228,3 @@ export function useOverlay() { } return context; } - diff --git a/frontend/features/map/components/overlay-system/overlay-dock.tsx b/frontend/features/map/components/overlay-system/overlay-dock.tsx new file mode 100644 index 0000000..97eebb8 --- /dev/null +++ b/frontend/features/map/components/overlay-system/overlay-dock.tsx @@ -0,0 +1,67 @@ +/* +======================================== +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) => ( +
+ + + + + + {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 new file mode 100644 index 0000000..23748d4 --- /dev/null +++ b/frontend/features/map/components/overlay-system/overlay.tsx @@ -0,0 +1,219 @@ +/* +======================================== +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 new file mode 100644 index 0000000..2c9118c --- /dev/null +++ b/frontend/features/map/components/property-filters.tsx @@ -0,0 +1,8 @@ +/* +======================================== +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 new file mode 100644 index 0000000..71d29b7 --- /dev/null +++ b/frontend/features/map/hooks/useMapInteractions.ts @@ -0,0 +1,38 @@ +/* +======================================== +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 new file mode 100644 index 0000000..eba6684 --- /dev/null +++ b/frontend/features/map/types/index.ts @@ -0,0 +1,41 @@ +/* +======================================== +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 new file mode 100644 index 0000000..c1bca06 --- /dev/null +++ b/frontend/features/map/utils/mapHelpers.ts @@ -0,0 +1,26 @@ +/* +======================================== +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 new file mode 100644 index 0000000..5afefed --- /dev/null +++ b/frontend/features/model-explanation/api/explanationApi.ts @@ -0,0 +1,51 @@ +/* +======================================== +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 new file mode 100644 index 0000000..db845a8 --- /dev/null +++ b/frontend/features/model-explanation/components/feature-importance-chart.tsx @@ -0,0 +1,71 @@ +/* +======================================== +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 new file mode 100644 index 0000000..8771f4d --- /dev/null +++ b/frontend/features/model-explanation/components/price-comparison-chart.tsx @@ -0,0 +1,94 @@ +/* +======================================== +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 new file mode 100644 index 0000000..9dd42cd --- /dev/null +++ b/frontend/features/model-explanation/types/index.ts @@ -0,0 +1,50 @@ +/* +======================================== +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 2b0fe1d..0d6b1fe 100644 --- a/frontend/hooks/use-mobile.tsx +++ b/frontend/hooks/use-mobile.tsx @@ -1,19 +1,37 @@ -import * as React from "react" +/* +======================================== +File: frontend/hooks/use-mobile.tsx +======================================== +*/ +import * as React from "react"; -const MOBILE_BREAKPOINT = 768 +const MOBILE_BREAKPOINT = 768; // Standard md breakpoint -export function useIsMobile() { - const [isMobile, setIsMobile] = React.useState(undefined) +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 + ); React.useEffect(() => { - const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) - const onChange = () => { - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) + // Ensure this runs only client-side + if (typeof window === "undefined") { + return; } - mql.addEventListener("change", onChange) - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) - return () => mql.removeEventListener("change", onChange) - }, []) - return !!isMobile + 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; } diff --git a/frontend/hooks/use-toast.ts b/frontend/hooks/use-toast.ts index 02e111d..eebb31c 100644 --- a/frontend/hooks/use-toast.ts +++ b/frontend/hooks/use-toast.ts @@ -1,106 +1,106 @@ -"use client" +/* +======================================== +File: frontend/hooks/use-toast.ts +======================================== +*/ +"use client"; // Inspired by react-hot-toast library -import * as React from "react" +import * as React from "react"; +// Import types from the actual Toast component location +import type { ToastActionElement, ToastProps } from "@/components/ui/toast"; -import type { - ToastActionElement, - ToastProps, -} from "@/components/ui/toast" - -const TOAST_LIMIT = 1 -const TOAST_REMOVE_DELAY = 1000000 +const TOAST_LIMIT = 1; // Show only one toast at a time +const TOAST_REMOVE_DELAY = 1000000; // A very long time (effectively manual dismiss only) 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 effects ! - This could be extracted into a dismissToast() action, - // but I'll keep it here for simplicity + // Side effect: schedule removal for dismissed toasts 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, + open: false, // Trigger the close animation } : t ), - } + }; } case "REMOVE_TOAST": if (action.toastId === undefined) { return { ...state, - toasts: [], - } + toasts: [], // Remove all toasts + }; } return { ...state, toasts: state.toasts.filter((t) => t.id !== action.toastId), - } + }; } -} +}; -const listeners: Array<(state: State) => void> = [] +const listeners: Array<(state: State) => void> = []; -let memoryState: State = { toasts: [] } +let memoryState: State = { toasts: [] }; // In-memory state function dispatch(action: Action) { - memoryState = reducer(memoryState, action) + memoryState = reducer(memoryState, action); listeners.forEach((listener) => { - listener(memoryState) - }) + listener(memoryState); + }); } -type Toast = Omit +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,36 +159,37 @@ function toast({ ...props }: Toast) { id, open: true, onOpenChange: (open) => { - if (!open) dismiss() + if (!open) dismiss(); // Ensure dismiss is called when the toast closes itself }, }, - }) + }); 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 () => { - const index = listeners.indexOf(setState) + // Clean up listener + const index = listeners.indexOf(setState); if (index > -1) { - listeners.splice(index, 1) + listeners.splice(index, 1); } - } - }, [state]) + }; + }, [state]); // Only re-subscribe if state instance changes (it shouldn't) return { ...state, toast, dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), - } + }; } -export { useToast, toast } +export { useToast, toast }; diff --git a/frontend/lib/utils.ts b/frontend/lib/utils.ts index bd0c391..6138a22 100644 --- a/frontend/lib/utils.ts +++ b/frontend/lib/utils.ts @@ -1,6 +1,14 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" +/* +======================================== +File: frontend/lib/utils.ts +======================================== +*/ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; +/** Utility function to merge Tailwind classes with clsx */ export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); } + +// Add other general utility functions here if needed diff --git a/frontend/next.config.ts b/frontend/next.config.ts index e9ffa30..550ee07 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -1,7 +1,22 @@ import type { NextConfig } from "next"; +/** @type {import('next').NextConfig} */ const nextConfig: NextConfig = { - /* config options here */ + reactStrictMode: true, // Recommended for development + // Add other Next.js configurations here as needed + // Example: environment variables accessible on the client-side + // env: { + // NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, + // }, + // Example: image optimization domains + // images: { + // remotePatterns: [ + // { + // protocol: 'https', + // hostname: 'example.com', + // }, + // ], + // }, }; export default nextConfig; diff --git a/frontend/package.json b/frontend/package.json index 00a6958..8314bf5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -61,7 +61,8 @@ "tailwindcss": "^3.4.17", "tailwindcss-animate": "^1.0.7", "vaul": "^0.9.6", - "zod": "^3.24.1" + "zod": "^3.24.1", + "zustand": "^5.0.3" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 50ab650..87bdff1 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -171,6 +171,9 @@ importers: zod: specifier: ^3.24.1 version: 3.24.2 + zustand: + specifier: ^5.0.3 + version: 5.0.3(@types/react@19.0.10)(react@19.0.0)(use-sync-external-store@1.4.0(react@19.0.0)) devDependencies: '@eslint/eslintrc': specifier: ^3 @@ -2926,6 +2929,24 @@ packages: zod@3.24.2: resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} + zustand@5.0.3: + resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': 19.0.10 + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -5939,3 +5960,9 @@ snapshots: yocto-queue@0.1.0: {} zod@3.24.2: {} + + zustand@5.0.3(@types/react@19.0.10)(react@19.0.0)(use-sync-external-store@1.4.0(react@19.0.0)): + optionalDependencies: + '@types/react': 19.0.10 + react: 19.0.0 + use-sync-external-store: 1.4.0(react@19.0.0) diff --git a/frontend/services/apiClient.ts b/frontend/services/apiClient.ts new file mode 100644 index 0000000..9088254 --- /dev/null +++ b/frontend/services/apiClient.ts @@ -0,0 +1,106 @@ +/* +======================================== +File: frontend/services/apiClient.ts (NEW - Dummy) +======================================== +*/ +import type { APIResponse } from "@/types/api"; // Import shared response type + +// --- 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"; + +/** + * Base function for making API requests. + * Includes dummy authorization header. + */ +async function fetchApi(endpoint: string, options: RequestInit = {}): Promise> { + const url = `${process.env.NEXT_PUBLIC_API_URL || "/api/v1"}${endpoint}`; + + const defaultHeaders: HeadersInit = { + "Content-Type": "application/json", + Authorization: DUMMY_AUTH_TOKEN, // Add dummy token here + }; + + const config: RequestInit = { + ...options, + headers: { + ...defaultHeaders, + ...options.headers, + }, + }; + + console.log(`DUMMY API Client: Requesting ${config.method || "GET"} ${url}`); + + // Simulate network delay + await new Promise((resolve) => setTimeout(resolve, Math.random() * 300 + 100)); + + 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" }; + } + + 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 }; + } + + // 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 --- + } 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 }; + } +} + +// --- Convenience Methods --- +const apiClient = { + get: (endpoint: string, options?: RequestInit) => fetchApi(endpoint, { ...options, method: "GET" }), + + post: (endpoint: string, body: any, options?: RequestInit) => + fetchApi(endpoint, { ...options, method: "POST", body: JSON.stringify(body) }), + + put: (endpoint: string, body: any, options?: RequestInit) => + fetchApi(endpoint, { ...options, method: "PUT", body: JSON.stringify(body) }), + + delete: (endpoint: string, options?: RequestInit) => fetchApi(endpoint, { ...options, method: "DELETE" }), + + patch: (endpoint: string, body: any, options?: RequestInit) => + fetchApi(endpoint, { ...options, method: "PATCH", body: JSON.stringify(body) }), +}; + +export default apiClient; diff --git a/frontend/store/mapStore.ts b/frontend/store/mapStore.ts new file mode 100644 index 0000000..58294b3 --- /dev/null +++ b/frontend/store/mapStore.ts @@ -0,0 +1,43 @@ +/* +======================================== +File: frontend/store/mapStore.ts (NEW - Dummy using Zustand) +======================================== +*/ +import { create } from "zustand"; +import type { MapLocation, MapLayerConfig } from "@/features/map/types"; // Import types + +interface MapState { + currentLocation: MapLocation; + zoomLevel: number; + activeLayers: MapLayerConfig[]; + isLoading: boolean; + setLocation: (location: MapLocation) => void; + setZoom: (zoom: number) => void; + toggleLayer: (layerId: string) => void; + setLoading: (loading: boolean) => void; +} + +export const useMapStore = create((set) => ({ + // Initial State + currentLocation: { lat: 13.7563, lng: 100.5018, name: "Bangkok" }, + zoomLevel: 12, + activeLayers: [ + // Example initial layer + { id: "base-tiles", name: "Base Map", url: "dummy-tile-url", type: "raster", visible: true }, + ], + isLoading: false, + + // Actions + setLocation: (location) => set({ currentLocation: location }), + setZoom: (zoom) => set({ zoomLevel: zoom }), + toggleLayer: (layerId) => + set((state) => ({ + activeLayers: state.activeLayers.map((layer) => + layer.id === layerId ? { ...layer, visible: !layer.visible } : layer + ), + })), + setLoading: (loading) => set({ isLoading: loading }), +})); + +// Usage in a component: +// const { currentLocation, setLocation, isLoading } = useMapStore(); diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts index 0af93fc..28cb5bc 100644 --- a/frontend/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -1,15 +1,17 @@ import type { Config } from "tailwindcss"; +import { fontFamily } from "tailwindcss/defaultTheme"; // Import default theme const config = { - darkMode: ["class"], + darkMode: ["class"], // Use class-based dark mode content: [ - "./pages/**/*.{ts,tsx}", - "./components/**/*.{ts,tsx}", - "./app/**/*.{ts,tsx}", - "./src/**/*.{ts,tsx}", - "*.{js,ts,jsx,tsx,mdx}", + "./app/**/*.{ts,tsx}", // Scan app directory + "./components/**/*.{ts,tsx}", // Scan components directory (common and ui) + "./features/**/*.{ts,tsx}", // <= NEW: Scan features directory + // Remove older paths if no longer relevant + // "./pages/**/*.{ts,tsx}", // Likely remove if using App Router only + // "./src/**/*.{ts,tsx}", // Remove if code is not in src/ ], - prefix: "", + prefix: "", // No prefix theme: { container: { center: true, @@ -19,7 +21,12 @@ const config = { }, }, extend: { + // Add sans-serif font family using CSS variable defined in layout.tsx + fontFamily: { + sans: ["var(--font-sans)", ...fontFamily.sans], + }, colors: { + // Keep Shadcn UI color definitions using CSS variables border: "hsl(var(--border))", input: "hsl(var(--input))", ring: "hsl(var(--ring))", @@ -53,15 +60,16 @@ const config = { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))", }, + // Sidebar specific colors (ensure these variables are defined in globals.css) sidebar: { DEFAULT: "hsl(var(--sidebar-background))", foreground: "hsl(var(--sidebar-foreground))", - primary: "hsl(var(--sidebar-primary))", - "primary-foreground": "hsl(var(--sidebar-primary-foreground))", - accent: "hsl(var(--sidebar-accent))", - "accent-foreground": "hsl(var(--sidebar-accent-foreground))", border: "hsl(var(--sidebar-border))", ring: "hsl(var(--sidebar-ring))", + accent: { + DEFAULT: "hsl(var(--sidebar-accent))", + foreground: "hsl(var(--sidebar-accent-foreground))", + }, }, }, borderRadius: { @@ -78,14 +86,20 @@ const config = { from: { height: "var(--radix-accordion-content-height)" }, to: { height: "0" }, }, + // Add caret-blink keyframes if not already present via a plugin + "caret-blink": { + "0%, 50%, 100%": { opacity: "1" }, + "25%, 75%": { opacity: "0" }, + }, }, animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", + "caret-blink": "caret-blink 1s ease-in-out infinite", // Add caret animation }, }, }, - plugins: [require("tailwindcss-animate")], + plugins: [require("tailwindcss-animate")], // Keep animate plugin } satisfies Config; export default config; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index d8b9323..d13dc6c 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,27 +1,56 @@ { "compilerOptions": { - "target": "ES2017", + "target": "ES2017", // Keep target "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, - "strict": true, - "noEmit": true, + "strict": true, // Keep strict mode + "noEmit": true, // Next.js handles emitting "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", + "module": "esnext", // Use esnext module system + "moduleResolution": "bundler", // Recommended for modern TS/JS "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve", + "jsx": "preserve", // Let Next.js handle JSX transform "incremental": true, "plugins": [ { "name": "next" } ], + // Ensure path aliases cover the new structure "paths": { - "@/*": ["./*"] + "@/*": ["./*"], // Base alias + "@/components/*": ["./components/*"], + "@/lib/*": ["./lib/*"], + "@/hooks/*": ["./hooks/*"], + "@/features/*": ["./features/*"], + "@/types/*": ["./types/*"], + "@/services/*": ["./services/*"], + "@/store/*": ["./store/*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + // Update include paths + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + // Add specific includes if needed, but '**/*.ts/tsx' usually covers it + // "features/**/*.ts", + // "features/**/*.tsx", + // "components/**/*.ts", + // "components/**/*.tsx", + // "lib/**/*.ts", + // "hooks/**/*.ts", + // "types/**/*.ts", + // "services/**/*.ts", + // "store/**/*.ts", + "eslint.config.mjs", // Include modern ESLint config + "postcss.config.mjs" // Include modern PostCSS config + ], + "exclude": [ + "node_modules" + // Add other excludes if necessary + ] } diff --git a/frontend/types/api.ts b/frontend/types/api.ts new file mode 100644 index 0000000..a756412 --- /dev/null +++ b/frontend/types/api.ts @@ -0,0 +1,41 @@ +/* +======================================== +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 +} + +/** Basic Property Information */ +export interface PropertySummary { + id: string; + address: string; + lat: number; + lng: number; + price: number; + type: string; // e.g., 'Condo', 'House' + size?: number; // sqm +} + +/** User representation */ +export interface User { + id: string; + username: string; + email: string; + // Add roles or other relevant fields +} + +// Add other globally shared types (e.g., PipelineStatus, DataSourceType if needed FE side) diff --git a/frontend/types/index.ts b/frontend/types/index.ts new file mode 100644 index 0000000..01638fe --- /dev/null +++ b/frontend/types/index.ts @@ -0,0 +1,12 @@ +/* +======================================== +File: frontend/types/index.ts (NEW - Barrel File) +======================================== +*/ + +// Re-export shared types for easier importing +export * from "./api"; + +// You can add other shared types here or export from other files in this directory +// export * from './user'; +// export * from './settings';