forked from chartdb/chartdb
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(share): add sharing capabilities to import and export diagrams (c…
…hartdb#365) * feat(share): add sharing capabilities to import and export diagrams * remove use client * fix build * add error parse indication * add import from initial dialog * fix build
- Loading branch information
Showing
30 changed files
with
1,377 additions
and
478 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
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 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7', | ||
{ | ||
variants: { | ||
variant: { | ||
default: 'bg-background text-foreground', | ||
destructive: | ||
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive', | ||
}, | ||
}, | ||
defaultVariants: { | ||
variant: 'default', | ||
}, | ||
} | ||
); | ||
|
||
const Alert = React.forwardRef< | ||
HTMLDivElement, | ||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants> | ||
>(({ className, variant, ...props }, ref) => ( | ||
<div | ||
ref={ref} | ||
role="alert" | ||
className={cn(alertVariants({ variant }), className)} | ||
{...props} | ||
/> | ||
)); | ||
Alert.displayName = 'Alert'; | ||
|
||
const AlertTitle = React.forwardRef< | ||
HTMLParagraphElement, | ||
React.HTMLAttributes<HTMLHeadingElement> | ||
>(({ className, ...props }, ref) => ( | ||
<h5 | ||
ref={ref} | ||
className={cn( | ||
'mb-1 font-medium leading-none tracking-tight', | ||
className | ||
)} | ||
{...props} | ||
/> | ||
)); | ||
AlertTitle.displayName = 'AlertTitle'; | ||
|
||
const AlertDescription = React.forwardRef< | ||
HTMLParagraphElement, | ||
React.HTMLAttributes<HTMLParagraphElement> | ||
>(({ className, ...props }, ref) => ( | ||
<div | ||
ref={ref} | ||
className={cn('text-sm [&_p]:leading-relaxed', className)} | ||
{...props} | ||
/> | ||
)); | ||
AlertDescription.displayName = 'AlertDescription'; | ||
|
||
export { Alert, AlertTitle, AlertDescription }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
import React, { useCallback, useEffect, useState } from 'react'; | ||
import { Upload, FileIcon, AlertCircle, Trash2 } from 'lucide-react'; | ||
import { Button } from '../button/button'; | ||
|
||
interface FileWithPreview extends File { | ||
preview?: string; | ||
} | ||
|
||
export interface FileUploaderProps { | ||
onFilesChange?: (files: File[]) => void; | ||
multiple?: boolean; | ||
supportedExtensions?: string[]; | ||
} | ||
|
||
export const FileUploader: React.FC<FileUploaderProps> = ({ | ||
onFilesChange, | ||
multiple, | ||
supportedExtensions, | ||
}) => { | ||
const [files, setFiles] = useState<FileWithPreview[]>([]); | ||
const [isDragging, setIsDragging] = useState(false); | ||
const [error, setError] = useState<string | null>(null); | ||
|
||
const isFileSupported = useCallback( | ||
(file: File) => { | ||
if (!supportedExtensions) return true; | ||
const fileExtension = file.name.split('.').pop()?.toLowerCase(); | ||
return fileExtension | ||
? supportedExtensions.includes(`.${fileExtension}`) | ||
: false; | ||
}, | ||
[supportedExtensions] | ||
); | ||
|
||
const handleFiles = useCallback( | ||
(selectedFiles: FileList) => { | ||
const newFiles = Array.from(selectedFiles) | ||
.filter((file) => { | ||
if (!isFileSupported(file)) { | ||
setError( | ||
`File type not supported. Supported types: ${supportedExtensions?.join(', ')}` | ||
); | ||
return false; | ||
} | ||
return true; | ||
}) | ||
.map((file) => | ||
Object.assign(file, { preview: URL.createObjectURL(file) }) | ||
); | ||
|
||
if (newFiles.length === 0) return; | ||
|
||
setError(null); | ||
setFiles((prevFiles) => { | ||
if (multiple) { | ||
return [...prevFiles, ...newFiles]; | ||
} else { | ||
return newFiles.slice(0, 1); | ||
} | ||
}); | ||
}, | ||
[multiple, supportedExtensions, isFileSupported] | ||
); | ||
|
||
const onDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => { | ||
e.preventDefault(); | ||
setIsDragging(true); | ||
}, []); | ||
|
||
const onDragLeave = useCallback((e: React.DragEvent<HTMLDivElement>) => { | ||
e.preventDefault(); | ||
setIsDragging(false); | ||
}, []); | ||
|
||
const onDrop = useCallback( | ||
(e: React.DragEvent<HTMLDivElement>) => { | ||
e.preventDefault(); | ||
setIsDragging(false); | ||
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { | ||
handleFiles(e.dataTransfer.files); | ||
} | ||
}, | ||
[handleFiles] | ||
); | ||
|
||
useEffect(() => { | ||
if (onFilesChange) { | ||
onFilesChange(files.length > 0 ? files : []); | ||
} | ||
}, [files, onFilesChange]); | ||
|
||
const removeFile = useCallback((fileToRemove: File) => { | ||
setFiles((prevFiles) => | ||
prevFiles.filter((file) => file !== fileToRemove) | ||
); | ||
}, []); | ||
|
||
return ( | ||
<div className="mx-auto w-full max-w-md"> | ||
<div | ||
onDragOver={onDragOver} | ||
onDragLeave={onDragLeave} | ||
onDrop={onDrop} | ||
className={`cursor-pointer rounded-lg border-2 border-dashed p-8 text-center transition-colors ${ | ||
isDragging | ||
? 'border-primary bg-primary/10 dark:bg-primary/20' | ||
: 'border-gray-300 hover:border-primary dark:border-gray-600 dark:hover:border-primary' | ||
}`} | ||
> | ||
<input | ||
type="file" | ||
multiple={multiple} | ||
onChange={(e) => | ||
e.target.files && handleFiles(e.target.files) | ||
} | ||
className="hidden" | ||
id="fileInput" | ||
accept={supportedExtensions?.join(',')} | ||
/> | ||
<label htmlFor="fileInput" className="cursor-pointer"> | ||
<Upload className="mx-auto size-12 text-gray-400 dark:text-gray-500" /> | ||
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400"> | ||
{multiple | ||
? 'Drag and drop files here or click to select' | ||
: 'Drag and drop a file here or click to select'} | ||
</p> | ||
{supportedExtensions ? ( | ||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400"> | ||
Supported types: {supportedExtensions.join(', ')} | ||
</p> | ||
) : null} | ||
</label> | ||
</div> | ||
|
||
{error ? ( | ||
<div className="mt-4 flex items-center rounded-lg bg-red-100 p-3 text-red-700 dark:bg-red-900 dark:text-red-200"> | ||
<AlertCircle className="mr-2 size-5" /> | ||
<span className="text-sm">{error}</span> | ||
</div> | ||
) : null} | ||
|
||
{files.length > 0 ? ( | ||
<ul className="mt-4 space-y-4"> | ||
{files.map((file) => ( | ||
<li | ||
key={file.name} | ||
className="flex items-center justify-between rounded-lg bg-gray-100 p-3 dark:bg-gray-800" | ||
> | ||
<div className="flex min-w-0 flex-1 items-center space-x-2"> | ||
<FileIcon className="size-5 text-primary" /> | ||
<span className="truncate text-sm font-medium text-gray-700 dark:text-gray-300"> | ||
{file.name} | ||
</span> | ||
</div> | ||
<Button | ||
variant="ghost" | ||
className="size-5 p-0 hover:bg-primary-foreground" | ||
onClick={() => removeFile(file)} | ||
> | ||
<Trash2 className="size-3.5 text-red-700" /> | ||
</Button> | ||
</li> | ||
))} | ||
</ul> | ||
) : null} | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.