Toast refactor, hts tweaks
This commit is contained in:
Generated
-35
@@ -31,7 +31,6 @@
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
"@radix-ui/react-toast": "^1.2.6",
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.1.6",
|
||||
@@ -2434,40 +2433,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-toast": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.6.tgz",
|
||||
"integrity": "sha512-gN4dpuIVKEgpLn1z5FhzT9mYRUitbfZq9XqN/7kkBMUgFTzTG8x/KszWJugJXHcwxckY8xcKDZPz7kG3o6DsUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-collection": "1.1.2",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.5",
|
||||
"@radix-ui/react-portal": "1.1.4",
|
||||
"@radix-ui/react-presence": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0",
|
||||
"@radix-ui/react-visually-hidden": "1.1.2"
|
||||
},
|
||||
"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-toggle": {
|
||||
"version": "1.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz",
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
"@radix-ui/react-toast": "^1.2.6",
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.1.6",
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Lock, Delete } from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const MAX_ATTEMPTS = 3;
|
||||
const LOCKOUT_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds
|
||||
@@ -27,8 +27,6 @@ const PinProtection = ({ onSuccess }) => {
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
let timer;
|
||||
if (lockoutTime > 0) {
|
||||
@@ -59,34 +57,25 @@ const PinProtection = ({ onSuccess }) => {
|
||||
|
||||
if (newAttempts >= MAX_ATTEMPTS) {
|
||||
setLockoutTime(LOCKOUT_DURATION);
|
||||
toast({
|
||||
title: "Too many attempts",
|
||||
toast.error("Too many attempts", {
|
||||
description: `Please try again in ${Math.ceil(LOCKOUT_DURATION / 60000)} minutes`,
|
||||
variant: "destructive",
|
||||
});
|
||||
setPin("");
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === "892312") {
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "PIN accepted",
|
||||
});
|
||||
toast.success("PIN accepted");
|
||||
// Reset attempts on success
|
||||
setAttempts(0);
|
||||
localStorage.removeItem('pinAttempts');
|
||||
localStorage.removeItem('lastAttemptTime');
|
||||
onSuccess();
|
||||
} else {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: `Incorrect PIN. ${MAX_ATTEMPTS - newAttempts} attempts remaining`,
|
||||
variant: "destructive",
|
||||
});
|
||||
toast.error(`Incorrect PIN. ${MAX_ATTEMPTS - newAttempts} attempts remaining`);
|
||||
setPin("");
|
||||
}
|
||||
}, [attempts, lockoutTime, onSuccess, toast]);
|
||||
}, [attempts, lockoutTime, onSuccess]);
|
||||
|
||||
const handleKeyPress = (value) => {
|
||||
if (pin.length < 6) {
|
||||
|
||||
@@ -229,8 +229,8 @@ export const BASE_IMPORT_FIELDS = [
|
||||
{
|
||||
label: "Weight",
|
||||
key: "weight",
|
||||
description: "Product weight (in lbs)",
|
||||
alternateMatches: ["weight (lbs.)"],
|
||||
description: "Product weight (in oz — convert lbs ×16, kg ×35.274)",
|
||||
alternateMatches: ["weight (lbs.)", "weight (oz)", "weight (oz.)"],
|
||||
fieldType: { type: "input" },
|
||||
width: 100,
|
||||
validations: [
|
||||
|
||||
@@ -3,7 +3,7 @@ import { SelectHeaderTable } from "./components/SelectHeaderTable"
|
||||
import { useRsi } from "../../hooks/useRsi"
|
||||
import type { RawData } from "../../types"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useToast } from "@/hooks/use-toast"
|
||||
import { toast } from "sonner"
|
||||
|
||||
type SelectHeaderProps = {
|
||||
data: RawData[]
|
||||
@@ -24,7 +24,6 @@ const isRowCompletelyEmpty = (row: RawData): boolean => {
|
||||
|
||||
export const SelectHeaderStep = ({ data, onContinue, onBack }: SelectHeaderProps) => {
|
||||
const { translations } = useRsi()
|
||||
const { toast } = useToast()
|
||||
const [selectedRows, setSelectedRows] = useState<ReadonlySet<number>>(new Set([0]))
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
@@ -133,19 +132,15 @@ export const SelectHeaderStep = ({ data, onContinue, onBack }: SelectHeaderProps
|
||||
setLocalData(filteredRows);
|
||||
setSelectedRows(new Set([newSelectedIndex]));
|
||||
|
||||
toast({
|
||||
title: "Rows removed",
|
||||
toast.success("Rows removed", {
|
||||
description: `Removed ${localData.length - filteredRows.length} empty, single-value, or duplicate rows`,
|
||||
variant: "default"
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: "No rows removed",
|
||||
toast.info("No rows removed", {
|
||||
description: "No empty, single-value, or duplicate rows were found",
|
||||
variant: "default"
|
||||
});
|
||||
}
|
||||
}, [localData, selectedRows, toast]);
|
||||
}, [localData, selectedRows]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-[calc(100vh-9.5rem)]">
|
||||
|
||||
@@ -13,7 +13,7 @@ import { exceedsMaxRecords } from "../utils/exceedsMaxRecords"
|
||||
import { useRsi } from "../hooks/useRsi"
|
||||
import type { RawData, Data } from "../types"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { useToast } from "@/hooks/use-toast"
|
||||
import { toast } from "sonner"
|
||||
import { addErrorsAndRunHooks } from "./ValidationStep/utils/dataMutations"
|
||||
import { computeMappingSignature, type MappingSignature } from "./ValidationStep/utils/mappingSignature"
|
||||
import { useValidationStore } from "./ValidationStep/store/validationStore"
|
||||
@@ -88,7 +88,6 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => {
|
||||
tableHook,
|
||||
onSubmit } = useRsi()
|
||||
const [uploadedFile, setUploadedFile] = useState<File | null>(null)
|
||||
const { toast } = useToast()
|
||||
const queryClient = useQueryClient()
|
||||
const resetValidationStore = useValidationStore((state) => state.reset)
|
||||
|
||||
@@ -116,13 +115,9 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => {
|
||||
}, [queryClient, resetValidationStore]);
|
||||
const errorToast = useCallback(
|
||||
(description: string) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: translations.alerts.toast.error,
|
||||
description,
|
||||
})
|
||||
toast.error(translations.alerts.toast.error, { description })
|
||||
},
|
||||
[toast, translations],
|
||||
[translations],
|
||||
)
|
||||
|
||||
// Keep track of global selections across steps
|
||||
|
||||
@@ -6,14 +6,6 @@ import { StepType } from "../UploadFlow"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { AuthContext } from "@/contexts/AuthContext"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Bug } from "lucide-react"
|
||||
@@ -27,17 +19,23 @@ type UploadProps = {
|
||||
onRestoreSession?: (session: ImportSession) => void
|
||||
}
|
||||
|
||||
const OrSeparator = () => (
|
||||
<div className="flex items-center justify-center">
|
||||
<Separator className="w-24" />
|
||||
<span className="px-3 text-muted-foreground text-sm font-medium">OR</span>
|
||||
<Separator className="w-24" />
|
||||
</div>
|
||||
)
|
||||
|
||||
export const UploadStep = ({ onContinue, setInitialState, onRestoreSession }: UploadProps) => {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const { translations } = useRsi()
|
||||
const { user } = useContext(AuthContext)
|
||||
const hasDebugPermission = Boolean(user?.is_admin || user?.permissions?.includes("admin:debug"))
|
||||
|
||||
// Debug import state
|
||||
const [debugDialogOpen, setDebugDialogOpen] = useState(false)
|
||||
const [debugJsonInput, setDebugJsonInput] = useState("")
|
||||
const [debugError, setDebugError] = useState<string | null>(null)
|
||||
|
||||
const [jsonInput, setJsonInput] = useState("")
|
||||
const [jsonError, setJsonError] = useState<string | null>(null)
|
||||
|
||||
const handleOnContinue = useCallback(
|
||||
async (data: XLSX.WorkBook, file: File) => {
|
||||
setIsLoading(true)
|
||||
@@ -46,23 +44,23 @@ export const UploadStep = ({ onContinue, setInitialState, onRestoreSession }: Up
|
||||
},
|
||||
[onContinue],
|
||||
)
|
||||
|
||||
|
||||
const handleStartFromScratch = useCallback(() => {
|
||||
if (setInitialState) {
|
||||
setInitialState({ type: StepType.validateData, data: [{}], isFromScratch: true })
|
||||
}
|
||||
}, [setInitialState])
|
||||
|
||||
const handleDebugImport = useCallback(() => {
|
||||
setDebugError(null)
|
||||
const handleJsonImport = useCallback(() => {
|
||||
setJsonError(null)
|
||||
try {
|
||||
const parsed = JSON.parse(debugJsonInput)
|
||||
const parsed = JSON.parse(jsonInput)
|
||||
|
||||
// Handle both array and object with products property
|
||||
let products: any[] = Array.isArray(parsed) ? parsed : parsed.products
|
||||
const products: any[] = Array.isArray(parsed) ? parsed : parsed.products
|
||||
|
||||
if (!Array.isArray(products) || products.length === 0) {
|
||||
setDebugError("JSON must be an array of products or an object with a 'products' array")
|
||||
setJsonError("JSON must be an array of products or an object with a 'products' array")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -79,47 +77,59 @@ export const UploadStep = ({ onContinue, setInitialState, onRestoreSession }: Up
|
||||
isFromScratch: true
|
||||
})
|
||||
}
|
||||
|
||||
setDebugDialogOpen(false)
|
||||
setDebugJsonInput("")
|
||||
} catch (e) {
|
||||
setDebugError(`Invalid JSON: ${e instanceof Error ? e.message : "Parse error"}`)
|
||||
setJsonError(`Invalid JSON: ${e instanceof Error ? e.message : "Parse error"}`)
|
||||
}
|
||||
}, [debugJsonInput, setInitialState])
|
||||
|
||||
}, [jsonInput, setInitialState])
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="flex items-baseline justify-between">
|
||||
<h2 className="text-3xl font-semibold mb-8 text-left">{translations.uploadStep.title}</h2>
|
||||
{hasDebugPermission && (
|
||||
<div className="max-w-xl mx-auto w-full space-y-8">
|
||||
{hasDebugPermission && (
|
||||
<>
|
||||
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
onClick={() => setDebugDialogOpen(true)}
|
||||
variant="outline"
|
||||
className="min-w-[200px] text-amber-600 border-amber-600 hover:bg-amber-50"
|
||||
disabled={!setInitialState}
|
||||
>
|
||||
<Bug className="mr-2 h-4 w-4" />
|
||||
<div className="rounded-lg border border-amber-600/40 p-6 space-y-3">
|
||||
<Label htmlFor="import-json" className="flex items-center gap-2 text-amber-600 font-semibold">
|
||||
<Bug className="h-4 w-4" />
|
||||
Import JSON
|
||||
</Button>
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Paste product data in the same JSON format as the API submission. The data will be loaded into the validation step.
|
||||
</p>
|
||||
<Textarea
|
||||
id="import-json"
|
||||
placeholder='[{"supplier": "...", "company": "...", "name": "...", "product_images": "url1,url2", ...}]'
|
||||
value={jsonInput}
|
||||
onChange={(e) => {
|
||||
setJsonInput(e.target.value)
|
||||
setJsonError(null)
|
||||
}}
|
||||
className="min-h-[160px] font-mono text-sm"
|
||||
/>
|
||||
{jsonError && (
|
||||
<p className="text-sm text-destructive">{jsonError}</p>
|
||||
)}
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
onClick={handleJsonImport}
|
||||
disabled={!setInitialState || !jsonInput.trim()}
|
||||
className="bg-amber-600 hover:bg-amber-700"
|
||||
>
|
||||
Import & Go to Validation
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<OrSeparator />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="max-w-xl mx-auto w-full space-y-8">
|
||||
|
||||
<div className="rounded-lg p-6 flex flex-col items-center">
|
||||
<DropZone onContinue={handleOnContinue} isLoading={isLoading} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center">
|
||||
<Separator className="w-24" />
|
||||
<span className="px-3 text-muted-foreground text-sm font-medium">OR</span>
|
||||
<Separator className="w-24" />
|
||||
</div>
|
||||
|
||||
|
||||
<OrSeparator />
|
||||
|
||||
<div className="flex justify-center pb-8">
|
||||
<Button
|
||||
onClick={handleStartFromScratch}
|
||||
@@ -136,50 +146,6 @@ export const UploadStep = ({ onContinue, setInitialState, onRestoreSession }: Up
|
||||
<SavedSessionsList onRestore={onRestoreSession} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Dialog open={debugDialogOpen} onOpenChange={setDebugDialogOpen}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-amber-600">
|
||||
<Bug className="h-5 w-5" />
|
||||
Debug: Import JSON Data
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Paste product data in the same JSON format as the API submission. The data will be loaded into the validation step.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="debug-json">Product JSON</Label>
|
||||
<Textarea
|
||||
id="debug-json"
|
||||
placeholder='[{"supplier": "...", "company": "...", "name": "...", "product_images": "url1,url2", ...}]'
|
||||
value={debugJsonInput}
|
||||
onChange={(e) => {
|
||||
setDebugJsonInput(e.target.value)
|
||||
setDebugError(null)
|
||||
}}
|
||||
className="min-h-[300px] font-mono text-sm"
|
||||
/>
|
||||
{debugError && (
|
||||
<p className="text-sm text-destructive">{debugError}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDebugDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleDebugImport}
|
||||
disabled={!debugJsonInput.trim()}
|
||||
className="bg-amber-600 hover:bg-amber-700"
|
||||
>
|
||||
Import & Go to Validation
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState } from "react"
|
||||
import { useRsi } from "../../../hooks/useRsi"
|
||||
import { readFileAsync } from "../utils/readFilesAsync"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useToast } from "@/hooks/use-toast"
|
||||
import { toast } from "sonner"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
type DropZoneProps = {
|
||||
@@ -14,7 +14,6 @@ type DropZoneProps = {
|
||||
|
||||
export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => {
|
||||
const { translations, maxFileSize, dateFormat, parseRaw } = useRsi()
|
||||
const { toast } = useToast()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
|
||||
noClick: true,
|
||||
@@ -29,9 +28,7 @@ export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => {
|
||||
onDropRejected: (fileRejections) => {
|
||||
setLoading(false)
|
||||
fileRejections.forEach((fileRejection) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: `${fileRejection.file.name} ${translations.uploadStep.dropzone.errorToastDescription}`,
|
||||
toast.error(`${fileRejection.file.name} ${translations.uploadStep.dropzone.errorToastDescription}`, {
|
||||
description: fileRejection.errors[0].message,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -262,7 +262,7 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps)
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
<Input {...field} autoComplete="off" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -290,10 +290,11 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps)
|
||||
<FormItem>
|
||||
<FormLabel>{user ? "New Password" : "Password"}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
{...field}
|
||||
placeholder={user ? "Leave blank to keep current password" : ""}
|
||||
<Input
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
{...field}
|
||||
placeholder={user ? "Leave blank to keep current password" : ""}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { UserList } from "./UserList";
|
||||
import { UserForm } from "./UserForm";
|
||||
import { toast } from "sonner";
|
||||
import config from "@/config";
|
||||
import { AuthContext } from "@/contexts/AuthContext";
|
||||
import { ShieldAlert } from "lucide-react";
|
||||
@@ -241,7 +242,16 @@ export function UserManagement() {
|
||||
}
|
||||
|
||||
console.log("Server response after saving user:", responseData);
|
||||
|
||||
|
||||
// Explicitly confirm whether a password was part of this save — the form
|
||||
// silently drops an empty password on edit, so without this there is no
|
||||
// way to tell if a password change actually went through.
|
||||
toast.success(
|
||||
userData.id
|
||||
? (formattedUserData.password ? "User updated — password changed" : "User updated (password not changed)")
|
||||
: "User created"
|
||||
);
|
||||
|
||||
// Reset the form state
|
||||
setSelectedUser(null);
|
||||
setIsAddingUser(false);
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { useDebounce } from '@/hooks/useDebounce';
|
||||
import { Search } from "lucide-react";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { toast } from "sonner";
|
||||
import config from "@/config";
|
||||
|
||||
interface VendorSetting {
|
||||
@@ -34,7 +34,6 @@ export function VendorSettings() {
|
||||
const [searchInputValue, setSearchInputValue] = useState('');
|
||||
const searchQuery = useDebounce(searchInputValue, 300); // 300ms debounce
|
||||
const [pendingChanges, setPendingChanges] = useState<Record<string, boolean>>({});
|
||||
const { toast } = useToast();
|
||||
|
||||
// Use useCallback to avoid unnecessary re-renders
|
||||
const loadSettings = useCallback(async () => {
|
||||
@@ -50,15 +49,11 @@ export function VendorSettings() {
|
||||
setSettings(data.items);
|
||||
setTotalCount(data.total);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: `Failed to load settings: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
toast.error(`Failed to load settings: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [page, searchQuery, pageSize, toast]);
|
||||
}, [page, searchQuery, pageSize]);
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
@@ -93,19 +88,12 @@ export function VendorSettings() {
|
||||
throw new Error(data.error || 'Failed to update vendor setting');
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Success",
|
||||
description: `Settings updated for vendor ${vendor}`,
|
||||
});
|
||||
toast.success(`Settings updated for vendor ${vendor}`);
|
||||
setPendingChanges(prev => ({ ...prev, [vendor]: false }));
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: `Failed to update setting: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
toast.error(`Failed to update setting: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}, [settings, toast]);
|
||||
}, [settings]);
|
||||
|
||||
const handleResetToDefault = useCallback(async (vendor: string) => {
|
||||
try {
|
||||
@@ -119,19 +107,12 @@ export function VendorSettings() {
|
||||
throw new Error(data.error || 'Failed to reset vendor setting');
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Success",
|
||||
description: `Settings reset for vendor ${vendor}`,
|
||||
});
|
||||
toast.success(`Settings reset for vendor ${vendor}`);
|
||||
loadSettings(); // Reload settings to get defaults
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: `Failed to reset setting: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
toast.error(`Failed to reset setting: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}, [loadSettings, toast]);
|
||||
}, [loadSettings]);
|
||||
|
||||
const totalPages = useMemo(() => Math.ceil(totalCount / pageSize), [totalCount, pageSize]);
|
||||
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
import * as React from "react"
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider
|
||||
|
||||
const ToastViewport = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||
|
||||
const toastVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border bg-background text-foreground",
|
||||
destructive:
|
||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Toast.displayName = ToastPrimitives.Root.displayName
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||
className
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
))
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||
|
||||
const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm opacity-90", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||
|
||||
export {
|
||||
type ToastProps,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { useToast } from "@/hooks/use-toast"
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from "@/components/ui/toast"
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast()
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
<ToastDescription>{description}</ToastDescription>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
)
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
"use client"
|
||||
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from "react"
|
||||
|
||||
import type {
|
||||
ToastActionElement,
|
||||
ToastProps,
|
||||
} from "@/components/ui/toast"
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
}
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const
|
||||
|
||||
let count = 0
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[]
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
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
|
||||
),
|
||||
}
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
}
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const listeners: Array<(state: State) => void> = []
|
||||
|
||||
let memoryState: State = { toasts: [] }
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action)
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id">
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId()
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
}
|
||||
}
|
||||
|
||||
export { useToast, toast }
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
SurchargeConfig,
|
||||
CogsCalculationMode,
|
||||
} from "@/types/discount-simulator";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { toast } from "sonner";
|
||||
import { toDateOnly } from "@/utils/businessTime";
|
||||
|
||||
const DEFAULT_POINT_VALUE = 0.005;
|
||||
@@ -53,7 +53,6 @@ const defaultShippingPromo = {
|
||||
};
|
||||
|
||||
export function DiscountSimulator() {
|
||||
const { toast } = useToast();
|
||||
const [dateRange, setDateRange] = useState<DateRange>(() => getDefaultDateRange());
|
||||
const [selectedPromoId, setSelectedPromoId] = useState<number | undefined>(undefined);
|
||||
const [productPromo, setProductPromo] = useState(defaultProductPromo);
|
||||
@@ -214,10 +213,8 @@ export function DiscountSimulator() {
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Simulation error', error);
|
||||
toast({
|
||||
title: 'Simulation failed',
|
||||
toast.error('Simulation failed', {
|
||||
description: error instanceof Error ? error.message : 'Unable to run discount simulation right now.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
},
|
||||
onSettled: () => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type HtsProduct = {
|
||||
@@ -47,7 +47,6 @@ type HtsLookupResponse = {
|
||||
};
|
||||
|
||||
export default function HtsLookup() {
|
||||
const { toast } = useToast();
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [submittedTerm, setSubmittedTerm] = useState("");
|
||||
const [copiedCode, setCopiedCode] = useState<string | null>(null);
|
||||
@@ -93,13 +92,9 @@ export default function HtsLookup() {
|
||||
|
||||
useEffect(() => {
|
||||
if (error instanceof Error) {
|
||||
toast({
|
||||
title: "Search failed",
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
});
|
||||
toast.error("Search failed", { description: error.message });
|
||||
}
|
||||
}, [error, toast]);
|
||||
}, [error]);
|
||||
|
||||
const totalMatches = data?.total ?? 0;
|
||||
const groupedResults = useMemo(() => data?.results ?? [], [data]);
|
||||
@@ -110,10 +105,8 @@ export default function HtsLookup() {
|
||||
const valueToCopy = code === "Unspecified" ? "" : code;
|
||||
|
||||
if (!navigator?.clipboard) {
|
||||
toast({
|
||||
title: "Clipboard unavailable",
|
||||
toast.error("Clipboard unavailable", {
|
||||
description: "Your browser did not allow copying the code.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -125,15 +118,12 @@ export default function HtsLookup() {
|
||||
}
|
||||
setCopiedCode(code);
|
||||
copyTimerRef.current = window.setTimeout(() => setCopiedCode(null), 1200);
|
||||
toast({
|
||||
title: "Copied HTS code",
|
||||
toast.success("Copied HTS code", {
|
||||
description: valueToCopy || "Empty code copied",
|
||||
});
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: "Copy failed",
|
||||
toast.error("Copy failed", {
|
||||
description: err instanceof Error ? err.message : "Unable to copy code",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -143,8 +133,7 @@ export default function HtsLookup() {
|
||||
const trimmed = searchTerm.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
toast({
|
||||
title: "Enter a search term",
|
||||
toast.info("Enter a search term", {
|
||||
description: "Search by title, SKU, vendor, barcode, or HTS code.",
|
||||
});
|
||||
return;
|
||||
|
||||
@@ -32,7 +32,7 @@ import { ProductDetail } from "@/components/products/ProductDetail";
|
||||
import { ProductViews } from "@/components/products/ProductViews";
|
||||
import { ProductTableSkeleton } from "@/components/products/ProductTableSkeleton";
|
||||
import { ProductSummaryCards } from "@/components/products/ProductSummaryCards";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { toast } from "sonner";
|
||||
import { useDebounce } from "@/hooks/useDebounce";
|
||||
import { AVAILABLE_COLUMNS, VIEW_COLUMNS, COLUMNS_BY_GROUP } from "@/components/products/columnDefinitions";
|
||||
import { transformMetricsRow, OPERATOR_MAP } from "@/utils/transformUtils";
|
||||
@@ -160,7 +160,6 @@ export function Products() {
|
||||
const [showNonReplenishable, setShowNonReplenishable] = useState(false);
|
||||
const [showInvisible, setShowInvisible] = useState(false);
|
||||
const [selectedProductId, setSelectedProductId] = useState<number | null>(null);
|
||||
const { toast } = useToast();
|
||||
const searchInputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
// Store visible columns and order for each view
|
||||
@@ -310,14 +309,10 @@ export function Products() {
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching products:', error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to fetch products. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
toast.error("Failed to fetch products. Please try again.");
|
||||
return null;
|
||||
}
|
||||
}, [currentPage, pageSize, sortColumn, sortDirection, activeView, effectiveFilters, showNonReplenishable, showInvisible, toast]);
|
||||
}, [currentPage, pageSize, sortColumn, sortDirection, activeView, effectiveFilters, showNonReplenishable, showInvisible]);
|
||||
|
||||
// Query for filter options
|
||||
const { data: filterOptionsData, isLoading: isLoadingFilterOptions } = useQuery({
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type NumericAggregate = {
|
||||
@@ -112,7 +112,6 @@ function formatCurrency(n: number): string {
|
||||
}
|
||||
|
||||
export default function SpecLookup() {
|
||||
const { toast } = useToast();
|
||||
const [company, setCompany] = useState("");
|
||||
const [term, setTerm] = useState("");
|
||||
const [companyOpen, setCompanyOpen] = useState(false);
|
||||
@@ -168,9 +167,9 @@ export default function SpecLookup() {
|
||||
|
||||
useEffect(() => {
|
||||
if (error instanceof Error) {
|
||||
toast({ title: "Search failed", description: error.message, variant: "destructive" });
|
||||
toast.error("Search failed", { description: error.message });
|
||||
}
|
||||
}, [error, toast]);
|
||||
}, [error]);
|
||||
|
||||
const handleSubmit = (event?: FormEvent) => {
|
||||
event?.preventDefault();
|
||||
@@ -178,7 +177,7 @@ export default function SpecLookup() {
|
||||
const trimmedTerm = term.trim();
|
||||
|
||||
if (!trimmedCompany && !trimmedTerm) {
|
||||
toast({ title: "Enter a company or product type" });
|
||||
toast.info("Enter a company or product type");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -193,7 +192,7 @@ export default function SpecLookup() {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!navigator?.clipboard) {
|
||||
toast({ title: "Clipboard unavailable", variant: "destructive" });
|
||||
toast.error("Clipboard unavailable");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -202,10 +201,8 @@ export default function SpecLookup() {
|
||||
setCopied(key);
|
||||
copyTimerRef.current = window.setTimeout(() => setCopied(null), 1200);
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: "Copy failed",
|
||||
toast.error("Copy failed", {
|
||||
description: err instanceof Error ? err.message : "Unable to copy",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user