Shadcn conversion + styling, match columns page

This commit is contained in:
2025-02-19 01:41:06 -05:00
parent 528fe7c024
commit 5bf265ed46
10 changed files with 355 additions and 257 deletions

View File

@@ -27,7 +27,7 @@ import {
import { Loader2, X, RefreshCw, AlertTriangle, RefreshCcw, Hourglass } from "lucide-react"; import { Loader2, X, RefreshCw, AlertTriangle, RefreshCcw, Hourglass } from "lucide-react";
import config from "../../config"; import config from "../../config";
import { toast } from "sonner"; import { toast } from "sonner";
import { Table, TableBody, TableCell, TableRow, TableHeader, TableHead } from "@/components/ui/table"; import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
interface ImportProgress { interface ImportProgress {
status: "running" | "error" | "complete" | "cancelled"; status: "running" | "error" | "complete" | "cancelled";

View File

@@ -1,5 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from "react" import { useCallback, useEffect, useMemo, useState } from "react"
import { useToast } from "@chakra-ui/react"
import { UserTableColumn } from "./components/UserTableColumn" import { UserTableColumn } from "./components/UserTableColumn"
import { useRsi } from "../../hooks/useRsi" import { useRsi } from "../../hooks/useRsi"
import { TemplateColumn } from "./components/TemplateColumn" import { TemplateColumn } from "./components/TemplateColumn"
@@ -11,6 +10,7 @@ import { normalizeTableData } from "./utils/normalizeTableData"
import type { Field, RawData } from "../../types" import type { Field, RawData } from "../../types"
import { getMatchedColumns } from "./utils/getMatchedColumns" import { getMatchedColumns } from "./utils/getMatchedColumns"
import { findUnmatchedRequiredFields } from "./utils/findUnmatchedRequiredFields" import { findUnmatchedRequiredFields } from "./utils/findUnmatchedRequiredFields"
import { toast } from "sonner"
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -80,7 +80,6 @@ export const MatchColumnsStep = <T extends string>({
onContinue, onContinue,
onBack, onBack,
}: MatchColumnsProps<T>) => { }: MatchColumnsProps<T>) => {
const toast = useToast()
const dataExample = data.slice(0, 2) const dataExample = data.slice(0, 2)
const { fields, autoMapHeaders, autoMapSelectValues, autoMapDistance, translations, allowInvalidSubmit } = useRsi<T>() const { fields, autoMapHeaders, autoMapSelectValues, autoMapDistance, translations, allowInvalidSubmit } = useRsi<T>()
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
@@ -92,24 +91,24 @@ export const MatchColumnsStep = <T extends string>({
const onChange = useCallback( const onChange = useCallback(
(value: T, columnIndex: number) => { (value: T, columnIndex: number) => {
const field = fields.find((field) => field.key === value) as unknown as Field<T> const field = fields.find((field: Field<T>) => field.key === value)
if (!field) return
const existingFieldIndex = columns.findIndex((column) => "value" in column && column.value === field.key) const existingFieldIndex = columns.findIndex((column) => "value" in column && column.value === field.key)
setColumns( setColumns(
columns.map<Column<T>>((column, index) => { columns.map<Column<T>>((column, index) => {
columnIndex === index ? setColumn(column, field, data) : column
if (columnIndex === index) { if (columnIndex === index) {
// Set the new column value
return setColumn(column, field, data, autoMapSelectValues) return setColumn(column, field, data, autoMapSelectValues)
} else if (index === existingFieldIndex) { } else if (index === existingFieldIndex) {
toast({ // Clear the old column that had this field
status: "warning", toast.warning(translations.matchColumnsStep.duplicateColumnWarningTitle, {
variant: "left-accent",
position: "bottom-left",
title: translations.matchColumnsStep.duplicateColumnWarningTitle,
description: translations.matchColumnsStep.duplicateColumnWarningDescription, description: translations.matchColumnsStep.duplicateColumnWarningDescription,
isClosable: true,
}) })
return setColumn(column) return setColumn(column)
} else { } else {
// Leave other columns unchanged
return column return column
} }
}), }),
@@ -120,7 +119,6 @@ export const MatchColumnsStep = <T extends string>({
columns, columns,
data, data,
fields, fields,
toast,
translations.matchColumnsStep.duplicateColumnWarningDescription, translations.matchColumnsStep.duplicateColumnWarningDescription,
translations.matchColumnsStep.duplicateColumnWarningTitle, translations.matchColumnsStep.duplicateColumnWarningTitle,
], ],

View File

@@ -1,10 +1,8 @@
import type React from "react" import type React from "react"
import type { Column, Columns } from "../MatchColumnsStep" import type { Column, Columns } from "../MatchColumnsStep"
import { Box, Flex, Heading, ModalBody, Text, useStyleConfig } from "@chakra-ui/react"
import { FadingWrapper } from "../../../components/FadingWrapper"
import { ContinueButton } from "../../../components/ContinueButton"
import { useRsi } from "../../../hooks/useRsi" import { useRsi } from "../../../hooks/useRsi"
import type { themeOverrides } from "../../../theme" import { Button } from "@/components/ui/button"
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"
type ColumnGridProps<T extends string> = { type ColumnGridProps<T extends string> = {
columns: Columns<T> columns: Columns<T>
@@ -15,8 +13,6 @@ type ColumnGridProps<T extends string> = {
isLoading: boolean isLoading: boolean
} }
export type Styles = (typeof themeOverrides)["components"]["MatchColumnsStep"]["baseStyle"]
export const ColumnGrid = <T extends string>({ export const ColumnGrid = <T extends string>({
columns, columns,
userColumn, userColumn,
@@ -26,52 +22,82 @@ export const ColumnGrid = <T extends string>({
isLoading, isLoading,
}: ColumnGridProps<T>) => { }: ColumnGridProps<T>) => {
const { translations } = useRsi() const { translations } = useRsi()
const styles = useStyleConfig("MatchColumnsStep") as Styles const columnWidth = 250
const gap = 16
const totalWidth = columns.length * columnWidth + (columns.length - 1) * gap
return ( return (
<> <div className="flex h-[calc(100vh-9.5rem)] flex-col">
<ModalBody flexDir="column" p={8} overflow="auto"> <div className="flex-1 overflow-hidden">
<Heading sx={styles.heading}>{translations.matchColumnsStep.title}</Heading> <div className="px-8 py-6">
<Flex <div className="mb-8">
flex={1} <h2 className="text-3xl font-semibold text-foreground">
display="grid" {translations.matchColumnsStep.title}
gridTemplateRows="auto auto auto 1fr" </h2>
gridTemplateColumns={`0.75rem repeat(${columns.length}, minmax(18rem, auto)) 0.75rem`} </div>
<ScrollArea className="relative">
<div className="space-y-8" style={{ width: totalWidth }}>
{/* Your table section */}
<div>
<h3 className="mb-4 text-lg font-medium text-foreground">
{translations.matchColumnsStep.userTableTitle}
</h3>
<div className="relative">
<div
className="grid gap-4"
style={{
gridTemplateColumns: `repeat(${columns.length}, ${columnWidth}px)`,
}}
> >
<Box gridColumn={`1/${columns.length + 3}`}>
<Text sx={styles.title}>{translations.matchColumnsStep.userTableTitle}</Text>
</Box>
{columns.map((column, index) => ( {columns.map((column, index) => (
<Box gridRow="2/3" gridColumn={`${index + 2}/${index + 3}`} pt={3} key={column.header + index}> <div key={column.header + index} className="relative">
{userColumn(column)} {userColumn(column)}
</Box> </div>
))} ))}
<FadingWrapper gridColumn={`1/${columns.length + 3}`} gridRow="2/3" /> </div>
<Box gridColumn={`1/${columns.length + 3}`} mt={7}> <div className="pointer-events-none absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-b from-transparent to-background" />
<Text sx={styles.title}>{translations.matchColumnsStep.templateTitle}</Text> </div>
</Box> </div>
<FadingWrapper gridColumn={`1/${columns.length + 3}`} gridRow="4/5" />
{columns.map((column, index) => ( {/* Will become section */}
<Box <div>
gridRow="4/5" <h3 className="mb-4 text-lg font-medium text-foreground">
gridColumn={`${index + 2}/${index + 3}`} {translations.matchColumnsStep.templateTitle}
key={column.header + index} </h3>
py="1.125rem" <div
pl={2} className="grid gap-4"
pr={3} style={{
gridTemplateColumns: `repeat(${columns.length}, ${columnWidth}px)`,
}}
> >
{columns.map((column, index) => (
<div key={column.header + index}>
{templateColumn(column)} {templateColumn(column)}
</Box> </div>
))} ))}
</Flex> </div>
</ModalBody> </div>
<ContinueButton </div>
isLoading={isLoading} <ScrollBar orientation="horizontal" />
onContinue={onContinue} </ScrollArea>
onBack={onBack} </div>
title={translations.matchColumnsStep.nextButtonTitle} </div>
backTitle={translations.matchColumnsStep.backButtonTitle} <div className="border-t bg-muted px-8 py-4">
/> <div className="flex items-center justify-between">
</> {onBack && (
<Button variant="outline" onClick={onBack}>
{translations.matchColumnsStep.backButtonTitle}
</Button>
)}
<Button
className="ml-auto"
disabled={isLoading}
onClick={() => onContinue([])}
>
{translations.matchColumnsStep.nextButtonTitle}
</Button>
</div>
</div>
</div>
) )
} }

View File

@@ -1,96 +1,139 @@
import {
Flex,
Text,
Accordion,
AccordionItem,
AccordionButton,
AccordionIcon,
Box,
AccordionPanel,
useStyleConfig,
} from "@chakra-ui/react"
import { useRsi } from "../../../hooks/useRsi" import { useRsi } from "../../../hooks/useRsi"
import type { Column } from "../MatchColumnsStep" import type { Column } from "../MatchColumnsStep"
import { ColumnType } from "../MatchColumnsStep" import { ColumnType } from "../MatchColumnsStep"
import { MatchIcon } from "./MatchIcon" import type { Fields, Field } from "../../../types"
import type { Fields } from "../../../types" import {
import type { Translations } from "../../../translationsRSIProps" Card,
import { MatchColumnSelect } from "../../../components/Selects/MatchColumnSelect" CardContent,
import { SubMatchingSelect } from "./SubMatchingSelect" CardHeader,
import type { Styles } from "./ColumnGrid" } from "@/components/ui/card"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const getAccordionTitle = <T extends string>(fields: Fields<T>, column: Column<T>, translations: Translations) => { type TemplateColumnProps<T extends string> = {
column: Column<T>
onChange: (value: T, columnIndex: number) => void
onSubChange: (value: string, columnIndex: number, entry: string) => void
}
const getAccordionTitle = <T extends string>(fields: Fields<T>, column: Column<T>, translations: any) => {
const fieldLabel = fields.find((field) => "value" in column && field.key === column.value)!.label const fieldLabel = fields.find((field) => "value" in column && field.key === column.value)!.label
return `${translations.matchColumnsStep.matchDropdownTitle} ${fieldLabel} (${ return `${translations.matchColumnsStep.matchDropdownTitle} ${fieldLabel} (${
"matchedOptions" in column && column.matchedOptions.filter((option) => !option.value).length "matchedOptions" in column && column.matchedOptions.filter((option) => !option.value).length
} ${translations.matchColumnsStep.unmatched})` } ${translations.matchColumnsStep.unmatched})`
} }
type TemplateColumnProps<T extends string> = {
onChange: (val: T, index: number) => void
onSubChange: (val: T, index: number, option: string) => void
column: Column<T>
}
export const TemplateColumn = <T extends string>({ column, onChange, onSubChange }: TemplateColumnProps<T>) => { export const TemplateColumn = <T extends string>({ column, onChange, onSubChange }: TemplateColumnProps<T>) => {
const { translations, fields } = useRsi<T>() const { translations, fields } = useRsi<T>()
const styles = useStyleConfig("MatchColumnsStep") as Styles
const isIgnored = column.type === ColumnType.ignored const isIgnored = column.type === ColumnType.ignored
const isChecked = const isChecked =
column.type === ColumnType.matched || column.type === ColumnType.matched ||
column.type === ColumnType.matchedCheckbox || column.type === ColumnType.matchedCheckbox ||
column.type === ColumnType.matchedSelectOptions column.type === ColumnType.matchedSelectOptions
const isSelect = "matchedOptions" in column const isSelect = "matchedOptions" in column
const selectOptions = fields.map(({ label, key }) => ({ value: key, label })) const selectOptions = fields.map(({ label, key }: { label: string; key: string }) => ({ value: key, label }))
const selectValue = selectOptions.find(({ value }) => "value" in column && column.value === value) const selectValue = column.type === ColumnType.empty ? undefined :
selectOptions.find(({ value }: { value: string }) => "value" in column && column.value === value)?.value
if (isIgnored) {
return (
<Card className="h-full opacity-50">
<CardHeader className="p-4">
<p className="text-sm text-muted-foreground">
{translations.matchColumnsStep.ignoredColumnText}
</p>
</CardHeader>
</Card>
)
}
return ( return (
<Flex minH={10} w="100%" flexDir="column" justifyContent="center"> <Card className="h-full">
{isIgnored ? ( <CardHeader className="flex flex-row items-center justify-between space-x-2 p-4">
<Text sx={styles.selectColumn.text}>{translations.matchColumnsStep.ignoredColumnText}</Text> <div className="flex-1">
) : ( <Select
<> key={`select-${column.index}-${("value" in column ? column.value : "empty")}`}
<Flex alignItems="center" minH={10} w="100%">
<Box flex={1}>
<MatchColumnSelect
placeholder={translations.matchColumnsStep.selectPlaceholder}
value={selectValue} value={selectValue}
onChange={(value) => onChange(value?.value as T, column.index)} onValueChange={(value) => onChange(value as T, column.index)}
options={selectOptions}
name={column.header}
/>
</Box>
<MatchIcon isChecked={isChecked} />
</Flex>
{isSelect && (
<Flex width="100%">
<Accordion allowMultiple width="100%">
<AccordionItem border="none" py={1}>
<AccordionButton
_hover={{ bg: "transparent" }}
_focus={{ boxShadow: "none" }}
px={0}
py={4}
data-testid="accordion-button"
> >
<AccordionIcon /> <SelectTrigger className="w-full">
<Box textAlign="left"> <SelectValue placeholder={translations.matchColumnsStep.selectPlaceholder} />
<Text sx={styles.selectColumn.accordionLabel}> </SelectTrigger>
{getAccordionTitle<T>(fields, column, translations)} <SelectContent
</Text> side="bottom"
</Box> align="start"
</AccordionButton> className="z-[1500]"
<AccordionPanel pb={4} pr={3} display="flex" flexDir="column"> >
{column.matchedOptions.map((option) => ( {selectOptions.map((option: { value: string; label: string }) => (
<SubMatchingSelect option={option} column={column} onSubChange={onSubChange} key={option.entry} /> <SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))} ))}
</AccordionPanel> </SelectContent>
</Select>
</div>
{isChecked && (
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10">
<Check className="h-4 w-4 text-primary" />
</div>
)}
</CardHeader>
{isSelect && (
<CardContent className="p-4">
<Accordion type="multiple" className="w-full">
<AccordionItem value="options" className="border-none">
<AccordionTrigger className="py-2 text-sm hover:no-underline">
{getAccordionTitle<T>(fields, column, translations)}
</AccordionTrigger>
<AccordionContent>
<div className="space-y-2">
{column.matchedOptions.map((option) => (
<div key={option.entry} className="space-y-1">
<p className="text-sm text-muted-foreground">
{option.entry}
</p>
<Select
value={option.value}
onValueChange={(value) => onSubChange(value, column.index, option.entry!)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder={translations.matchColumnsStep.subSelectPlaceholder} />
</SelectTrigger>
<SelectContent
side="bottom"
align="start"
className="z-[1000]"
>
{fields
.find((field: Field<T>) => "value" in column && field.key === column.value)
?.fieldType.options.map((option: { value: string; label: string }) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
))}
</div>
</AccordionContent>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
</Flex> </CardContent>
)} )}
</> </Card>
)}
</Flex>
) )
} }

View File

@@ -1,10 +1,10 @@
import { Box, Flex, IconButton, Text, useStyleConfig } from "@chakra-ui/react" import { Button } from "@/components/ui/button"
import { CgClose, CgUndo } from "react-icons/cg" import { Card, CardContent, CardHeader } from "@/components/ui/card"
import { X, Undo2 } from "lucide-react"
import type { Column } from "../MatchColumnsStep" import type { Column } from "../MatchColumnsStep"
import { ColumnType } from "../MatchColumnsStep" import { ColumnType } from "../MatchColumnsStep"
import { dataAttr } from "@chakra-ui/utils"
import type { Styles } from "./ColumnGrid"
import type { RawData } from "../../../types" import type { RawData } from "../../../types"
import { cn } from "@/lib/utils"
type UserTableColumnProps<T extends string> = { type UserTableColumnProps<T extends string> = {
column: Column<T> column: Column<T>
@@ -14,7 +14,6 @@ type UserTableColumnProps<T extends string> = {
} }
export const UserTableColumn = <T extends string>(props: UserTableColumnProps<T>) => { export const UserTableColumn = <T extends string>(props: UserTableColumnProps<T>) => {
const styles = useStyleConfig("MatchColumnsStep") as Styles
const { const {
column: { header, index, type }, column: { header, index, type },
entries, entries,
@@ -22,33 +21,38 @@ export const UserTableColumn = <T extends string>(props: UserTableColumnProps<T>
onRevertIgnore, onRevertIgnore,
} = props } = props
const isIgnored = type === ColumnType.ignored const isIgnored = type === ColumnType.ignored
return ( return (
<Box> <Card className={cn(
<Flex px={6} justifyContent="space-between" alignItems="center" mb={4}> "h-full transition-opacity",
<Text sx={styles.userTable.header} data-ignored={dataAttr(isIgnored)}> isIgnored && "opacity-50"
)}>
<CardHeader className="flex flex-row items-center justify-between space-x-2 p-4">
<h4 className="truncate font-medium leading-none">
{header} {header}
</Text> </h4>
{type === ColumnType.ignored ? ( <Button
<IconButton variant="ghost"
aria-label="Ignore column" size="icon"
icon={<CgUndo />} onClick={() => isIgnored ? onRevertIgnore(index) : onIgnore(index)}
onClick={() => onRevertIgnore(index)} className="h-8 w-8 shrink-0"
{...styles.userTable.ignoreButton} >
/> {isIgnored ? <Undo2 className="h-4 w-4" /> : <X className="h-4 w-4" />}
) : ( </Button>
<IconButton </CardHeader>
aria-label="Ignore column" <CardContent className="space-y-2 p-4">
icon={<CgClose />} {entries.slice(0, 3).map((entry, i) => (
onClick={() => onIgnore(index)} <div
{...styles.userTable.ignoreButton} key={`${entry || ""}-${i}`}
/> className={cn(
"truncate px-3 py-2 text-sm",
isIgnored ? "text-muted-foreground" : "text-foreground"
)} )}
</Flex> >
{entries.map((entry, index) => ( {entry || ""}
<Text key={(entry || "") + index} sx={styles.userTable.cell} data-ignored={dataAttr(isIgnored)}> </div>
{entry}
</Text>
))} ))}
</Box> </CardContent>
</Card>
) )
} }

View File

@@ -44,8 +44,8 @@ export const Steps = () => {
return ( return (
<> <>
<div className="hidden border-b bg-muted px-8 py-6 md:block lg:px-24 xl:px-36"> <div className="hidden border-b bg-muted px-4 py-6 md:block">
<nav className="mx-auto flex items-center justify-between" aria-label="Steps"> <nav className="mx-auto flex items-center justify-center gap-4 lg:gap-24" aria-label="Steps">
{steps.map((key, index) => { {steps.map((key, index) => {
const isActive = index === activeStep const isActive = index === activeStep
const isCompleted = index < activeStep const isCompleted = index < activeStep
@@ -73,15 +73,13 @@ export const Steps = () => {
</span> </span>
)} )}
</div> </div>
<span className={`ml-2 text-sm font-medium w-24 ${ <span className={`ml-2 text-sm font-medium ${
isActive ? 'text-foreground' : 'text-muted-foreground' isActive ? 'text-foreground' : 'text-muted-foreground'
}`}> }`}>
{translations[key].title} {translations[key].title}
</span> </span>
</button> </button>
{index < steps.length - 1 && (
<Separator className="ml-2 mr-0 bg-muted-foreground/80" />
)}
</div> </div>
) )
})} })}

View File

@@ -57,13 +57,13 @@ export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => {
<div <div
{...getRootProps()} {...getRootProps()}
className={cn( className={cn(
"flex h-full w-full flex-1 flex-col items-center justify-center rounded-lg border-2 border-dashed border-muted p-12", "flex h-full w-full flex-1 flex-col items-center justify-center rounded-lg border-2 border-dashed border-secondary-foreground/30 bg-muted/90 p-12",
isDragActive && "border-primary bg-muted/50" isDragActive && "border-primary bg-muted"
)} )}
> >
<input {...getInputProps()} data-testid="rsi-dropzone" /> <input {...getInputProps()} data-testid="rsi-dropzone" />
{isDragActive ? ( {isDragActive ? (
<p className="text-lg text-muted-foreground"> <p className="text-lg text-muted-foreground mb-1 py-6">
{translations.uploadStep.dropzone.activeDropzoneTitle} {translations.uploadStep.dropzone.activeDropzoneTitle}
</p> </p>
) : loading || isLoading ? ( ) : loading || isLoading ? (
@@ -75,7 +75,7 @@ export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => {
<p className="mb-4 text-lg text-muted-foreground"> <p className="mb-4 text-lg text-muted-foreground">
{translations.uploadStep.dropzone.title} {translations.uploadStep.dropzone.title}
</p> </p>
<Button onClick={open} variant="secondary"> <Button onClick={open} variant="default">
{translations.uploadStep.dropzone.buttonTitle} {translations.uploadStep.dropzone.buttonTitle}
</Button> </Button>
</> </>

View File

@@ -311,7 +311,7 @@ export const themeOverrides = {
Modal: { Modal: {
baseStyle: { baseStyle: {
dialog: { dialog: {
borderRadius: "lg", borderRadius: "md",
bg: "background", bg: "background",
fontSize: "lg", fontSize: "lg",
color: "textColor", color: "textColor",
@@ -360,7 +360,7 @@ export const themeOverrides = {
minH: "calc(var(--chakra-vh) - 4rem)", minH: "calc(var(--chakra-vh) - 4rem)",
maxW: "calc(var(--chakra-vw) - 4rem)", maxW: "calc(var(--chakra-vw) - 4rem)",
my: "2rem", my: "2rem",
borderRadius: "3xl", borderRadius: "lg",
overflow: "hidden", overflow: "hidden",
}, },
}, },

View File

@@ -1,14 +1,12 @@
import { ReactSpreadsheetImport } from "@/lib/react-spreadsheet-import/src";
import { useState } from "react"; import { useState } from "react";
import { ReactSpreadsheetImport } from "@/lib/react-spreadsheet-import/src";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Box, Card, ScrollArea } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Code } from "@/components/ui/code"; import { Code } from "@/components/ui/code";
import { toast } from "sonner";
import { motion } from "framer-motion";
export function Import() { const IMPORT_FIELDS = [
const [isOpen, setIsOpen] = useState(false);
const [importedData, setImportedData] = useState<any[] | null>(null);
const fields = [
{ {
label: "Name", label: "Name",
key: "name", key: "name",
@@ -57,43 +55,74 @@ export function Import() {
}, },
], ],
}, },
]; ];
export function Import() {
const [isOpen, setIsOpen] = useState(false);
const [importedData, setImportedData] = useState<any[] | null>(null);
const handleData = async (data: any, file: File) => { const handleData = async (data: any, file: File) => {
try {
console.log("Imported Data:", data); console.log("Imported Data:", data);
console.log("File:", file); console.log("File:", file);
setImportedData(data); setImportedData(data);
setIsOpen(false); setIsOpen(false);
toast.success("Data imported successfully");
} catch (error) {
toast.error("Failed to import data");
console.error("Import error:", error);
}
}; };
return ( return (
<div className="container mx-auto p-4"> <motion.div
<div className="flex justify-between items-center mb-6"> layout
<h1 className="text-2xl font-bold">Import Data</h1> transition={{
<Button onClick={() => setIsOpen(true)}> layout: {
Import Spreadsheet duration: 0.15,
ease: [0.4, 0, 0.2, 1]
}
}}
className="container mx-auto py-6 space-y-4"
>
<motion.div
layout="position"
transition={{ duration: 0.15 }}
className="flex items-center justify-between"
>
<h1 className="text-3xl font-bold tracking-tight">Add New Products</h1>
</motion.div>
<Card className="max-w-[400px]">
<CardHeader>
<CardTitle>Import Data</CardTitle>
</CardHeader>
<CardContent>
<Button onClick={() => setIsOpen(true)} className="w-full">
Upload Spreadsheet
</Button> </Button>
</div> </CardContent>
</Card>
{importedData && ( {importedData && (
<div className="mt-6">
<h2 className="text-xl font-semibold mb-4">Imported Data</h2>
<Card> <Card>
<ScrollArea className="h-[500px] w-full rounded-md border p-4"> <CardHeader>
<Code> <CardTitle>Preview Imported Data</CardTitle>
</CardHeader>
<CardContent>
<Code className="p-4 w-full rounded-md border">
{JSON.stringify(importedData, null, 2)} {JSON.stringify(importedData, null, 2)}
</Code> </Code>
</ScrollArea> </CardContent>
</Card> </Card>
</div>
)} )}
<ReactSpreadsheetImport <ReactSpreadsheetImport
isOpen={isOpen} isOpen={isOpen}
onClose={() => setIsOpen(false)} onClose={() => setIsOpen(false)}
onSubmit={handleData} onSubmit={handleData}
fields={fields} fields={IMPORT_FIELDS}
/> />
</div> </motion.div>
); );
} }

View File

@@ -1,7 +1,7 @@
#!/bin/zsh #!/bin/zsh
#Clear previous mount in case its still there #Clear previous mount in case its still there
umount "/Users/matt/Library/Mobile Documents/com~apple~CloudDocs/Dev/inventory/inventory-server" umount '/Users/matt/Library/Mobile Documents/com~apple~CloudDocs/Dev/inventory/inventory-server'
#Mount #Mount
sshfs matt@dashboard.kent.pw:/var/www/html/inventory -p 22122 "/Users/matt/Library/Mobile Documents/com~apple~CloudDocs/Dev/inventory/inventory-server/" sshfs matt@dashboard.kent.pw:/var/www/html/inventory -p 22122 '/Users/matt/Library/Mobile Documents/com~apple~CloudDocs/Dev/inventory/inventory-server/'