Skip to content

Commit

Permalink
Merge branch 'dev' of https://github.com/brainstormforce/force-ui int…
Browse files Browse the repository at this point in the history
…o dev
  • Loading branch information
vrundakansara committed Nov 12, 2024
2 parents f528eaa + aa5ad38 commit 8a1dfcb
Show file tree
Hide file tree
Showing 7 changed files with 360 additions and 3 deletions.
57 changes: 57 additions & 0 deletions src/components/dropzone/dropzone.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Dropzone.stories.js

import Dropzone from './dropzone'; // Adjust the import path as needed
import type { Meta, StoryFn } from '@storybook/react';

const meta: Meta<typeof Dropzone> = {
title: 'Molecules/Dropzone',
component: Dropzone,
parameters: {
layout: 'centered',
},
tags: [ 'autodocs' ],
argTypes: {
size: {
control: { type: 'select' },
},
},
};

export default meta;

const Template: StoryFn<typeof Dropzone> = ( args ) => <Dropzone { ...args } />;

export const Basic = Template.bind( {} );
Basic.args = {
size: 'md',
label: 'Drag and drop or browse files',
helpText: 'Click to upload your files',
inlineIcon: false,
disabled: false,
error: false,
errorText: 'Upload failed, please try again.',
};

export const LargeSize = Template.bind( {} );
LargeSize.args = {
...Basic.args,
size: 'lg',
};

export const DisabledDropzone = Template.bind( {} );
DisabledDropzone.args = {
...Basic.args,
disabled: true,
};

export const InlineIcon = Template.bind( {} );
InlineIcon.args = {
...Basic.args,
inlineIcon: true,
};

export const CustomHelpText = Template.bind( {} );
CustomHelpText.args = {
...Basic.args,
helpText: 'Ensure your files do not exceed 10MB',
};
284 changes: 284 additions & 0 deletions src/components/dropzone/dropzone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
import { useState, createContext, useContext, useRef } from 'react';
import { CloudUpload, File, ImageOff, Trash } from 'lucide-react';
import { cn, formatFileSize } from '@/utilities/functions';
import Loader from '../loader';
import { nanoid } from 'nanoid';

export interface DropzoneProps {
/** Callback function when a file is uploaded */
onFileUpload?: ( file: File ) => void;
/** Determines if the icon should be inline */
inlineIcon?: boolean;
/** Label for the dropzone */
label?: string;
/** Help text for the dropzone */
helpText?: string;
/** Size variant of the dropzone */
size?: 'sm' | 'md' | 'lg';
/** Indicates if the component is disabled */
disabled?: boolean;
/** Indicates if the component is in error state */
error?: boolean;
/** Error text to display */
errorText?: string;
}

// Context interface for file data sharing
export interface FileUploadContextType {
file: File | null;
removeFile: () => void;
isLoading: boolean;
error: boolean;
errorText?: string;
}

// Create a context to share file data between Dropzone and FilePreview
const FileUploadContext = createContext<FileUploadContextType | null>( null );

const useFileUploadContext = () => useContext( FileUploadContext );

// FilePreview
export const FilePreview = () => {
const { file, removeFile, isLoading, error, errorText } =
useFileUploadContext()!;

if ( ! file ) {
return null;
}

return (
<div
className={ cn(
'border border-solid border-transparent flex items-start justify-between rounded mt-2 bg-field-primary-background p-3 gap-3',
error && 'border-alert-border-danger bg-alert-background-danger'
) }
>
<div className="flex items-center gap-3">
{ isLoading && <File className="size-6" /> }
{ ! isLoading &&
( file.type.startsWith( 'image/' ) ? (
<div
className={ cn(
'size-10 rounded-sm flex items-center justify-center shrink-0',
error && 'bg-gray-200 '
) }
>
{ error ? (
<ImageOff className="size-6 text-field-helper" />
) : (
<img
src={ URL.createObjectURL( file ) }
alt="Preview"
className="w-full object-cover"
/>
) }
</div>
) : (
<span>
<File className="size-6 text-icon-primary" />
</span>
) ) }

<div className="text-left flex flex-col">
<span className="text-sm font-medium text-field-label">
{ isLoading ? 'Loading...' : file.name }
</span>
{ ! isLoading && (
<span
className={ cn(
'text-xs text-field-helper',
error && 'text-support-error'
) }
>
{ error ? errorText : formatFileSize( file.size ) }
</span>
) }
</div>
</div>
{ isLoading ? (
<Loader />
) : (
<button
onClick={ removeFile }
className="mt-1.5 cursor-pointer bg-transparent border-0 p-0 m-0 ring-0 focus:outline-none"
>
<Trash className="size-4 text-support-error" />
</button>
) }
</div>
);
};

// Dropzone Component with embedded FilePreview subcomponent
export const Dropzone = ( {
onFileUpload,
inlineIcon = false,
label = 'Drag and drop or browse files',
helpText = 'Help Text',
size = 'sm',
disabled = false,
error = false,
errorText = 'Upload failed, please try again.',
}: DropzoneProps ) => {
const [ isLoading, setIsLoading ] = useState( false );
const [ file, setFile ] = useState<File | null>( null );
const [ isDragging, setIsDragging ] = useState( false );

const handleDrop = ( e: React.DragEvent<HTMLDivElement> ) => {
if ( disabled ) {
return;
}
setIsLoading( true );
e.preventDefault();
e.stopPropagation();
setIsDragging( false );
const droppedFile = e.dataTransfer.files[ 0 ];
if ( droppedFile ) {
setFile( droppedFile );
if ( onFileUpload ) {
onFileUpload( droppedFile );
}
}
setIsLoading( false );
};

const handleDragOver = ( e: React.DragEvent<HTMLDivElement> ) => {
if ( disabled ) {
return;
}
e.preventDefault();
setIsDragging( true );
};

const handleDragLeave = () => {
if ( ! disabled ) {
setIsDragging( false );
}
};

const handleFileChange = ( e: React.ChangeEvent<HTMLInputElement> ) => {
if ( disabled ) {
return;
}
setIsLoading( true );
const files = e.target.files;
if ( ! files ) {
return;
}
const selectedFile = files[ 0 ];
if ( selectedFile ) {
setFile( selectedFile );
if ( onFileUpload ) {
onFileUpload( selectedFile );
}
}
setIsLoading( false );
};

const removeFile = () => {
setFile( null );
};
const sizeClasses = {
sm: {
label: 'text-sm',
helpText: 'text-xs',
icon: 'size-5',
padding: inlineIcon ? 'p-3' : 'p-5',
gap: 'gap-2.5',
},
md: {
label: 'text-sm',
helpText: 'text-xs',
icon: 'size-5',
padding: inlineIcon ? 'p-4' : 'p-6',
gap: 'gap-3',
},
lg: {
label: 'text-base',
helpText: 'text-sm',
icon: 'size-6',
padding: inlineIcon ? 'p-4' : 'p-6',
gap: 'gap-3',
},
};
const uploadInputID = useRef( `fui-file-upload-${ nanoid() }` );
return (
<FileUploadContext.Provider
value={ { file, removeFile, isLoading, error, errorText } }
>
<div>
<label htmlFor={ uploadInputID.current }>
<div
className={ cn(
'min-w-80 cursor-pointer mx-auto border-dotted border-2 rounded-md text-center hover:border-field-dropzone-color hover:bg-field-dropzone-background-hover focus:outline-none focus:ring focus:ring-toggle-on focus:ring-offset-2 transition-[border,box-shadow] duration-300 ease-in-out',
isDragging
? 'border-field-dropzone-color bg-field-dropzone-background-hover'
: 'border-field-border',
disabled &&
'border-field-border bg-field-background-disabled cursor-not-allowed hover:border-field-border',
sizeClasses[ size ].padding
) }
onDragOver={ handleDragOver }
onDragLeave={ handleDragLeave }
onDrop={ handleDrop }
>
<div
className={ cn(
'flex flex-col items-center justify-center',
inlineIcon &&
`flex-row items-start ${ sizeClasses[ size ].gap }`
) }
>
<div>
<CloudUpload
className={ cn(
'text-field-dropzone-color size-6',
sizeClasses[ size ].icon,
disabled && 'text-field-color-disabled'
) }
/>
</div>
<div className="flex flex-col">
<span
className={ cn(
'mt-1 text-center font-medium text-field-label',
inlineIcon && 'text-left mt-0',
sizeClasses[ size ].label,
disabled && 'text-field-color-disabled'
) }
>
{ label }
</span>
{ helpText && (
<span
className={ cn(
'mt-1 text-center font-medium text-field-helper',
inlineIcon && 'text-left',
sizeClasses[ size ].helpText,
disabled &&
'text-field-color-disabled'
) }
>
{ helpText }
</span>
) }
</div>
</div>
<input
id={ uploadInputID.current }
type="file"
className="sr-only"
onChange={ handleFileChange }
disabled={ disabled }
/>
</div>
</label>

<FilePreview />
</div>
</FileUploadContext.Provider>
);
};
Dropzone.displayName = 'Dropzone';

export default Dropzone;
1 change: 1 addition & 0 deletions src/components/dropzone/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './dropzone';
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ export { default as BarChart } from './bar-chart';
export { default as LineChart } from './line-chart';
export { default as PieChart } from './pie-chart';
export { default as AreaChart } from './area-chart';
export { default as Dropzone } from './dropzone';
2 changes: 1 addition & 1 deletion src/components/loader/loader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const Loader = ( {
className = '',
}: LoaderProps ) => {
const variantClassNames = {
primary: 'text-brand-primary-600 bg-background-primary',
primary: 'text-brand-primary-600',
secondary: 'text-background-primary',
}?.[ variant ];

Expand Down
4 changes: 2 additions & 2 deletions src/components/search/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ export const SearchBoxInput = forwardRef<HTMLInputElement, SearchBoxInputProps>(
searchTerm,
setSearchTerm,
} = useSearchContext();
const bagdeSize = size === 'lg' ? 'sm' : 'xs';
const badgeSize = size === 'lg' ? 'sm' : 'xs';
const handleChange = ( event: React.ChangeEvent<HTMLInputElement> ) => {
const newValue = event.target.value;
setSearchTerm!( newValue );
Expand Down Expand Up @@ -303,7 +303,7 @@ export const SearchBoxInput = forwardRef<HTMLInputElement, SearchBoxInputProps>(
/>
<Badge
label={ `⌘/` }
size={ bagdeSize }
size={ badgeSize }
type="rounded"
variant="neutral"
/>
Expand Down
14 changes: 14 additions & 0 deletions src/utilities/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,17 @@ export const getOperatingSystem = () => {
}
return operatingSystem;
};

// Utility function to convert bytes to a more readable format
export const formatFileSize = ( size ) => {
if ( size < 1024 ) {
return `${ size } bytes`;
}
if ( size < 1024 * 1024 ) {
return `${ ( size / 1024 ).toFixed( 2 ) } KB`;
}
if ( size < 1024 * 1024 * 1024 ) {
return `${ ( size / ( 1024 * 1024 ) ).toFixed( 2 ) } MB`;
}
return `${ ( size / ( 1024 * 1024 * 1024 ) ).toFixed( 2 ) } GB`; // Format size to GB
};

0 comments on commit 8a1dfcb

Please sign in to comment.