From edbfdd7e907ee6a02e9b04a1ce53ca204d5f4f0d Mon Sep 17 00:00:00 2001 From: jackcruz53 Date: Mon, 8 May 2023 18:58:50 -0700 Subject: [PATCH 1/7] -WIP for the photo input editor --- package.json | 1 + src/components/photo_input.tsx | 181 +++++++++++++++------------------ src/components/root_layout.tsx | 8 +- yarn.lock | 5 + 4 files changed, 94 insertions(+), 101 deletions(-) diff --git a/package.json b/package.json index 7144b425..a117e2bb 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "react-refresh": "^0.11.0", "react-router-dom": "^6.8.1", "react-simple-code-editor": "^0.13.1", + "react-uuid": "^2.0.0", "remark": "^14.0.2", "remark-gfm": "^3.0.1", "resolve": "^1.20.0", diff --git a/src/components/photo_input.tsx b/src/components/photo_input.tsx index 85dc769c..8aad78f5 100644 --- a/src/components/photo_input.tsx +++ b/src/components/photo_input.tsx @@ -1,127 +1,114 @@ -import ImageBlobReduce from 'image-blob-reduce' -import {isEmpty} from 'lodash' -import React, {ChangeEvent, FC, MouseEvent, useEffect, useRef, useState} from 'react' -import {Button, Card, Image} from 'react-bootstrap' -import {TfiGallery} from 'react-icons/tfi' +import ImageBlobReduce from 'image-blob-reduce'; +import {isEmpty} from 'lodash'; +import React, {ChangeEvent, FC, MouseEvent, useEffect, useRef, useState} from 'react'; +import {Button, Card, Image} from 'react-bootstrap'; +import {TfiGallery} from 'react-icons/tfi'; +import Collapsible from './collapsible'; +import GpsCoordStr from './gps_coord_str'; +import PhotoMetaData from '../types/photo_metadata.type'; +import uuid from 'react-uuid'; -import Collapsible from './collapsible' -import GpsCoordStr from './gps_coord_str' -import PhotoMetaData from '../types/photo_metadata.type' +interface Photo { + id: string; + blob: Blob; + note?: string; +} interface PhotoInputProps { - children: React.ReactNode, - label: string, - metadata: PhotoMetaData, - photo: Blob | undefined, - upsertPhoto: (file: Blob) => void, + children: React.ReactNode; + label: string; + metadata: PhotoMetaData; + photos: Photo[]; + upsertPhoto: (photo: Photo) => void; + deletePhoto: (id: string) => void; + maxPhotos?: number; } -// TODO: Determine whether or not the useEffect() method is needed. -// We don't seem to need a separate camera button on an Android phone. -// However, we may need to request access to the camera -// before it can me used. Then clean up the corresponding code that is currently -// commented out. - /** * Component for photo input * * @param children Content (most commonly markdown text) describing the photo requirement * @param label Label for the photo requirement - * @param metadata Abreviated photo metadata including timestamp and geolocation - * @param photo Blob containing the photo itself + * @param metadata Abbreviated photo metadata including timestamp and geolocation + * @param photos An array of photos with associated metadata * @param upsertPhoto Function used to update/insert a photo into the store + * @param deletePhoto Function used to delete a photo from the store + * @param maxPhotos Maximum number of photos that can be added (default: 1) */ -const PhotoInput: FC = ({children, label, metadata, photo, upsertPhoto}) => { - // Create references to the hidden file inputs - const hiddenPhotoCaptureInputRef = useRef(null) - const hiddenPhotoUploadInputRef = useRef(null) +const PhotoInput: FC = ({ + children, + label, + photos, + upsertPhoto, + deletePhoto, + maxPhotos = 1, +}) => { - const [cameraAvailable, setCameraAvailable] = useState(false) + const handleFileupload = async (event: ChangeEvent) => { + if (event.target.files && event.target.files.length > 0) { + const photo = event.target.files[0]; + const imageBlobReduce = new ImageBlobReduce(); + const blob = await imageBlobReduce.toBlob(photo); + const id = uuid(); + upsertPhoto({id, blob}); - // Handle button clicks - const handlePhotoCaptureButtonClick = (event: MouseEvent) => { - hiddenPhotoCaptureInputRef.current && hiddenPhotoCaptureInputRef.current.click() - } - const handlePhotoGalleryButtonClick = (event: MouseEvent) => { - hiddenPhotoUploadInputRef.current && hiddenPhotoUploadInputRef.current.click() - } - - useEffect(() => { - if(navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { - navigator.mediaDevices.getUserMedia({ video: true }) - .then(() => { - setCameraAvailable(true) - }); + event.target.value = ''; } + }; - }) - - const handleFileInputChange = (event: ChangeEvent) => { - if (event.target.files) { - const file = event.target.files[0] - upsertPhoto(file) + const handleNoteChange = (id: string, event: ChangeEvent) => { + const note = event.target.value; + const photoIndex = photos.findIndex(photo => photo.id === id); + if (photoIndex >= 0) { + const updatedPhoto = {...photos[photoIndex], note}; + upsertPhoto(updatedPhoto); } - } + }; + return ( <> - {/* Card.Text renders a

by defult. The children come from markdown + {/* Card.Text renders a

by default. The children come from markdown and may be a

. Nested

s are not allowed, so we use a

*/} - - {children} - + {children} + {/* Display the photos */} + {photos.map((photo, index) => ( +
+ {/* keeping blank so that it */} +
- {/* {(cameraAvailable || cameraAvailable) && - - } */} - + {/* Input for the note */} + handleNoteChange(id, event)} + /> + {/* Delete button */} +
- {/* */} - - {photo && ( - <> - -
- - Timestamp: { - metadata?.timestamp ? ({metadata.timestamp}) : - (Missing) - } -
- Geolocation: { - metadata?.geolocation?.latitude && metadata?.geolocation?.latitude?.deg.toString() !== 'NaN' && - metadata?.geolocation?.longitude && metadata?.geolocation?.longitude?.deg.toString() !== 'NaN' ? - : - Missing - } -
- - )} - - - - ); +
+ ))} + + {/* Display the "Add Photo" button if maxPhotos is not reached */} + {maxPhotos > photos.length && ( +
+ +
+ )} + + + +); }; -export default PhotoInput +export default PhotoInput; \ No newline at end of file diff --git a/src/components/root_layout.tsx b/src/components/root_layout.tsx index 2d5e6791..c22a40c3 100644 --- a/src/components/root_layout.tsx +++ b/src/components/root_layout.tsx @@ -2,7 +2,7 @@ import {FC} from 'react' import Container from 'react-bootstrap/Container'; import Image from 'react-bootstrap/Image' -import NavBar from 'react-bootstrap/NavBar' +import Navbar from 'react-bootstrap/Navbar' interface RootLayoutProps { children: React.ReactNode, @@ -20,11 +20,11 @@ const RootLayout: FC = ({children}) => { return (
{/* TODO: Add a menu */} - + - Quality Install Tool + Quality Install Tool - +
{children}
diff --git a/yarn.lock b/yarn.lock index fddebe18..744cae15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9036,6 +9036,11 @@ react-transition-group@^4.4.2: loose-envify "^1.4.0" prop-types "^15.6.2" +react-uuid@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/react-uuid/-/react-uuid-2.0.0.tgz#e3c97e190db9eef53cb9dacfcc7314d913a411fa" + integrity sha512-FNUH/8WR/FEtx0Bu6gmt1eONfc413hhvrEXFWUSFGvznUhI4dYoVZA09p7JHoTpnM4WC2D/bG2YSxGKXF4oVLg== + react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" From 7753d2f385dff2009521d770cf686742f689620c Mon Sep 17 00:00:00 2001 From: jackcruz53 Date: Mon, 8 May 2023 19:00:28 -0700 Subject: [PATCH 2/7] -lint fixes --- src/components/photo_input.tsx | 68 +++++++++++++++++----------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/src/components/photo_input.tsx b/src/components/photo_input.tsx index 8aad78f5..d41f1a8a 100644 --- a/src/components/photo_input.tsx +++ b/src/components/photo_input.tsx @@ -1,28 +1,28 @@ -import ImageBlobReduce from 'image-blob-reduce'; -import {isEmpty} from 'lodash'; -import React, {ChangeEvent, FC, MouseEvent, useEffect, useRef, useState} from 'react'; -import {Button, Card, Image} from 'react-bootstrap'; -import {TfiGallery} from 'react-icons/tfi'; +import ImageBlobReduce from 'image-blob-reduce' +import {isEmpty} from 'lodash' +import React, {ChangeEvent, FC, MouseEvent, useEffect, useRef, useState} from 'react' +import {Button, Card, Image} from 'react-bootstrap' +import {TfiGallery} from 'react-icons/tfi' -import Collapsible from './collapsible'; -import GpsCoordStr from './gps_coord_str'; -import PhotoMetaData from '../types/photo_metadata.type'; -import uuid from 'react-uuid'; +import Collapsible from './collapsible' +import GpsCoordStr from './gps_coord_str' +import PhotoMetaData from '../types/photo_metadata.type' +import uuid from 'react-uuid' interface Photo { - id: string; - blob: Blob; - note?: string; + id: string + blob: Blob + note?: string } interface PhotoInputProps { - children: React.ReactNode; - label: string; - metadata: PhotoMetaData; - photos: Photo[]; - upsertPhoto: (photo: Photo) => void; - deletePhoto: (id: string) => void; - maxPhotos?: number; + children: React.ReactNode + label: string + metadata: PhotoMetaData + photos: Photo[] + upsertPhoto: (photo: Photo) => void + deletePhoto: (id: string) => void + maxPhotos?: number } /** @@ -47,24 +47,24 @@ const PhotoInput: FC = ({ const handleFileupload = async (event: ChangeEvent) => { if (event.target.files && event.target.files.length > 0) { - const photo = event.target.files[0]; - const imageBlobReduce = new ImageBlobReduce(); - const blob = await imageBlobReduce.toBlob(photo); - const id = uuid(); - upsertPhoto({id, blob}); + const photo = event.target.files[0] + const imageBlobReduce = new ImageBlobReduce() + const blob = await imageBlobReduce.toBlob(photo) + const id = uuid() + upsertPhoto({id, blob}) - event.target.value = ''; + event.target.value = '' } - }; + } const handleNoteChange = (id: string, event: ChangeEvent) => { - const note = event.target.value; - const photoIndex = photos.findIndex(photo => photo.id === id); + const note = event.target.value + const photoIndex = photos.findIndex(photo => photo.id === id) if (photoIndex >= 0) { - const updatedPhoto = {...photos[photoIndex], note}; - upsertPhoto(updatedPhoto); + const updatedPhoto = {...photos[photoIndex], note} + upsertPhoto(updatedPhoto) } - }; + } return ( @@ -108,7 +108,7 @@ const PhotoInput: FC = ({ -); -}; +) +} -export default PhotoInput; \ No newline at end of file +export default PhotoInput \ No newline at end of file From f15c2f9e3f789b9f84aa14ce0ca5c9c33d3dd84e Mon Sep 17 00:00:00 2001 From: jackcruz53 Date: Mon, 12 Jun 2023 14:14:13 -0700 Subject: [PATCH 3/7] -wip --- src/components/photo_input.tsx | 5 +++-- src/components/photo_input_wrapper.tsx | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/photo_input.tsx b/src/components/photo_input.tsx index d41f1a8a..9bfece09 100644 --- a/src/components/photo_input.tsx +++ b/src/components/photo_input.tsx @@ -16,10 +16,11 @@ interface Photo { } interface PhotoInputProps { + id: string, children: React.ReactNode label: string metadata: PhotoMetaData - photos: Photo[] + photos: [{id: string, photo: Photo }] upsertPhoto: (photo: Photo) => void deletePhoto: (id: string) => void maxPhotos?: number @@ -37,6 +38,7 @@ interface PhotoInputProps { * @param maxPhotos Maximum number of photos that can be added (default: 1) */ const PhotoInput: FC = ({ + id, children, label, photos, @@ -44,7 +46,6 @@ const PhotoInput: FC = ({ deletePhoto, maxPhotos = 1, }) => { - const handleFileupload = async (event: ChangeEvent) => { if (event.target.files && event.target.files.length > 0) { const photo = event.target.files[0] diff --git a/src/components/photo_input_wrapper.tsx b/src/components/photo_input_wrapper.tsx index f9f105d6..e0a86417 100644 --- a/src/components/photo_input_wrapper.tsx +++ b/src/components/photo_input_wrapper.tsx @@ -29,12 +29,12 @@ const PhotoInputWrapper: FC = ({children, id, label}) => {({attachments, upsertAttachment}) => { - const upsertPhoto = (file: Blob) => { + const upsertPhoto = (file: Blob, photoId: string) => { // Reduce the image size as needed ImageBlobReduce() .toBlob(file, {max: MAX_IMAGE_DIM}) .then(blob => { - upsertAttachment(blob, id) + upsertAttachment(blob, photoId) }) } From e8fa000af865ae9f08beb5d768dec29858451a1b Mon Sep 17 00:00:00 2001 From: jackcruz53 Date: Thu, 15 Jun 2023 11:07:08 -0700 Subject: [PATCH 4/7] wip --- src/components/photo_input.tsx | 161 +++++++++++-------------- src/components/photo_input_wrapper.tsx | 12 +- 2 files changed, 82 insertions(+), 91 deletions(-) diff --git a/src/components/photo_input.tsx b/src/components/photo_input.tsx index 9bfece09..6dc30506 100644 --- a/src/components/photo_input.tsx +++ b/src/components/photo_input.tsx @@ -1,115 +1,100 @@ -import ImageBlobReduce from 'image-blob-reduce' -import {isEmpty} from 'lodash' -import React, {ChangeEvent, FC, MouseEvent, useEffect, useRef, useState} from 'react' -import {Button, Card, Image} from 'react-bootstrap' -import {TfiGallery} from 'react-icons/tfi' - +import React, { ChangeEvent, FC, MouseEvent, useEffect, useRef, useState } from 'react' +import { Button, Card, Image } from 'react-bootstrap' +import { TfiCamera, TfiGallery } from 'react-icons/tfi' import Collapsible from './collapsible' import GpsCoordStr from './gps_coord_str' import PhotoMetaData from '../types/photo_metadata.type' -import uuid from 'react-uuid' +import ImageBlobReduce from 'image-blob-reduce' +import Photo from './photo' +import PhotoMetadata from '../types/photo_metadata.type' -interface Photo { - id: string - blob: Blob - note?: string -} interface PhotoInputProps { - id: string, children: React.ReactNode label: string - metadata: PhotoMetaData - photos: [{id: string, photo: Photo }] - upsertPhoto: (photo: Photo) => void - deletePhoto: (id: string) => void - maxPhotos?: number + photos: {id: string, data: {blob: Blob, metadata: PhotoMetadata}}[] + upsertPhoto: (file: Blob, id: string) => void + id: string } -/** - * Component for photo input - * - * @param children Content (most commonly markdown text) describing the photo requirement - * @param label Label for the photo requirement - * @param metadata Abbreviated photo metadata including timestamp and geolocation - * @param photos An array of photos with associated metadata - * @param upsertPhoto Function used to update/insert a photo into the store - * @param deletePhoto Function used to delete a photo from the store - * @param maxPhotos Maximum number of photos that can be added (default: 1) - */ -const PhotoInput: FC = ({ - id, - children, - label, - photos, - upsertPhoto, - deletePhoto, - maxPhotos = 1, -}) => { - const handleFileupload = async (event: ChangeEvent) => { - if (event.target.files && event.target.files.length > 0) { - const photo = event.target.files[0] - const imageBlobReduce = new ImageBlobReduce() - const blob = await imageBlobReduce.toBlob(photo) - const id = uuid() - upsertPhoto({id, blob}) - - event.target.value = '' +const PhotoInput: FC = ({ children, label, photos, upsertPhoto, id }) => { + const hiddenPhotoUploadInputRef = useRef(null) + const hiddenCameraInputRef = useRef(null) + const photoIds = [photos.map((photo) => photo.id.split('~').pop())] + const [photoId, setPhotoId] = useState(photoIds.length > 0 ? Math.max(photoIds as any as number) : 0) + console.log(photoId) + const handlePhotoGalleryButtonClick = (event: MouseEvent) => { + if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { + navigator.mediaDevices.getUserMedia({ video: true }) } + hiddenPhotoUploadInputRef.current && hiddenPhotoUploadInputRef.current.click() + } + + const handleCameraButtonClick = (event: MouseEvent) => { + hiddenCameraInputRef.current && hiddenCameraInputRef.current.click() } - const handleNoteChange = (id: string, event: ChangeEvent) => { - const note = event.target.value - const photoIndex = photos.findIndex(photo => photo.id === id) - if (photoIndex >= 0) { - const updatedPhoto = {...photos[photoIndex], note} - upsertPhoto(updatedPhoto) + const handleFileInputChange = async (event: ChangeEvent) => { + if (event.target.files && event.target.files.length > 0) { + const files = Array.from(event.target.files) + for (const file of files) { + const imageBlobReduce = new ImageBlobReduce() + setPhotoId(photoId + 1) + const blob = await imageBlobReduce.toBlob(file) + const blobId = `${id}~${photoId.toString()}` + upsertPhoto(blob, blobId) + } + + event.target.value = '' } } - return ( <> - + - {/* Card.Text renders a

by default. The children come from markdown - and may be a

. Nested

s are not allowed, so we use a

*/} {children} - {/* Display the photos */} - {photos.map((photo, index) => ( -
- {/* keeping blank so that it */} -
- {/* Input for the note */} - handleNoteChange(id, event)} - /> - {/* Delete button */} - + )} +
-
- ))} - - {/* Display the "Add Photo" button if maxPhotos is not reached */} - {maxPhotos > photos.length && ( -
- -
- )} - - - -) + + + {photos.length > 0 && ( + <> + {photos.map((photo, index) => ( +
+ +
+ ))} + + )} + + + + ) } -export default PhotoInput \ No newline at end of file +export default PhotoInput diff --git a/src/components/photo_input_wrapper.tsx b/src/components/photo_input_wrapper.tsx index e0a86417..cf8a51e7 100644 --- a/src/components/photo_input_wrapper.tsx +++ b/src/components/photo_input_wrapper.tsx @@ -1,5 +1,5 @@ import ImageBlobReduce from 'image-blob-reduce' -import React, {FC} from 'react' +import React, {FC, useState} from 'react' import {StoreContext} from './store' import PhotoInput from './photo_input' @@ -14,6 +14,12 @@ interface PhotoInputWrapperProps { label: string } +const filterAttachmentsByIdPrefix = (attachments, idPrefix: string)=> { + return Object.entries(attachments).filter((attachment: any) => attachment[0].startsWith(idPrefix)).map((attachment: any) => { + return {id: attachment[0], data: attachment[1] } + }) +} + /** * A component that wraps a PhotoInput component in order to tie it to the data store * @@ -37,11 +43,11 @@ const PhotoInputWrapper: FC = ({children, id, label}) => upsertAttachment(blob, photoId) }) } + const photos = filterAttachmentsByIdPrefix(attachments, id) return ( ) }} From 5541d2cf7a4395c074358c570efd828b38d7e90b Mon Sep 17 00:00:00 2001 From: jackcruz53 Date: Fri, 16 Jun 2023 11:32:10 -0700 Subject: [PATCH 5/7] enables multiple photo upload and deleting photos for the photo input component --- src/components/photo.tsx | 18 ++++++++++-- src/components/photo_input.tsx | 12 ++++---- src/components/photo_input_wrapper.tsx | 6 ++-- src/components/store.tsx | 38 +++++++++++++++++++++++++- 4 files changed, 62 insertions(+), 12 deletions(-) diff --git a/src/components/photo.tsx b/src/components/photo.tsx index da9fcd02..ff54d039 100644 --- a/src/components/photo.tsx +++ b/src/components/photo.tsx @@ -1,6 +1,6 @@ import {isEmpty} from 'lodash' import React, {FC, useEffect, useState} from 'react' -import {Button, Card, Image} from 'react-bootstrap' +import {Button, Card, Image, Modal, ModalBody, Popover} from 'react-bootstrap' import GpsCoordStr from './gps_coord_str' @@ -13,6 +13,7 @@ interface PhotoProps { metadata: PhotoMetadata, photo: Blob | undefined, required: boolean, + deletePhoto: (id: string) => void, } /** @@ -27,8 +28,8 @@ interface PhotoProps { * photo attachement in the data store with the given id. When set, the Photo component * will always show and the Photo component will indicate when the photo is missing. */ -const Photo: FC = ({description, label, metadata, photo, required}) => { - +const Photo: FC = ({description, label, metadata, photo, required, deletePhoto, id}) => { + const [showDeleteModal, setShowDeleteModal] = useState(false) return (photo || required)? ( <> @@ -55,7 +56,18 @@ const Photo: FC = ({description, label, metadata, photo, required}) : Missing } +
+ + + +

Are you sure you want to delete this photo?

+
+ + +
+
+
) : required && ( Missing Photo diff --git a/src/components/photo_input.tsx b/src/components/photo_input.tsx index 6dc30506..0940979a 100644 --- a/src/components/photo_input.tsx +++ b/src/components/photo_input.tsx @@ -2,8 +2,6 @@ import React, { ChangeEvent, FC, MouseEvent, useEffect, useRef, useState } from import { Button, Card, Image } from 'react-bootstrap' import { TfiCamera, TfiGallery } from 'react-icons/tfi' import Collapsible from './collapsible' -import GpsCoordStr from './gps_coord_str' -import PhotoMetaData from '../types/photo_metadata.type' import ImageBlobReduce from 'image-blob-reduce' import Photo from './photo' import PhotoMetadata from '../types/photo_metadata.type' @@ -14,15 +12,15 @@ interface PhotoInputProps { label: string photos: {id: string, data: {blob: Blob, metadata: PhotoMetadata}}[] upsertPhoto: (file: Blob, id: string) => void + removeAttachment: (id: string) => void id: string } -const PhotoInput: FC = ({ children, label, photos, upsertPhoto, id }) => { +const PhotoInput: FC = ({ children, label, photos, upsertPhoto, removeAttachment, id }) => { const hiddenPhotoUploadInputRef = useRef(null) const hiddenCameraInputRef = useRef(null) const photoIds = [photos.map((photo) => photo.id.split('~').pop())] const [photoId, setPhotoId] = useState(photoIds.length > 0 ? Math.max(photoIds as any as number) : 0) - console.log(photoId) const handlePhotoGalleryButtonClick = (event: MouseEvent) => { if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { navigator.mediaDevices.getUserMedia({ video: true }) @@ -49,6 +47,10 @@ const PhotoInput: FC = ({ children, label, photos, upsertPhoto, } } + const handleFileDelete = (id: string) => { + removeAttachment(id) + } + return ( <> @@ -86,7 +88,7 @@ const PhotoInput: FC = ({ children, label, photos, upsertPhoto, <> {photos.map((photo, index) => (
- +
))} diff --git a/src/components/photo_input_wrapper.tsx b/src/components/photo_input_wrapper.tsx index cf8a51e7..a2246041 100644 --- a/src/components/photo_input_wrapper.tsx +++ b/src/components/photo_input_wrapper.tsx @@ -14,7 +14,7 @@ interface PhotoInputWrapperProps { label: string } -const filterAttachmentsByIdPrefix = (attachments, idPrefix: string)=> { +const filterAttachmentsByIdPrefix = (attachments: { [s: string]: unknown } | ArrayLike, idPrefix: string)=> { return Object.entries(attachments).filter((attachment: any) => attachment[0].startsWith(idPrefix)).map((attachment: any) => { return {id: attachment[0], data: attachment[1] } }) @@ -33,7 +33,7 @@ const PhotoInputWrapper: FC = ({children, id, label}) => return ( - {({attachments, upsertAttachment}) => { + {({attachments, upsertAttachment, removeAttachment}) => { const upsertPhoto = (file: Blob, photoId: string) => { // Reduce the image size as needed @@ -47,7 +47,7 @@ const PhotoInputWrapper: FC = ({children, id, label}) => return ( ) }} diff --git a/src/components/store.tsx b/src/components/store.tsx index 187af238..1ff265dd 100644 --- a/src/components/store.tsx +++ b/src/components/store.tsx @@ -15,6 +15,10 @@ interface UpsertAttachment { (blob: Blob, id: string): void; } +interface RemoveAttachment { + (id: string): void; +} + interface UpsertData { (pathStr: string, data: any): void; } @@ -24,6 +28,7 @@ export const StoreContext = React.createContext({ doc: {} as JSONValue, upsertAttachment: ((blob: Blob, id: any) => {}) as UpsertAttachment, upsertData: ((pathStr: string, data: any) => {}) as UpsertData, + removeAttachment: ((id: string) => {}) as RemoveAttachment, }); @@ -263,8 +268,39 @@ export const StoreProvider: FC = ({ children, dbName, docId } }; + const removeAttachment: RemoveAttachment = async (id: string) => { + // Remove the attachment from memory + const newAttachments = { ...attachments } + delete newAttachments[id] + setAttachments(newAttachments) + + // Remove the attachment from the database + const removeBlobDB = async (rev: string): Promise => { + let result = null + if (db) { + try { + result = await db.removeAttachment(docId, id, rev) + } catch (err) { + // Try again with the latest rev value + const doc = await db.get(docId) + result = await removeBlobDB(doc._rev) + } finally { + if (result) { + revisionRef.current = result.rev + } + } + } + return result + } + + if (revisionRef.current) { + await removeBlobDB(revisionRef.current) + } + }; + + return ( - + {children} ); From f9715ce232283df99f5c5665f06d93eb7efa8d3d Mon Sep 17 00:00:00 2001 From: jackcruz53 Date: Wed, 21 Jun 2023 10:15:13 -0700 Subject: [PATCH 6/7] -prevent desktop from trying to access camera --- src/components/photo_input.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/photo_input.tsx b/src/components/photo_input.tsx index 0940979a..0183bcd5 100644 --- a/src/components/photo_input.tsx +++ b/src/components/photo_input.tsx @@ -16,11 +16,13 @@ interface PhotoInputProps { id: string } + const PhotoInput: FC = ({ children, label, photos, upsertPhoto, removeAttachment, id }) => { const hiddenPhotoUploadInputRef = useRef(null) const hiddenCameraInputRef = useRef(null) const photoIds = [photos.map((photo) => photo.id.split('~').pop())] const [photoId, setPhotoId] = useState(photoIds.length > 0 ? Math.max(photoIds as any as number) : 0) + const isMobile = /Mobi/.test(navigator.userAgent) const handlePhotoGalleryButtonClick = (event: MouseEvent) => { if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { navigator.mediaDevices.getUserMedia({ video: true }) @@ -59,7 +61,7 @@ const PhotoInput: FC = ({ children, label, photos, upsertPhoto, {children}
- {( + {( isMobile && From 50a812e7951b0f9dcb65e2b4679ec4613bbbd2cd Mon Sep 17 00:00:00 2001 From: jackcruz53 Date: Wed, 21 Jun 2023 10:33:53 -0700 Subject: [PATCH 7/7] Allows photo component to display multiple photos and adapt formatting --- src/components/photo.tsx | 8 ++++---- src/components/photo_input_wrapper.tsx | 2 +- src/components/photo_wrapper.tsx | 20 ++++++++++++++++---- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/components/photo.tsx b/src/components/photo.tsx index ff54d039..2dee6f47 100644 --- a/src/components/photo.tsx +++ b/src/components/photo.tsx @@ -13,7 +13,7 @@ interface PhotoProps { metadata: PhotoMetadata, photo: Blob | undefined, required: boolean, - deletePhoto: (id: string) => void, + deletePhoto?: (id: string) => void, } /** @@ -57,9 +57,9 @@ const Photo: FC = ({description, label, metadata, photo, required, d Missing }
- + {deletePhoto && } - + {deletePhoto &&

Are you sure you want to delete this photo?

@@ -67,7 +67,7 @@ const Photo: FC = ({description, label, metadata, photo, required, d
-
+
} ) : required && ( Missing Photo diff --git a/src/components/photo_input_wrapper.tsx b/src/components/photo_input_wrapper.tsx index a2246041..c7575a72 100644 --- a/src/components/photo_input_wrapper.tsx +++ b/src/components/photo_input_wrapper.tsx @@ -14,7 +14,7 @@ interface PhotoInputWrapperProps { label: string } -const filterAttachmentsByIdPrefix = (attachments: { [s: string]: unknown } | ArrayLike, idPrefix: string)=> { +export const filterAttachmentsByIdPrefix = (attachments: { [s: string]: unknown } | ArrayLike, idPrefix: string)=> { return Object.entries(attachments).filter((attachment: any) => attachment[0].startsWith(idPrefix)).map((attachment: any) => { return {id: attachment[0], data: attachment[1] } }) diff --git a/src/components/photo_wrapper.tsx b/src/components/photo_wrapper.tsx index 411ef31e..f20a1694 100644 --- a/src/components/photo_wrapper.tsx +++ b/src/components/photo_wrapper.tsx @@ -3,6 +3,7 @@ import React, {FC} from 'react' import {StoreContext} from './store' import Photo from './photo' import PhotoMetadata from '../types/photo_metadata.type' +import { filterAttachmentsByIdPrefix } from './photo_input_wrapper' interface PhotoWrapperProps { @@ -29,11 +30,22 @@ const PhotoWrapper: FC = ({children, id, label, required}) => return ( {({attachments, doc}) => { + const photos = filterAttachmentsByIdPrefix(attachments, id) + return ( - +
+ { photos.map((photo, index) => { + return( +
1 ? '50%' : '100%' }}> + +
+ ) + }) + } +
) }}