From 3eeb92e8445944ddc810f8da710e303865c7d1f3 Mon Sep 17 00:00:00 2001 From: jackcruz53 Date: Thu, 3 Aug 2023 08:39:28 -0700 Subject: [PATCH 1/3] overhauls the photo and photoInput component --- src/App.css | 6 ++ src/components/jobs_view.tsx | 10 +- src/components/photo.tsx | 87 +++++++++++++++++- src/components/photo_input.tsx | 111 +++++++++++++++-------- src/components/photo_input_wrapper.tsx | 35 +++++-- src/components/photo_wrapper.tsx | 41 ++++++--- src/components/print_section wrapper.tsx | 47 +++++----- src/components/print_section.tsx | 83 +++++++++-------- src/components/store.tsx | 84 +++++++++++++++++ src/types/photo_metadata.type.ts | 1 + 10 files changed, 381 insertions(+), 124 deletions(-) diff --git a/src/App.css b/src/App.css index 63bf1b00..335b478e 100644 --- a/src/App.css +++ b/src/App.css @@ -156,4 +156,10 @@ h3 { /* stylizes tab component here to avoid putting styles in editor*/ .tab-content { margin-top: 1rem; +} + +.photo-card-header { + display: flex; + justify-content: space-between; + align-items: center; } \ No newline at end of file diff --git a/src/components/jobs_view.tsx b/src/components/jobs_view.tsx index 26af9fb4..3b7f61d1 100644 --- a/src/components/jobs_view.tsx +++ b/src/components/jobs_view.tsx @@ -131,7 +131,13 @@ const JobList: React.FC = ({ dbName }) => { const name = input if (name !== null) { const date = new Date() - await putNewDoc(db, name, date, dbName, templatesConfig[dbName].title) + await putNewDoc( + db, + name, + date, + dbName, + templatesConfig[dbName].title, + ) } // Refresh the job list after adding the new job await retrieveJobs() @@ -144,7 +150,7 @@ const JobList: React.FC = ({ dbName }) => { const doc = await db.get(jobId) await db.remove(doc) // Remove the existing document doc._id = newName // Set the new name as the ID - doc.metadata_.project_name = input; + doc.metadata_.project_name = input await db.putIfNotExists(doc) } diff --git a/src/components/photo.tsx b/src/components/photo.tsx index 640403d8..68c417b2 100644 --- a/src/components/photo.tsx +++ b/src/components/photo.tsx @@ -1,10 +1,12 @@ -import React from 'react' +import React, { useState } from 'react' import type { FC } from 'react' -import { Card, Image } from 'react-bootstrap' +import { Button, Card, Image, Modal, ModalBody, Popover } from 'react-bootstrap' import DateTimeStr from './date_time_str' import GpsCoordStr from './gps_coord_str' import type PhotoMetadata from '../types/photo_metadata.type' +import { debounce } from 'lodash' +import TextInput from './text_input' interface PhotoProps { description: React.ReactNode @@ -13,6 +15,9 @@ interface PhotoProps { metadata: PhotoMetadata photo: Blob | undefined required: boolean + deletePhoto?: (id: string) => void + updateNotes?: (id: string, notes: string) => void + count?: string } /** @@ -33,12 +38,24 @@ const Photo: FC = ({ metadata, photo, required, + deletePhoto, + updateNotes, + id, + count, }) => { + const [showDeleteModal, setShowDeleteModal] = useState(false) + const [notes, setNotes] = useState(metadata.notes || '') + const handleNotesChange = (input: string) => { + setNotes(input) + debounce(() => updateNotes && updateNotes(id, notes), 500) + } return photo || required ? ( <> - {label} +
+ {label} {count} +
{/* Card.Text renders a

by defult. The description comes from markdown and may be a

. Nested

s are not allowed, so we use a

*/} {description} @@ -62,7 +79,71 @@ const Photo: FC = ({ />{' '} } + {(updateNotes || metadata.notes) && ( + + )} + {deletePhoto && ( + + )} + {deletePhoto && ( + + +

+ 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 ea02af8a..2e5188a6 100644 --- a/src/components/photo_input.tsx +++ b/src/components/photo_input.tsx @@ -3,16 +3,19 @@ import type { ChangeEvent, FC, MouseEvent } from 'react' import { Button, Card, Image } from 'react-bootstrap' import { TfiGallery } from 'react-icons/tfi' import Collapsible from './collapsible' -import DateTimeStr from './date_time_str' -import GpsCoordStr from './gps_coord_str' -import type PhotoMetaData from '../types/photo_metadata.type' +import Photo from './photo' +import PhotoMetadata from '../types/photo_metadata.type' +import ImageBlobReduce from 'image-blob-reduce' interface PhotoInputProps { children: React.ReactNode label: string - metadata: PhotoMetaData - photo: Blob | undefined - upsertPhoto: (file: Blob) => void + photos: { id: string; data: { blob: Blob; metadata: PhotoMetadata } }[] + upsertPhoto: (file: Blob, id: string) => void + removeAttachment: (id: string) => void + id: string + maxPhotos?: number + updateNotes?: (id: string, notes: string) => void } // TODO: Determine whether or not the useEffect() method is needed. @@ -33,16 +36,28 @@ interface PhotoInputProps { const PhotoInput: FC = ({ children, label, - metadata, - photo, + photos, upsertPhoto, + removeAttachment, + maxPhotos = 1, + id, + updateNotes, }) => { // Create references to the hidden file inputs const hiddenPhotoCaptureInputRef = useRef(null) 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 [cameraAvailable, setCameraAvailable] = useState(false) + if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { + navigator.mediaDevices.getUserMedia({ video: true }) + } + // Handle button clicks const handlePhotoCaptureButtonClick = ( event: MouseEvent, @@ -65,18 +80,31 @@ const PhotoInput: FC = ({ } }) - const handleFileInputChange = (event: ChangeEvent) => { - if (event.target.files) { - const file = event.target.files[0] - upsertPhoto(file) + 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 = '' } } + const handleFileDelete = (id: string) => { + removeAttachment(id) + } + // Check if there is already a photo - const hasPhoto = !!photo + const hasPhoto = !!photos.length // Button text based on whether there is a photo or not - const buttonText = hasPhoto ? 'Replace Photo' : 'Add Photo' + const buttonText = 'Add Photo' return ( <> @@ -93,12 +121,6 @@ const PhotoInput: FC = ({ variant="outline-primary"> Camera } */} -
{/* = ({ type="file" capture="environment" /> - {photo && ( + {photos.length && ( <> - -
- - Timestamp:{' '} - {metadata?.timestamp ? ( - - ) : ( - Missing - )} -
- Geolocation:{' '} - { - - {' '} - - } -
+ {photos.map((photo, index) => ( +
+ +
+ ))} )} + {photos.length <= maxPhotos && ( + + )}
diff --git a/src/components/photo_input_wrapper.tsx b/src/components/photo_input_wrapper.tsx index f4ef65e7..81cc75c5 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' @@ -13,6 +13,17 @@ interface PhotoInputWrapperProps { label: 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] } + }) +} + /** * A component that wraps a PhotoInput component in order to tie it to the data store * @@ -29,25 +40,29 @@ const PhotoInputWrapper: FC = ({ }) => { return ( - {({ attachments, upsertAttachment }) => { - const upsertPhoto = (img_file: Blob) => { + {({ + attachments, + upsertAttachment, + removeAttachment, + updateAttachmentMetaData, + }) => { + const upsertPhoto = (img_file: Blob, photoId: string) => { // Reduce the image size as needed ImageBlobReduce() .toBlob(img_file, { max: MAX_IMAGE_DIM }) .then(blob => { - upsertAttachment(blob, id) + upsertAttachment(blob, photoId) }) } - + const photos = filterAttachmentsByIdPrefix(attachments, id) return ( {children} diff --git a/src/components/photo_wrapper.tsx b/src/components/photo_wrapper.tsx index 8a503a5e..cdde9028 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 { children: React.ReactNode @@ -32,18 +33,36 @@ const PhotoWrapper: FC = ({ return ( {({ attachments, data }) => { + const photos = filterAttachmentsByIdPrefix(attachments, id) return ( - +
+ {photos.map((photo, index) => { + return ( +
+ +
+ ) + })} +
) }}
diff --git a/src/components/print_section wrapper.tsx b/src/components/print_section wrapper.tsx index b8ab12d8..1b6b4504 100644 --- a/src/components/print_section wrapper.tsx +++ b/src/components/print_section wrapper.tsx @@ -1,34 +1,39 @@ -import React, {FC} from 'react' +import React, { FC } from 'react' -import {StoreContext} from './store' +import { StoreContext } from './store' import PrintSection from './print_section' interface PrintSectionWrapperProps { - children: React.ReactNode, - label: string, + children: React.ReactNode + label: string } /** * A component that wraps a PrintSection component in order to tie it to the data store - * + * * @param children Content (most commonly markdown text) to be passed on as the PhotInput * children * @param label The button label of the PrintSection component */ -const PrintSectionWrapper: FC = ({children, label}) => { - - // Generate an id for the input - return ( - - {({metadata}) => { - return ( - - ) - } - } - - -)} - +const PrintSectionWrapper: FC = ({ + children, + label, +}) => { + // Generate an id for the input + return ( + + {({ metadata }) => { + return ( + + ) + }} + + ) +} -export default PrintSectionWrapper \ No newline at end of file +export default PrintSectionWrapper diff --git a/src/components/print_section.tsx b/src/components/print_section.tsx index 859ab128..722e6670 100644 --- a/src/components/print_section.tsx +++ b/src/components/print_section.tsx @@ -1,49 +1,58 @@ -import {useEffect, useId, useRef} from 'react' +import { useEffect, useId, useRef } from 'react' import print from 'print-js' -import React, {FC, ReactNode} from 'react' +import React, { FC, ReactNode } from 'react' import Button from 'react-bootstrap/Button' - interface PrintSectionProps { - children: ReactNode, - label: string - project_name: string, - workflow_name: string, + children: ReactNode + label: string + project_name: string + workflow_name: string } /** * Component with a print button for printing the component's child content - * - * @param children Content for printing + * + * @param children Content for printing * @param label Label for the print button */ -const PrintSection: FC = ({children, label, project_name, workflow_name}) => { - const printContainerId = useId(); - return ( - <> - -
-
- {children} -
-
- - ) +const PrintSection: FC = ({ + children, + label, + project_name, + workflow_name, +}) => { + const printContainerId = useId() + return ( + <> + +
+
{children}
+
+ + ) } -export default PrintSection \ No newline at end of file +export default PrintSection diff --git a/src/components/store.tsx b/src/components/store.tsx index c2a0fe90..0ccc4f74 100644 --- a/src/components/store.tsx +++ b/src/components/store.tsx @@ -19,6 +19,8 @@ import { putNewDoc } from '../utilities/database_utils' PouchDB.plugin(PouchDBUpsert) type UpsertAttachment = (blob: Blob, id: string) => void +type RemoveAttachment = (id: string) => void +type UpdateAttachmentMetaData = (id: string, metadata: any) => void type UpsertData = (pathStr: string, data: any) => void @@ -35,6 +37,11 @@ export const StoreContext = React.createContext({ data: {} satisfies JSONValue, metadata: {} satisfies Metadata | undefined, upsertAttachment: ((blob: Blob, id: any) => {}) as UpsertAttachment, + removeAttachment: ((id: string) => {}) as RemoveAttachment, + updateAttachmentMetaData: (( + id: string, + metadata: any, + ) => {}) as UpdateAttachmentMetaData, upsertData: ((pathStr: string, data: any) => {}) as UpsertData, upsertDoc: ((pathStr: string, data: any) => {}) as UpsertDoc, }) @@ -340,6 +347,81 @@ export const StoreProvider: FC = ({ } } + 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) + } + } + + const updateAttachmentMetaData = async (id: string, metadata: any) => { + // Update the attachment metadata in memory + const newAttachments = { + ...attachments, + [id]: { + ...attachments[id], + metadata, + }, + } + setAttachments(newAttachments) + + // Update the attachment metadata in the database + const updateBlobDB = async ( + rev: string, + ): Promise => { + let result = null + if (db != null) { + try { + result = await db.putAttachment( + docId, + id, + rev, + attachments[id].blob, + attachments[id].blob.type, + ) + } catch (err) { + // Try again with the latest rev value + const doc = await db.get(docId) + result = await updateBlobDB(doc._rev) + } finally { + if (result != null) { + revisionRef.current = result.rev + } + } + } + return result + } + + if (revisionRef.current) { + updateBlobDB(revisionRef.current) + } + } + return ( = ({ data, metadata, upsertAttachment, + removeAttachment, + updateAttachmentMetaData, upsertData, upsertDoc, }} diff --git a/src/types/photo_metadata.type.ts b/src/types/photo_metadata.type.ts index 4b5c0555..c8ef572c 100644 --- a/src/types/photo_metadata.type.ts +++ b/src/types/photo_metadata.type.ts @@ -6,6 +6,7 @@ interface PhotoMetadata { } // TODO: replace string with more precise type timestamp: string + notes?: string | null } export default PhotoMetadata From 287f8d81bbe5323349b569bafe78d4cb4796c767 Mon Sep 17 00:00:00 2001 From: jackcruz53 Date: Thu, 3 Aug 2023 10:44:24 -0700 Subject: [PATCH 2/3] wip --- src/components/photo.tsx | 4 +- src/components/photo_input_wrapper.tsx | 4 +- src/components/store.tsx | 54 ++++---------------------- src/components/text_input.tsx | 4 ++ 4 files changed, 16 insertions(+), 50 deletions(-) diff --git a/src/components/photo.tsx b/src/components/photo.tsx index 68c417b2..8fcc7109 100644 --- a/src/components/photo.tsx +++ b/src/components/photo.tsx @@ -7,6 +7,7 @@ import GpsCoordStr from './gps_coord_str' import type PhotoMetadata from '../types/photo_metadata.type' import { debounce } from 'lodash' import TextInput from './text_input' +import { pathToId } from '../utilities/paths_utils' interface PhotoProps { description: React.ReactNode @@ -47,7 +48,7 @@ const Photo: FC = ({ const [notes, setNotes] = useState(metadata.notes || '') const handleNotesChange = (input: string) => { setNotes(input) - debounce(() => updateNotes && updateNotes(id, notes), 500) + updateNotes && updateNotes(`${id}.notes`, input) } return photo || required ? ( <> @@ -88,6 +89,7 @@ const Photo: FC = ({ min={0} max={280} regexp={/^[a-zA-Z0-9\s]*$/} + disabled={!updateNotes} /> )} {deletePhoto && ( diff --git a/src/components/photo_input_wrapper.tsx b/src/components/photo_input_wrapper.tsx index 81cc75c5..337f5bb3 100644 --- a/src/components/photo_input_wrapper.tsx +++ b/src/components/photo_input_wrapper.tsx @@ -44,7 +44,7 @@ const PhotoInputWrapper: FC = ({ attachments, upsertAttachment, removeAttachment, - updateAttachmentMetaData, + upsertMetaData, }) => { const upsertPhoto = (img_file: Blob, photoId: string) => { // Reduce the image size as needed @@ -62,7 +62,7 @@ const PhotoInputWrapper: FC = ({ upsertPhoto={upsertPhoto} id={id} removeAttachment={removeAttachment} - updateNotes={updateAttachmentMetaData} + updateNotes={upsertMetaData} > {children} diff --git a/src/components/store.tsx b/src/components/store.tsx index 0ccc4f74..871c1c18 100644 --- a/src/components/store.tsx +++ b/src/components/store.tsx @@ -20,7 +20,7 @@ PouchDB.plugin(PouchDBUpsert) type UpsertAttachment = (blob: Blob, id: string) => void type RemoveAttachment = (id: string) => void -type UpdateAttachmentMetaData = (id: string, metadata: any) => void +type UpsertMetaData = (id: string, metadata: any) => void type UpsertData = (pathStr: string, data: any) => void @@ -38,10 +38,7 @@ export const StoreContext = React.createContext({ metadata: {} satisfies Metadata | undefined, upsertAttachment: ((blob: Blob, id: any) => {}) as UpsertAttachment, removeAttachment: ((id: string) => {}) as RemoveAttachment, - updateAttachmentMetaData: (( - id: string, - metadata: any, - ) => {}) as UpdateAttachmentMetaData, + upsertMetaData: ((id: string, metadata: any) => {}) as UpsertMetaData, upsertData: ((pathStr: string, data: any) => {}) as UpsertData, upsertDoc: ((pathStr: string, data: any) => {}) as UpsertDoc, }) @@ -379,47 +376,10 @@ export const StoreProvider: FC = ({ } } - const updateAttachmentMetaData = async (id: string, metadata: any) => { - // Update the attachment metadata in memory - const newAttachments = { - ...attachments, - [id]: { - ...attachments[id], - metadata, - }, - } - setAttachments(newAttachments) - - // Update the attachment metadata in the database - const updateBlobDB = async ( - rev: string, - ): Promise => { - let result = null - if (db != null) { - try { - result = await db.putAttachment( - docId, - id, - rev, - attachments[id].blob, - attachments[id].blob.type, - ) - } catch (err) { - // Try again with the latest rev value - const doc = await db.get(docId) - result = await updateBlobDB(doc._rev) - } finally { - if (result != null) { - revisionRef.current = result.rev - } - } - } - return result - } - - if (revisionRef.current) { - updateBlobDB(revisionRef.current) - } + const upsertMetaData = (path: string, metadata: any) => { + path = 'metadata_.attachments.' + path + console.log(metadata, path, doc) + upsertDoc(path, metadata) } return ( @@ -430,7 +390,7 @@ export const StoreProvider: FC = ({ metadata, upsertAttachment, removeAttachment, - updateAttachmentMetaData, + upsertMetaData, upsertData, upsertDoc, }} diff --git a/src/components/text_input.tsx b/src/components/text_input.tsx index 9a16ef76..dc613f9d 100644 --- a/src/components/text_input.tsx +++ b/src/components/text_input.tsx @@ -10,6 +10,7 @@ interface TextInputProps { min: number max: number regexp: RegExp + disabled?: boolean } /** @@ -32,6 +33,7 @@ const TextInput: FC = ({ min, max, regexp, + disabled, }) => { const [error, setError] = useState('') @@ -59,6 +61,8 @@ const TextInput: FC = ({ placeholder="A placeholder" value={value || ''} isInvalid={Boolean(error)} + disabled={disabled} + draggable={!disabled} /> {error && ( From 41a70256c09661bca5cd92e80915cf2441d89795 Mon Sep 17 00:00:00 2001 From: jackcruz53 Date: Tue, 8 Aug 2023 15:10:03 -0700 Subject: [PATCH 3/3] -Adds multi photo input component -Adds multi photo display -Adds delete button for photos -Adds notes to photos and photo input -Adds photo numbering to display --- public/print.css | 35 +++++++ src/App.css | 39 +++++++ src/components/notes.tsx | 41 ++++++++ src/components/photo.tsx | 121 ++++++---------------- src/components/photo_input.tsx | 134 +++++++++++++++++++------ src/components/photo_input_wrapper.tsx | 2 + src/components/photo_wrapper.tsx | 21 ++-- src/components/store.tsx | 4 +- 8 files changed, 258 insertions(+), 139 deletions(-) create mode 100644 src/components/notes.tsx diff --git a/public/print.css b/public/print.css index abd76314..0e1b372f 100644 --- a/public/print.css +++ b/public/print.css @@ -175,4 +175,39 @@ h3 { /* stylizes tab component here to avoid putting styles in editor*/ .tab-content { margin-top: 1rem; +} + +.photo-card-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.photo-input-delete-button { + cursor: 'pointer'; + border-width: '1px'; + border-radius: '6px'; +} + +.delete-modal-delete { + cursor: 'pointer'; + color: 'red'; + border-width: '1px'; + border-radius: '6px'; +} + +.photo-input-photos-container { + display: 'flex'; + flex-wrap: 'wrap'; +} + +.delete-modal-container { + display: 'flex'; + align-items: 'center'; + justify-content: 'space-between'; +} + +.report-print-photo-notes { + display: flex; + flex-direction: column; } \ No newline at end of file diff --git a/src/App.css b/src/App.css index 99af7dbc..b5e43d8a 100644 --- a/src/App.css +++ b/src/App.css @@ -181,4 +181,43 @@ h3 { display: flex; justify-content: space-between; align-items: center; +} + +.photo-input-delete-button { + cursor: 'pointer'; + border-width: '1px'; + border-radius: '6px'; +} + +.delete-modal-delete { + cursor: 'pointer'; + color: 'red'; + border-width: '1px'; + border-radius: '6px'; +} + +.photo-input-photos-container { + display: 'flex'; + flex-wrap: 'wrap'; +} + +.delete-modal-container { + display: 'flex'; + align-items: 'center'; + justify-content: 'space-between'; +} + +.report-print-photo-notes { + display: flex; + flex-direction: column; +} + +.photo-wrapper { + display: 'flex'; + flex-wrap: 'wrap'; +} + +.photo-display-container { + display: flex; + width: fit-content; } \ No newline at end of file diff --git a/src/components/notes.tsx b/src/components/notes.tsx new file mode 100644 index 00000000..54b951c7 --- /dev/null +++ b/src/components/notes.tsx @@ -0,0 +1,41 @@ +import { FC, useState } from "react"; +import TextInput from "./text_input" +import { pathToId } from "../utilities/paths_utils"; + +interface NotesInputProps { + value?: string | null, + updateValue: ((id: string, notes: string) => void) | undefined; + id: string, +} +const NotesInput: FC =({ + value, + updateValue, + id +}) => { + const [notes, updateNotes] = useState(value) + const notesId = `${id}.notes` + const handleNotesChange = (inputValue: string) => { + updateNotes(inputValue) + updateValue && updateValue(notesId, inputValue) + } + return ( + { + handleNotesChange(value) + }} + min={0} + max={280} + regexp={/.*/} + disabled={ + !updateValue + } + /> + ) +} + +export default NotesInput diff --git a/src/components/photo.tsx b/src/components/photo.tsx index 8fcc7109..2f834f15 100644 --- a/src/components/photo.tsx +++ b/src/components/photo.tsx @@ -1,11 +1,10 @@ -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import type { FC } from 'react' import { Button, Card, Image, Modal, ModalBody, Popover } from 'react-bootstrap' import DateTimeStr from './date_time_str' import GpsCoordStr from './gps_coord_str' import type PhotoMetadata from '../types/photo_metadata.type' -import { debounce } from 'lodash' import TextInput from './text_input' import { pathToId } from '../utilities/paths_utils' @@ -19,6 +18,8 @@ interface PhotoProps { deletePhoto?: (id: string) => void updateNotes?: (id: string, notes: string) => void count?: string + disallowNotes?: boolean + notes?: any } /** @@ -39,17 +40,10 @@ const Photo: FC = ({ metadata, photo, required, - deletePhoto, - updateNotes, - id, count, + disallowNotes, + notes }) => { - const [showDeleteModal, setShowDeleteModal] = useState(false) - const [notes, setNotes] = useState(metadata.notes || '') - const handleNotesChange = (input: string) => { - setNotes(input) - updateNotes && updateNotes(`${id}.notes`, input) - } return photo || required ? ( <> @@ -64,88 +58,31 @@ const Photo: FC = ({ <>
- - Timestamp:{' '} - {metadata?.timestamp ? ( - - ) : ( - Missing +
+ + Timestamp:{' '} + {metadata?.timestamp ? ( + + ) : ( + Missing + )} +
+ Geolocation:{' '} + { + + {' '} + + } +
+ {(notes && !disallowNotes) && ( +
+ Notes: + {notes} +
)} -
- Geolocation:{' '} - { - - {' '} - - } - {(updateNotes || metadata.notes) && ( - - )} - {deletePhoto && ( - - )} -
- {deletePhoto && ( - - -

- 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 2e5188a6..fcb927f8 100644 --- a/src/components/photo_input.tsx +++ b/src/components/photo_input.tsx @@ -1,11 +1,13 @@ import React, { useEffect, useRef, useState } from 'react' import type { ChangeEvent, FC, MouseEvent } from 'react' -import { Button, Card, Image } from 'react-bootstrap' +import { Button, Card, Image, Modal, ModalBody } from 'react-bootstrap' import { TfiGallery } from 'react-icons/tfi' import Collapsible from './collapsible' -import Photo from './photo' import PhotoMetadata from '../types/photo_metadata.type' import ImageBlobReduce from 'image-blob-reduce' +import GpsCoordStr from './gps_coord_str' +import DateTimeStr from './date_time_str' +import NotesInput from './notes' interface PhotoInputProps { children: React.ReactNode @@ -16,6 +18,8 @@ interface PhotoInputProps { id: string maxPhotos?: number updateNotes?: (id: string, notes: string) => void + disallowNotes?: boolean, + metadata?: any } // TODO: Determine whether or not the useEffect() method is needed. @@ -42,6 +46,8 @@ const PhotoInput: FC = ({ maxPhotos = 1, id, updateNotes, + disallowNotes, + metadata }) => { // Create references to the hidden file inputs const hiddenPhotoCaptureInputRef = useRef(null) @@ -51,6 +57,7 @@ const PhotoInput: FC = ({ const [photoId, setPhotoId] = useState( photoIds.length > 0 ? Math.max(photoIds as any as number) : 0, ) + const [showDeleteModal, setShowDeleteModal] = useState(false) const [cameraAvailable, setCameraAvailable] = useState(false) @@ -115,21 +122,6 @@ const PhotoInput: FC = ({ and may be a

. Nested

s are not allowed, so we use a

*/} {children} -
- {/* {(cameraAvailable || cameraAvailable) && - - } */} -
- {/* */} = ({ type="file" capture="environment" /> - {photos.length && ( + {( <> {photos.map((photo, index) => (
- + + + <> + +
+ + Timestamp:{' '} + {photo.data.metadata?.timestamp ? ( + + ) : ( + Missing + )} +
+ Geolocation:{' '} + { + + {' '} + + } + {(updateNotes || !disallowNotes) && + + } + { + + } +
+ {handleFileDelete && ( + + +

+ Are you sure you + want to delete + this photo? +

+
+ + +
+
+
+ )} + +
+
))} diff --git a/src/components/photo_input_wrapper.tsx b/src/components/photo_input_wrapper.tsx index 2213a4e9..3b672db9 100644 --- a/src/components/photo_input_wrapper.tsx +++ b/src/components/photo_input_wrapper.tsx @@ -45,6 +45,7 @@ const PhotoInputWrapper: FC = ({ upsertAttachment, removeAttachment, upsertMetaData, + metadata }) => { const upsertPhoto = (img_file: Blob, photoId: string) => { // Reduce the image size as needed @@ -63,6 +64,7 @@ const PhotoInputWrapper: FC = ({ id={id} removeAttachment={removeAttachment} updateNotes={upsertMetaData} + metadata={metadata} > {children} diff --git a/src/components/photo_wrapper.tsx b/src/components/photo_wrapper.tsx index cdde9028..0c996eb7 100644 --- a/src/components/photo_wrapper.tsx +++ b/src/components/photo_wrapper.tsx @@ -1,9 +1,10 @@ -import React, { FC } from 'react' +import React, { FC, useEffect, useState } from 'react' import { StoreContext } from './store' import Photo from './photo' import PhotoMetadata from '../types/photo_metadata.type' import { filterAttachmentsByIdPrefix } from './photo_input_wrapper' +import { get } from 'lodash' interface PhotoWrapperProps { children: React.ReactNode @@ -32,27 +33,19 @@ const PhotoWrapper: FC = ({ }) => { return ( - {({ attachments, data }) => { + {({ attachments, data, metadata }) => { const photos = filterAttachmentsByIdPrefix(attachments, id) return ( -
+
{photos.map((photo, index) => { return ( -
+
{}) as UpsertAttachment, removeAttachment: ((id: string) => {}) as RemoveAttachment, upsertMetaData: ((id: string, metadata: any) => {}) as UpsertMetaData, @@ -260,6 +261,7 @@ export const StoreProvider: FC = ({ } else { result.metadata_.last_modified_at = new Date() } + setMetaData(result.metadata_) return result }) .then(function (res) {