[ABANDONED] React/Redux front end for the Flexor social network.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

158 lines
5.2 KiB

4 years ago
5 years ago
5 years ago
5 years ago
5 years ago
4 years ago
5 years ago
4 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
4 years ago
5 years ago
5 years ago
5 years ago
4 years ago
5 years ago
4 years ago
5 years ago
5 years ago
4 years ago
5 years ago
4 years ago
5 years ago
4 years ago
5 years ago
5 years ago
4 years ago
5 years ago
4 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
4 years ago
5 years ago
5 years ago
5 years ago
5 years ago
4 years ago
5 years ago
4 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
  1. import React, { FC, ChangeEvent, useState, useRef } from 'react'
  2. import { useSelector, useDispatch } from 'react-redux'
  3. import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
  4. import { faUpload } from '@fortawesome/free-solid-svg-icons'
  5. import { BlockBlobClient, AnonymousCredential } from '@azure/storage-blob'
  6. import { useTheme } from '../../hooks'
  7. import { setFieldValue } from '../../actions/forms'
  8. import { showNotification } from '../../actions/notifications'
  9. import { getFieldValue } from '../../selectors/forms'
  10. import { apiFetch } from '../../api/fetch'
  11. import { MEDIA_DEFAULT_MAX_SIZE } from '../../constants'
  12. import { AppState, AppThunkDispatch, SasResponse, NotificationType } from '../../types'
  13. import Progress from '../../components/progress'
  14. import FieldLabel from '../../components/controls/field-label'
  15. import AvatarEditor from '../../components/avatar-editor'
  16. interface Props {
  17. name: string
  18. label: string
  19. width: number
  20. height: number
  21. help?: string
  22. previewWidth?: number
  23. maxSize?: number
  24. }
  25. const FileField: FC<Props> = props => {
  26. const theme = useTheme()
  27. const value = useSelector<AppState, string>(state => getFieldValue<string>(state, props.name, ''))
  28. const dispatch = useDispatch<AppThunkDispatch>()
  29. const [progress, setProgress] = useState(0)
  30. const [file, setFile] = useState<File | undefined>()
  31. const [uploading, setUploading] = useState(false)
  32. const [uploaded, setUploaded] = useState(false)
  33. const {
  34. name,
  35. label,
  36. width,
  37. height,
  38. help,
  39. previewWidth = 128,
  40. maxSize = MEDIA_DEFAULT_MAX_SIZE,
  41. } = props
  42. const handleChange = async (event: ChangeEvent<HTMLInputElement>) => {
  43. if (event.target.files && event.target.files[0]) {
  44. if (event.target.files[0].size > maxSize) {
  45. const maxSizeString = Math.round(maxSize / 1024 / 1024)
  46. dispatch(showNotification(NotificationType.Error, `Files must be less than ${maxSizeString} MBs`))
  47. return
  48. }
  49. setFile(event.target.files[0])
  50. }
  51. }
  52. const handleUpload = async () => {
  53. if (!file) return
  54. setUploading(true)
  55. const ext = file.name.substring(file.name.lastIndexOf('.'))
  56. const { sas, blobUrl, id } = await apiFetch<SasResponse>({ path: '/v1/sas' })
  57. const filename = `${id}${ext}`
  58. const url = `${blobUrl}/${filename}`
  59. try {
  60. const blockBlobClient = new BlockBlobClient(`${url}?${sas}`, new AnonymousCredential())
  61. await blockBlobClient.uploadBrowserData(file, {
  62. onProgress: p => {
  63. setProgress((p.loadedBytes / file.size) * 100)
  64. }
  65. })
  66. await apiFetch({
  67. path: '/v1/media',
  68. method: 'post',
  69. body: {
  70. name: filename,
  71. size: file.size,
  72. type: file.type,
  73. originalName: file.name,
  74. }
  75. })
  76. dispatch(setFieldValue(name, url))
  77. setUploaded(true)
  78. } catch (err) {
  79. console.error(err)
  80. dispatch(showNotification(NotificationType.Error, `Upload error: ${err}`))
  81. }
  82. setUploading(false)
  83. }
  84. const handleDelete = async () => {
  85. if (uploaded) {
  86. await apiFetch({
  87. path: `/v1/media?name=${value}`,
  88. method: 'delete',
  89. })
  90. }
  91. dispatch(setFieldValue(name, ''))
  92. }
  93. if (file) {
  94. return (
  95. <div className="field">
  96. <FieldLabel>{label} (<span style={{ color: theme.red }}>Unsaved</span>)</FieldLabel>
  97. <AvatarEditor
  98. file={file}
  99. width={width}
  100. height={height} />
  101. </div>
  102. )
  103. }
  104. if (uploading) {
  105. return (
  106. <div className="field">
  107. <FieldLabel>{label}</FieldLabel>
  108. <Progress value={progress} />
  109. </div>
  110. )
  111. }
  112. return (
  113. <div className="field">
  114. <FieldLabel>{label}</FieldLabel>
  115. {value &&
  116. <div style={{ padding: '1rem 0px' }}>
  117. <img src={value} style={{ width: previewWidth }} />
  118. <div style={{ color: theme.secondary, fontSize: '0.8rem' }}>
  119. <a style={{ color: theme.red }} onClick={() => handleDelete()}>Delete</a>
  120. </div>
  121. </div>
  122. }
  123. {!value &&
  124. <div>
  125. <label className="file-input" style={{ backgroundColor: theme.primary, color: theme.primaryAlternate }}>
  126. <input type="file" name={name} onChange={handleChange} />
  127. <span className="button-icon">
  128. <FontAwesomeIcon icon={faUpload} />
  129. </span>
  130. <span>Choose a file...</span>
  131. </label>
  132. <p className="help" style={{ color: theme.text }}>{help}</p>
  133. </div>
  134. }
  135. </div>
  136. )
  137. }
  138. export default FileField