FileDropdown.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import React, { useState } from 'react';
  2. import { FormikProps } from 'formik';
  3. import { Icon, Loader } from 'semantic-ui-react';
  4. import Dropzone from 'react-dropzone';
  5. enum Status {
  6. Accepted = 'accepted',
  7. Rejected = 'rejected',
  8. Active = 'active',
  9. Parsing = 'parsing',
  10. Default = 'default'
  11. }
  12. const determineStatus = (
  13. acceptedFiles: File[],
  14. rejectedFiles: File[],
  15. error: string | undefined,
  16. isDragActive: boolean,
  17. parsing: boolean
  18. ): Status => {
  19. if (parsing) return Status.Parsing;
  20. if (error || rejectedFiles.length) return Status.Rejected;
  21. if (acceptedFiles.length) return Status.Accepted;
  22. if (isDragActive) return Status.Active;
  23. return Status.Default;
  24. };
  25. // Get color by status (imporant to use #FFFFFF format, so we can easily swicth the opacity!)
  26. const getStatusColor = (status: Status): string => {
  27. switch (status) {
  28. case Status.Accepted:
  29. return '#00DBB0';
  30. case Status.Rejected:
  31. return '#FF3861';
  32. case Status.Active:
  33. case Status.Parsing:
  34. return '#000000';
  35. default:
  36. return '#333333';
  37. }
  38. };
  39. const dropdownDivStyle = (status: Status): React.CSSProperties => {
  40. const mainColor = getStatusColor(status);
  41. return {
  42. cursor: 'pointer',
  43. border: `1px solid ${mainColor + '30'}`,
  44. borderRadius: '3px',
  45. padding: '1.5em',
  46. color: mainColor,
  47. fontWeight: 'bold',
  48. transition: 'color 0.5s, border-color 0.5s'
  49. };
  50. };
  51. const dropdownIconStyle = (): React.CSSProperties => {
  52. return {
  53. marginRight: '0.5em',
  54. opacity: 0.5
  55. };
  56. };
  57. const innerSpanStyle = (): React.CSSProperties => {
  58. return {
  59. display: 'flex',
  60. alignItems: 'center'
  61. };
  62. };
  63. // Interpret the file as a UTF-8 string
  64. // https://developer.mozilla.org/en-US/docs/Web/API/Blob/text
  65. const parseFileAsUtf8 = async (file: any): Promise<string> => {
  66. const text = await file.text();
  67. return text;
  68. };
  69. // Interpret the file as containing binary data. This will load the entire
  70. // file into memory which may crash the brower with very large files.
  71. const parseFileAsBinary = async (file: any): Promise<ArrayBuffer> => {
  72. // return file.arrayBuffer();
  73. // This newer API not fully supported yet in all browsers
  74. // https://developer.mozilla.org/en-US/docs/Web/API/Blob/arrayBuffer
  75. return new Promise((resolve): void => {
  76. const reader = new FileReader();
  77. reader.onload = ({ target }: ProgressEvent<FileReader>): void => {
  78. if (target && target.result) {
  79. resolve(target.result as ArrayBuffer);
  80. }
  81. };
  82. reader.readAsArrayBuffer(file);
  83. });
  84. };
  85. type FileDropdownProps<FormValuesT> = {
  86. error: string | undefined;
  87. name: keyof FormValuesT & string;
  88. setFieldValue: FormikProps<FormValuesT>['setFieldValue'];
  89. setFieldTouched: FormikProps<FormValuesT>['setFieldTouched'];
  90. acceptedFormats: string | string[];
  91. defaultText: string;
  92. interpretAs: 'utf-8' | 'binary';
  93. };
  94. export default function FileDropdown<ValuesT = {}> (props: FileDropdownProps<ValuesT>) {
  95. const [parsing, setParsing] = useState(false);
  96. const { error, name, setFieldValue, setFieldTouched, acceptedFormats, defaultText, interpretAs } = props;
  97. return (
  98. <Dropzone
  99. onDropAccepted={async acceptedFiles => {
  100. setParsing(true);
  101. let contents;
  102. if (interpretAs === 'utf-8') {
  103. contents = await parseFileAsUtf8(acceptedFiles[0]);
  104. } else {
  105. contents = await parseFileAsBinary(acceptedFiles[0]);
  106. }
  107. setFieldValue(name, contents, true);
  108. setFieldTouched(name, true);
  109. setParsing(false);
  110. }}
  111. multiple={false}
  112. accept={acceptedFormats}
  113. >
  114. {({ getRootProps, getInputProps, acceptedFiles, rejectedFiles, isDragActive }) => {
  115. const status = determineStatus(acceptedFiles, rejectedFiles, error, isDragActive, parsing);
  116. return (
  117. <section>
  118. <div {...getRootProps({ style: dropdownDivStyle(status) })}>
  119. <input {...getInputProps()} />
  120. {
  121. <span style={innerSpanStyle()}>
  122. <Icon name="cloud upload" size="huge" style={dropdownIconStyle()} />
  123. <p>
  124. {status === Status.Parsing && (
  125. <>
  126. <Loader style={{ marginRight: '0.5em' }} size="small" inline active /> Uploading...
  127. </>
  128. )}
  129. {status === Status.Rejected && (
  130. <>
  131. {error || 'This is not a correct file!'}
  132. <br />
  133. </>
  134. )}
  135. {status === Status.Accepted && (
  136. <>
  137. {`Current file: ${acceptedFiles[0].name}`}
  138. <br />
  139. </>
  140. )}
  141. {status !== Status.Parsing && defaultText}
  142. </p>
  143. </span>
  144. }
  145. </div>
  146. </section>
  147. );
  148. }}
  149. </Dropzone>
  150. );
  151. }