diff --git a/crawler/frontend/package-lock.json b/crawler/frontend/package-lock.json index 66afda9..68a156c 100644 --- a/crawler/frontend/package-lock.json +++ b/crawler/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@hookform/resolvers": "^5.1.1", + "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-hover-card": "^1.1.14", "@radix-ui/react-label": "^2.1.7", @@ -913,6 +914,34 @@ "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", "license": "MIT" }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.14.tgz", + "integrity": "sha512-IOZfZ3nPvN6lXpJTBCunFQPRSvK8MDgSc1FB85xnIpUKOw9en0dJj8JmCAxV7BiZdtYlUpmrQjoTFkVYtdoWzQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.14", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", diff --git a/crawler/frontend/package.json b/crawler/frontend/package.json index 552d48f..bf0764c 100644 --- a/crawler/frontend/package.json +++ b/crawler/frontend/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@hookform/resolvers": "^5.1.1", + "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-hover-card": "^1.1.14", "@radix-ui/react-label": "^2.1.7", diff --git a/crawler/frontend/src/App.tsx b/crawler/frontend/src/App.tsx index d87b9c4..75b65ff 100644 --- a/crawler/frontend/src/App.tsx +++ b/crawler/frontend/src/App.tsx @@ -4,6 +4,7 @@ import './App.css'; import { AppSidebar } from './AppSidebar'; import { getUser, handleCallback, logout } from './auth/authService'; import ActiveQuery from './components/ActiveQuery'; +import AlertError from './components/AlertError'; import LoginModal from './components/LoginModal'; import { Map } from './components/Map'; import { Parameters, type ParameterValues } from './components/Parameters'; @@ -12,10 +13,51 @@ import { Button } from './components/ui/button'; import { Separator } from './components/ui/separator'; import { SidebarInset, SidebarProvider, SidebarTrigger } from './components/ui/sidebar'; +const fetchData = async (user: User, baseQueyrUri: string, parameters: ParameterValues, method: string = 'GET') => { + const accessToken = user.access_token; + const queryString = new URLSearchParams(); + queryString.append('listing_type', parameters.listing_type) + if (parameters.min_bedrooms) { + queryString.append('min_bedrooms', parameters.min_bedrooms.toString()); + } + if (parameters.max_bedrooms) { + queryString.append('max_bedrooms', parameters.max_bedrooms.toString()) + } + if (parameters.max_price) { + queryString.append("max_price", parameters.max_price.toString()); + } + if (parameters.min_price) { + queryString.append("min_price", parameters.min_price.toString()); + } + if (parameters.min_sqm) { + queryString.append("min_sqm", parameters.min_sqm.toString()); + } + + const response = await fetch(baseQueyrUri + '?' + queryString, + { + method: method, + headers: { + 'Authorization': `Bearer ${accessToken}`, // Pass the token + 'Content-Type': 'application/json', + }, + } + ); + if (!response.ok) { + throw new Error('Error: ' + response.status); + } + const data: Response = await response.json(); + return data; +}; + + function App() { const [listingData, setListingData] = useState({}); const [taskID, setTaskID] = useState(null); const [user, setUser] = useState(null); + const [isParametersModalOpen, setIsParametersModalOpen] = useState(true); + const [queryParameters, setQueryParameters] = useState(null); + const [submitError, setSubmitError] = useState(null); + const [alertDialogIsOpen, setAlertDialogIsOpen] = useState(false); useEffect(() => { // Check if this is a callback from Authentik (after login) @@ -30,68 +72,40 @@ function App() { getUser().then(setUser); }, []); - const [isParametersModalOpen, setIsParametersModalOpen] = useState(true) - const [queryParameters, setQueryParameters] = useState(null) - const fetchData = async (baseQueyrUri: string, parameters: ParameterValues, method: string = 'GET') => { - const accessToken = user?.access_token; - const queryString = new URLSearchParams(); - queryString.append('listing_type', parameters.listing_type) - if (parameters.min_bedrooms) { - queryString.append('min_bedrooms', parameters.min_bedrooms.toString()); - } - if (parameters.max_bedrooms) { - queryString.append('max_bedrooms', parameters.max_bedrooms.toString()) - } - if (parameters.max_price) { - queryString.append("max_price", parameters.max_price.toString()); - } - if (parameters.min_price) { - queryString.append("min_price", parameters.min_price.toString()); - } - if (parameters.min_sqm) { - queryString.append("min_sqm", parameters.min_sqm.toString()); - } + if (!user) { + return + } - const response = await fetch(baseQueyrUri + '?' + queryString, - { - method: method, - headers: { - 'Authorization': `Bearer ${accessToken}`, // Pass the token - 'Content-Type': 'application/json', - }, - } - ); - if (!response.ok) { - throw new Error('Error: ' + response.status); - } - const data: Response = await response.json(); - return data; - }; const onSubmit = async (action: 'fetch-data' | 'visualize', parameters: ParameterValues) => { // Fetch listing data setQueryParameters(parameters) setIsParametersModalOpen(false) let data = null; if (action === 'visualize') { - data = await fetchData("/api/listing_geojson", parameters); + try { + data = await fetchData(user, "/api/listing_geojson", parameters); + } catch (error) { + setSubmitError(error.message) + setAlertDialogIsOpen(true) + } if (data) { setListingData(data); } } else if (action === 'fetch-data') { - data = await fetchData("/api/refresh_listings", parameters, 'POST'); + try { + data = await fetchData(user, "/api/refresh_listings", parameters, 'POST'); + } catch (error) { + setSubmitError(error.message) + setAlertDialogIsOpen(true) + } if (data) { // @ts-expect-error setTaskID(data.task_id) } } console.log(data) - setIsParametersModalOpen(false) - } - if (!user) { - return - } return ( <> @@ -121,8 +135,9 @@ function App() {

Welcome, {user.profile.name}!

- + +
{Object.keys(listingData).length > 0 &&
diff --git a/crawler/frontend/src/components/ActiveQuery.tsx b/crawler/frontend/src/components/ActiveQuery.tsx index e91a760..9830ecf 100644 --- a/crawler/frontend/src/components/ActiveQuery.tsx +++ b/crawler/frontend/src/components/ActiveQuery.tsx @@ -1,6 +1,7 @@ import { getUser } from '@/auth/authService'; import type { User } from 'oidc-client-ts'; import React, { useEffect, useState } from 'react'; +import AlertError from './AlertError'; import { HoverCard, HoverCardContent, HoverCardTrigger } from './ui/hover-card'; import { Progress } from './ui/progress'; @@ -17,7 +18,6 @@ const fetchTaskStatus = async (user: User, taskID: string) => { 'Content-Type': 'application/json', }, }); - if (!response.ok) { throw new Error(`Failed to fetch task status: ${response.status}`); } @@ -73,6 +73,8 @@ const ActiveQuery: React.FC = ({ const [progressPercentage, setProgressPercentage] = useState(0); const [taskStatus, setTaskStatus] = useState(TaskStatus.QUEUED); const [lastUpdateTime, setLastUpdateTime] = useState(new Date()); + const [fetchStatusError, setFetchStatusError] = useState(null); + const [alertDialogIsOpen, setAlertDialogIsOpen] = useState(false); // fetch status periodically // maybe move to ws one day @@ -88,7 +90,12 @@ const ActiveQuery: React.FC = ({ } catch (error: any) { clearInterval(interval); setTaskStatus(TaskStatus.FAILED) - alert(error) + setAlertDialogIsOpen(true) + if (error instanceof Error) { + setFetchStatusError(error.message) + } else { + setFetchStatusError('Failed to update task status: ' + error.toString()) + } } if (!data) { clearInterval(interval); @@ -120,7 +127,7 @@ const ActiveQuery: React.FC = ({
- {taskStatus &&

Task status: {taskStatus}

} + {taskStatus && <>Task status: {taskStatus} }
@@ -130,7 +137,7 @@ const ActiveQuery: React.FC = ({
- + ) }; diff --git a/crawler/frontend/src/components/AlertError.tsx b/crawler/frontend/src/components/AlertError.tsx new file mode 100644 index 0000000..e771b0e --- /dev/null +++ b/crawler/frontend/src/components/AlertError.tsx @@ -0,0 +1,26 @@ +import { AlertDialog, AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "./ui/alert-dialog"; + +export default function AlertError( + props: { + message: string | null; + open: boolean; + setIsOpen: (isOpen: boolean) => void; + } +) { + return ( + + {/* Open */} + + + Error + + {props.message} + + + + props.setIsOpen(false)}>Close + + + + ) +} diff --git a/crawler/frontend/src/components/Parameters.tsx b/crawler/frontend/src/components/Parameters.tsx index e379274..26ea8ec 100644 --- a/crawler/frontend/src/components/Parameters.tsx +++ b/crawler/frontend/src/components/Parameters.tsx @@ -36,6 +36,7 @@ export function Parameters( props: { isOpen: boolean, onSubmit: (action: 'fetch-data' | 'visualize', fromValues: ParameterValues) => void, + setIsOpen: (isOpen: boolean) => void } ) { const { @@ -76,10 +77,10 @@ export function Parameters( return <> - {/* */} - + + {/* */} - + diff --git a/crawler/frontend/src/components/ui/alert-dialog.tsx b/crawler/frontend/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..935eecf --- /dev/null +++ b/crawler/frontend/src/components/ui/alert-dialog.tsx @@ -0,0 +1,155 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/crawler/frontend/src/components/ui/alert.tsx b/crawler/frontend/src/components/ui/alert.tsx new file mode 100644 index 0000000..1421354 --- /dev/null +++ b/crawler/frontend/src/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription }