[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.

174 lines
5.8 KiB

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. // file-field.tsx
  2. // Copyright (C) 2020 Dwayne Harris
  3. // This program is free software: you can redistribute it and/or modify
  4. // it under the terms of the GNU General Public License as published by
  5. // the Free Software Foundation, either version 3 of the License, or
  6. // (at your option) any later version.
  7. // This program is distributed in the hope that it will be useful,
  8. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. // GNU General Public License for more details.
  11. // You should have received a copy of the GNU General Public License
  12. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. import React, { FC, ChangeEvent, useState } from 'react'
  14. import { useSelector, useDispatch } from 'react-redux'
  15. import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
  16. import { faUpload } from '@fortawesome/free-solid-svg-icons'
  17. import { BlockBlobClient, AnonymousCredential } from '@azure/storage-blob'
  18. import { useTheme } from '../../hooks'
  19. import { setFieldValue } from '../../actions/forms'
  20. import { showNotification } from '../../actions/notifications'
  21. import { getFieldValue } from '../../selectors/forms'
  22. import { apiFetch } from '../../api/fetch'
  23. import { MEDIA_DEFAULT_MAX_SIZE } from '../../constants'
  24. import { AppState, AppThunkDispatch, SasResponse, NotificationType } from '../../types'
  25. import Progress from '../../components/progress'
  26. import FieldLabel from '../../components/controls/field-label'
  27. import AvatarEditor from '../../components/avatar-editor'
  28. interface Props {
  29. name: string
  30. label: string
  31. width: number
  32. height: number
  33. help?: string
  34. previewWidth?: number
  35. maxSize?: number
  36. }
  37. const FileField: FC<Props> = props => {
  38. const theme = useTheme()
  39. const value = useSelector<AppState, string>(state => getFieldValue<string>(state, props.name, ''))
  40. const dispatch = useDispatch<AppThunkDispatch>()
  41. const [progress, setProgress] = useState(0)
  42. const [file, setFile] = useState<File | undefined>()
  43. const [uploading, setUploading] = useState(false)
  44. const [uploaded, setUploaded] = useState(false)
  45. const {
  46. name,
  47. label,
  48. width,
  49. height,
  50. help,
  51. previewWidth = 128,
  52. maxSize = MEDIA_DEFAULT_MAX_SIZE,
  53. } = props
  54. const handleChange = async (event: ChangeEvent<HTMLInputElement>) => {
  55. if (event.target.files && event.target.files[0]) {
  56. if (event.target.files[0].size > maxSize) {
  57. const maxSizeString = Math.round(maxSize / 1024 / 1024)
  58. dispatch(showNotification(NotificationType.Error, `Files must be less than ${maxSizeString} MBs`))
  59. return
  60. }
  61. setFile(event.target.files[0])
  62. }
  63. }
  64. const handleUpload = async () => {
  65. if (!file) return
  66. setUploading(true)
  67. const ext = file.name.substring(file.name.lastIndexOf('.'))
  68. const { sas, blobUrl, id } = await apiFetch<SasResponse>({ path: '/v1/sas' })
  69. const filename = `${id}${ext}`
  70. const url = `${blobUrl}/${filename}`
  71. try {
  72. const blockBlobClient = new BlockBlobClient(`${url}?${sas}`, new AnonymousCredential())
  73. await blockBlobClient.uploadBrowserData(file, {
  74. onProgress: p => {
  75. setProgress((p.loadedBytes / file.size) * 100)
  76. }
  77. })
  78. await apiFetch({
  79. path: '/v1/media',
  80. method: 'post',
  81. body: {
  82. name: filename,
  83. size: file.size,
  84. type: file.type,
  85. originalName: file.name,
  86. }
  87. })
  88. dispatch(setFieldValue(name, url))
  89. setUploaded(true)
  90. } catch (err) {
  91. console.error(err)
  92. dispatch(showNotification(NotificationType.Error, `Upload error: ${err}`))
  93. }
  94. setUploading(false)
  95. }
  96. const handleDelete = async () => {
  97. if (uploaded) {
  98. await apiFetch({
  99. path: `/v1/media?name=${value}`,
  100. method: 'delete',
  101. })
  102. }
  103. dispatch(setFieldValue(name, ''))
  104. }
  105. if (file) {
  106. return (
  107. <div className="field">
  108. <FieldLabel>{label} (<span style={{ color: theme.red }}>Unsaved</span>)</FieldLabel>
  109. <AvatarEditor
  110. file={file}
  111. width={width}
  112. height={height} />
  113. </div>
  114. )
  115. }
  116. if (uploading) {
  117. return (
  118. <div className="field">
  119. <FieldLabel>{label}</FieldLabel>
  120. <Progress value={progress} />
  121. </div>
  122. )
  123. }
  124. return (
  125. <div className="field">
  126. <FieldLabel>{label}</FieldLabel>
  127. {value &&
  128. <div style={{ padding: '1rem 0px' }}>
  129. <img src={value} style={{ width: previewWidth }} />
  130. <div style={{ color: theme.secondary, fontSize: '0.8rem' }}>
  131. <a style={{ color: theme.red }} onClick={() => handleDelete()}>Delete</a>
  132. </div>
  133. </div>
  134. }
  135. {!value &&
  136. <div>
  137. <label className="file-input" style={{ backgroundColor: theme.primary, color: theme.primaryAlternate }}>
  138. <input type="file" name={name} onChange={handleChange} />
  139. <span className="button-icon">
  140. <FontAwesomeIcon icon={faUpload} />
  141. </span>
  142. <span>Choose a file...</span>
  143. </label>
  144. <p className="help" style={{ color: theme.text }}>{help}</p>
  145. </div>
  146. }
  147. </div>
  148. )
  149. }
  150. export default FileField