handle errors better in the ui with an alert component
This commit is contained in:
parent
5bd4562205
commit
b1cbe8505a
8 changed files with 351 additions and 51 deletions
29
crawler/frontend/package-lock.json
generated
29
crawler/frontend/package-lock.json
generated
|
|
@ -9,6 +9,7 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-hover-card": "^1.1.14",
|
"@radix-ui/react-hover-card": "^1.1.14",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
|
@ -913,6 +914,34 @@
|
||||||
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
|
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@radix-ui/react-arrow": {
|
||||||
"version": "1.1.7",
|
"version": "1.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-hover-card": "^1.1.14",
|
"@radix-ui/react-hover-card": "^1.1.14",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import './App.css';
|
||||||
import { AppSidebar } from './AppSidebar';
|
import { AppSidebar } from './AppSidebar';
|
||||||
import { getUser, handleCallback, logout } from './auth/authService';
|
import { getUser, handleCallback, logout } from './auth/authService';
|
||||||
import ActiveQuery from './components/ActiveQuery';
|
import ActiveQuery from './components/ActiveQuery';
|
||||||
|
import AlertError from './components/AlertError';
|
||||||
import LoginModal from './components/LoginModal';
|
import LoginModal from './components/LoginModal';
|
||||||
import { Map } from './components/Map';
|
import { Map } from './components/Map';
|
||||||
import { Parameters, type ParameterValues } from './components/Parameters';
|
import { Parameters, type ParameterValues } from './components/Parameters';
|
||||||
|
|
@ -12,10 +13,51 @@ import { Button } from './components/ui/button';
|
||||||
import { Separator } from './components/ui/separator';
|
import { Separator } from './components/ui/separator';
|
||||||
import { SidebarInset, SidebarProvider, SidebarTrigger } from './components/ui/sidebar';
|
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() {
|
function App() {
|
||||||
const [listingData, setListingData] = useState({});
|
const [listingData, setListingData] = useState({});
|
||||||
const [taskID, setTaskID] = useState<string | null>(null);
|
const [taskID, setTaskID] = useState<string | null>(null);
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
const [isParametersModalOpen, setIsParametersModalOpen] = useState(true);
|
||||||
|
const [queryParameters, setQueryParameters] = useState<ParameterValues | null>(null);
|
||||||
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||||
|
const [alertDialogIsOpen, setAlertDialogIsOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check if this is a callback from Authentik (after login)
|
// Check if this is a callback from Authentik (after login)
|
||||||
|
|
@ -30,68 +72,40 @@ function App() {
|
||||||
getUser().then(setUser);
|
getUser().then(setUser);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [isParametersModalOpen, setIsParametersModalOpen] = useState(true)
|
if (!user) {
|
||||||
const [queryParameters, setQueryParameters] = useState<ParameterValues | null>(null)
|
return <LoginModal isOpen={user === 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());
|
|
||||||
}
|
|
||||||
|
|
||||||
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) => {
|
const onSubmit = async (action: 'fetch-data' | 'visualize', parameters: ParameterValues) => {
|
||||||
// Fetch listing data
|
// Fetch listing data
|
||||||
setQueryParameters(parameters)
|
setQueryParameters(parameters)
|
||||||
setIsParametersModalOpen(false)
|
setIsParametersModalOpen(false)
|
||||||
let data = null;
|
let data = null;
|
||||||
if (action === 'visualize') {
|
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) {
|
if (data) {
|
||||||
setListingData(data);
|
setListingData(data);
|
||||||
}
|
}
|
||||||
} else if (action === 'fetch-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) {
|
if (data) {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
setTaskID(data.task_id)
|
setTaskID(data.task_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(data)
|
console.log(data)
|
||||||
setIsParametersModalOpen(false)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return <LoginModal isOpen={user === null} />
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SidebarProvider defaultOpen={false}>
|
<SidebarProvider defaultOpen={false}>
|
||||||
|
|
@ -121,8 +135,9 @@ function App() {
|
||||||
<div className="flex gap-2 p-2 bg-gray-100">
|
<div className="flex gap-2 p-2 bg-gray-100">
|
||||||
<h1>Welcome, {user.profile.name}!</h1>
|
<h1>Welcome, {user.profile.name}!</h1>
|
||||||
<Button onClick={logout}>Logout</Button>
|
<Button onClick={logout}>Logout</Button>
|
||||||
<Parameters onSubmit={onSubmit} isOpen={isParametersModalOpen} />
|
<Parameters onSubmit={onSubmit} isOpen={isParametersModalOpen} setIsOpen={setIsParametersModalOpen} />
|
||||||
<ActiveQuery taskID={taskID} />
|
<ActiveQuery taskID={taskID} />
|
||||||
|
<AlertError message={submitError} open={alertDialogIsOpen} setIsOpen={setAlertDialogIsOpen} />
|
||||||
</div>
|
</div>
|
||||||
{Object.keys(listingData).length > 0 &&
|
{Object.keys(listingData).length > 0 &&
|
||||||
<div className="flex-1 w-full relative" style={{ minHeight: 0, marginBottom: '8rem' }}>
|
<div className="flex-1 w-full relative" style={{ minHeight: 0, marginBottom: '8rem' }}>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { getUser } from '@/auth/authService';
|
import { getUser } from '@/auth/authService';
|
||||||
import type { User } from 'oidc-client-ts';
|
import type { User } from 'oidc-client-ts';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import AlertError from './AlertError';
|
||||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from './ui/hover-card';
|
import { HoverCard, HoverCardContent, HoverCardTrigger } from './ui/hover-card';
|
||||||
import { Progress } from './ui/progress';
|
import { Progress } from './ui/progress';
|
||||||
|
|
||||||
|
|
@ -17,7 +18,6 @@ const fetchTaskStatus = async (user: User, taskID: string) => {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch task status: ${response.status}`);
|
throw new Error(`Failed to fetch task status: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
@ -73,6 +73,8 @@ const ActiveQuery: React.FC<ModalProps> = ({
|
||||||
const [progressPercentage, setProgressPercentage] = useState<number>(0);
|
const [progressPercentage, setProgressPercentage] = useState<number>(0);
|
||||||
const [taskStatus, setTaskStatus] = useState<TaskStatus | null>(TaskStatus.QUEUED);
|
const [taskStatus, setTaskStatus] = useState<TaskStatus | null>(TaskStatus.QUEUED);
|
||||||
const [lastUpdateTime, setLastUpdateTime] = useState<Date>(new Date());
|
const [lastUpdateTime, setLastUpdateTime] = useState<Date>(new Date());
|
||||||
|
const [fetchStatusError, setFetchStatusError] = useState<string | null>(null);
|
||||||
|
const [alertDialogIsOpen, setAlertDialogIsOpen] = useState(false);
|
||||||
|
|
||||||
// fetch status periodically
|
// fetch status periodically
|
||||||
// maybe move to ws one day
|
// maybe move to ws one day
|
||||||
|
|
@ -88,7 +90,12 @@ const ActiveQuery: React.FC<ModalProps> = ({
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
setTaskStatus(TaskStatus.FAILED)
|
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) {
|
if (!data) {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
|
|
@ -120,7 +127,7 @@ const ActiveQuery: React.FC<ModalProps> = ({
|
||||||
<div>
|
<div>
|
||||||
<HoverCard>
|
<HoverCard>
|
||||||
<HoverCardTrigger>
|
<HoverCardTrigger>
|
||||||
{taskStatus && <p>Task status: {taskStatus} </p>}
|
{taskStatus && <>Task status: {taskStatus} </>}
|
||||||
<Progress value={progressPercentage} />
|
<Progress value={progressPercentage} />
|
||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
<HoverCardContent>
|
<HoverCardContent>
|
||||||
|
|
@ -130,7 +137,7 @@ const ActiveQuery: React.FC<ModalProps> = ({
|
||||||
</HoverCardContent>
|
</HoverCardContent>
|
||||||
</HoverCard>
|
</HoverCard>
|
||||||
</div>
|
</div>
|
||||||
|
<AlertError message={fetchStatusError} open={alertDialogIsOpen} setIsOpen={setAlertDialogIsOpen} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
|
||||||
26
crawler/frontend/src/components/AlertError.tsx
Normal file
26
crawler/frontend/src/components/AlertError.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<AlertDialog open={props.open} onOpenChange={props.setIsOpen}>
|
||||||
|
{/* <AlertDialogTrigger>Open</AlertDialogTrigger> */}
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Error</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{props.message}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogAction onClick={() => props.setIsOpen(false)}>Close</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -36,6 +36,7 @@ export function Parameters(
|
||||||
props: {
|
props: {
|
||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
onSubmit: (action: 'fetch-data' | 'visualize', fromValues: ParameterValues) => void,
|
onSubmit: (action: 'fetch-data' | 'visualize', fromValues: ParameterValues) => void,
|
||||||
|
setIsOpen: (isOpen: boolean) => void
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
|
|
@ -76,10 +77,10 @@ export function Parameters(
|
||||||
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
{/* <Dialog open={props.isOpen} > */}
|
<Dialog open={props.isOpen} onOpenChange={props.setIsOpen} >
|
||||||
<Dialog >
|
{/* <Dialog > */}
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline">Open Parameters</Button>
|
<Button variant="outline" onClick={() => props.setIsOpen(true)}>Open Parameters</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
|
|
|
||||||
155
crawler/frontend/src/components/ui/alert-dialog.tsx
Normal file
155
crawler/frontend/src/components/ui/alert-dialog.tsx
Normal file
|
|
@ -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<typeof AlertDialogPrimitive.Root>) {
|
||||||
|
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Overlay
|
||||||
|
data-slot="alert-dialog-overlay"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPortal>
|
||||||
|
<AlertDialogOverlay />
|
||||||
|
<AlertDialogPrimitive.Content
|
||||||
|
data-slot="alert-dialog-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogHeader({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-dialog-header"
|
||||||
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogFooter({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-dialog-footer"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Title
|
||||||
|
data-slot="alert-dialog-title"
|
||||||
|
className={cn("text-lg font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Description
|
||||||
|
data-slot="alert-dialog-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogAction({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Action
|
||||||
|
className={cn(buttonVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogCancel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Cancel
|
||||||
|
className={cn(buttonVariants({ variant: "outline" }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogPortal,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
}
|
||||||
66
crawler/frontend/src/components/ui/alert.tsx
Normal file
66
crawler/frontend/src/components/ui/alert.tsx
Normal file
|
|
@ -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<typeof alertVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert"
|
||||||
|
role="alert"
|
||||||
|
className={cn(alertVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-title"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-description"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Alert, AlertTitle, AlertDescription }
|
||||||
Loading…
Add table
Add a link
Reference in a new issue