Skip to content

Commit

Permalink
initial changes for allowing uploads of photos
Browse files Browse the repository at this point in the history
  • Loading branch information
ttomasz committed Sep 12, 2023
1 parent beb48f6 commit 4d16d6e
Show file tree
Hide file tree
Showing 9 changed files with 276 additions and 36 deletions.
10 changes: 10 additions & 0 deletions src/components/sidebar-left.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import DefibrillatorDetails from "./sidebar/defibrillatorDetails";
import SidebarAction from "../model/sidebarAction";
import DefibrillatorEditor from "./sidebar/defibrillatorEditor";
import { DefibrillatorData } from "../model/defibrillatorData";
import PhotoReport from "./sidebar/photoReporter";
import PhotoUpload from "./sidebar/photoUploader";

const SidebarLeft: FC<SidebarLeftProps> = (props) => {
const {
Expand Down Expand Up @@ -42,6 +44,14 @@ const SidebarLeft: FC<SidebarLeftProps> = (props) => {
data={data}
/>
);
case SidebarAction.reportPhoto:
return (
<PhotoReport data={data} closeSidebar={closeSidebar} />
);
case SidebarAction.uploadPhoto:
return (
<PhotoUpload data={data} closeSidebar={closeSidebar} />
);
default:
return null;
}
Expand Down
20 changes: 20 additions & 0 deletions src/components/sidebar.css
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,23 @@
.legend-text {
font-size: calc(100% - 1px);
}

.image-gallery-slide {
max-height: 300px;
}

.image-gallery-custom-icon {
color:#fff;
/* transition:all .3s ease-out; */
/* appearance:none; */
background-color:transparent;
/* border:0; */
cursor:pointer;
/* outline:none; */
position: absolute;
left: 15px;
bottom: 15px;
/* padding: 4; */
z-index:4;
filter:drop-shadow(0 2px 2px #1a1a1a)
}
16 changes: 16 additions & 0 deletions src/components/sidebar/access.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
import { useTranslation } from "react-i18next";
import React from "react";

const accessToColourMapping = {
yes: "has-background-green has-text-white-ter",
no: "has-background-red has-text-white-ter",
private: "has-background-blue has-text-white-ter",
permissive: "has-background-blue has-text-white-ter",
customers: "has-background-yellow has-text-black-ter",
default: "has-background-gray has-text-white-ter",
};

export function accessColourClass(access: string): string {
if (access in accessToColourMapping) {
return accessToColourMapping[access as keyof typeof accessToColourMapping];
}
return accessToColourMapping.default;
}

export default function AccessFormField({ access, setAccess }: AccessFormFieldProps) {
const { t } = useTranslation();
const groupName = "aedAccess";
Expand Down
97 changes: 61 additions & 36 deletions src/components/sidebar/defibrillatorDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { FC } from "react";
import React, { FC, useRef } from "react";
import { useTranslation } from "react-i18next";
import {
Button,
Card, Columns, Image,
} from "react-bulma-components";
import {
Expand All @@ -10,6 +11,7 @@ import {
import Icon from "@mdi/react";
import ImageGallery, { ReactImageGalleryItem } from "react-image-gallery";
import "react-image-gallery/styles/css/image-gallery.css";
import SidebarAction from "src/model/sidebarAction";
import {
CloseSidebarButton,
CopyUrlButton, EditButton,
Expand All @@ -22,48 +24,71 @@ import { OpeningHoursField } from "./openingHours";
import { CheckDateField } from "./verificationDate";
import { DefibrillatorData } from "../../model/defibrillatorData";
import DetailTextRow from "./detailTextRow";
import { useAppContext } from "../../appContext";
import { accessColourClass } from "./access";
import { backendBaseUrl } from "../../backend";
import { initialModalState, ModalType } from "../../model/modal";

const testImages: Array<ReactImageGalleryItem> = [
{
original: "https://picsum.photos/id/1018/1000/600/",
thumbnail: "https://picsum.photos/id/1018/250/150/",
},
{
original: "https://picsum.photos/id/1015/1000/600/",
thumbnail: "https://picsum.photos/id/1015/250/150/",
},
{
original: "https://picsum.photos/id/1019/1000/600/",
thumbnail: "https://picsum.photos/id/1019/250/150/",
},
];

const accessToColourMapping = {
yes: "has-background-green has-text-white-ter",
no: "has-background-red has-text-white-ter",
private: "has-background-blue has-text-white-ter",
permissive: "has-background-blue has-text-white-ter",
customers: "has-background-yellow has-text-black-ter",
default: "has-background-gray has-text-white-ter",
};

function accessColourClass(access: string): string {
if (access in accessToColourMapping) {
return accessToColourMapping[access as keyof typeof accessToColourMapping];
function photoGallery(data: DefibrillatorData, closeSidebar: () => void) {
const { t } = useTranslation();
const { authState: { auth }, setSidebarAction, setModalState } = useAppContext();
let images: ReactImageGalleryItem[] = [];
// Currently only one photo allowed
if (data.photoRelativeUrl !== undefined && data.photoRelativeUrl !== null) {
images = [
{
original: backendBaseUrl + data.photoRelativeUrl,
},
];
}
return accessToColourMapping.default;
}

function photoGallery(images: Array<ReactImageGalleryItem>) {
if (images.length > 0) {
const refImg = useRef<ImageGallery>(null);
const renderCustomControls = () => (
<Button
outlined
inverted
p={2}
className="image-gallery-custom-icon"
onClick={() => setSidebarAction(SidebarAction.reportPhoto)}
>
{
t("photo.report")
// refImg !== null && refImg.current !== null ? refImg.current.getCurrentIndex() : "nulllllll"
}
</Button>
);

return (
<div>
<ImageGallery items={images} showPlayButton={false} showThumbnails={images.length > 1} />
<hr />
<ImageGallery
ref={refImg}
items={images}
lazyLoad
showPlayButton={false}
showThumbnails={images.length > 1}
renderCustomControls={renderCustomControls}
/>
<hr style={{ marginTop: "0.5rem", marginBottom: "1rem" }} />
</div>
);
}
return null;
return (
<div>
<Button
m={1}
onClick={() => {
if (auth === null || !auth.authenticated()) {
closeSidebar();
setModalState({ ...initialModalState, visible: true, type: ModalType.NeedToLogin });
}
setSidebarAction(SidebarAction.uploadPhoto);
}}
>
{t("photo.upload")}
</Button>
<hr style={{ marginTop: "0.5rem", marginBottom: "1rem" }} />
</div>
);
}

const DefibrillatorDetails: FC<DefibrillatorDetailsProps> = (props) => {
Expand Down Expand Up @@ -97,7 +122,7 @@ const DefibrillatorDetails: FC<DefibrillatorDetailsProps> = (props) => {
<CloseSidebarButton closeSidebarFunction={closeSidebar} />
</Card.Header>
<Card.Content pl={3} pr={3} mb={1} pt={4} className="content pb-0">
{photoGallery(testImages)}
{photoGallery(data, closeSidebar)}
<Columns vCentered className="is-mobile">
<Columns.Column textAlign="center" size={2}>
<Icon path={mdiHomeRoof} size={1.15} className="icon" color="#028955" />
Expand Down
69 changes: 69 additions & 0 deletions src/components/sidebar/photoReporter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React, { FC } from "react";
import { Button, Card, Image } from "react-bulma-components";
import { useTranslation } from "react-i18next";
import { DefibrillatorData } from "src/model/defibrillatorData";
import SidebarAction from "src/model/sidebarAction";
import { CloseSidebarButton } from "./buttons";
import { accessColourClass } from "./access";
import { useAppContext } from "../../appContext";
import { backendBaseUrl } from "../../backend";

interface DefibrillatorDetailsProps {
data: DefibrillatorData | null,
closeSidebar: () => void,
}

const PhotoReport: FC<DefibrillatorDetailsProps> = (props) => {
const { t } = useTranslation();
const {
data, closeSidebar,
} = props;
const { setSidebarAction } = useAppContext();
if (data === null) return null;
const accessText = data.tags.access ? ` - ${t(`access.${data.tags.access}`)}` : "";
const sendReport = (photoId: string | undefined) => {
if (photoId === undefined) {
console.error("Photo id is undefined. Report issue to the maintainers.");
return;
}
console.log("Reported photo:", photoId);
fetch(`${backendBaseUrl}/api/v1/photos/report`, {
method: "POST",
body: `id=${encodeURIComponent(photoId)}`,
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
},
})
.then(() => console.log("uploaded")) // todo: add error handling
.catch((error) => console.log(error));
closeSidebar();
};

return (
<div className="sidebar" id="sidebar-div">
<Card>
<Card.Header
id="sidebar-header"
shadowless
className={accessColourClass(data.tags.access)}
alignItems="center"
>
<Image m={2} className="icon" src="./img/logo-aed.svg" color="white" alt="" size={48} />
<span
className="is-size-5 py-2 has-text-weight-light"
id="sidebar-caption"
>
{t("sidebar.caption_info") + accessText}
</span>
<CloseSidebarButton closeSidebarFunction={closeSidebar} />
</Card.Header>
<Card.Content>
<Button m={2} onClick={() => setSidebarAction(SidebarAction.showDetails)}>{t("cancel")}</Button>
<Button m={2} onClick={() => sendReport(data.photoId)}>{t("photo.send_report")}</Button>
</Card.Content>
</Card>
</div>
);
};

export default PhotoReport;
94 changes: 94 additions & 0 deletions src/components/sidebar/photoUploader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React, { FC, useState } from "react";
import { Button, Card, Image } from "react-bulma-components";
import { useTranslation } from "react-i18next";
import { DefibrillatorData } from "src/model/defibrillatorData";
import SidebarAction from "src/model/sidebarAction";
import { CloseSidebarButton } from "./buttons";
import { accessColourClass } from "./access";
import { useAppContext } from "../../appContext";
import { backendBaseUrl } from "../../backend";

interface DefibrillatorDetailsProps {
data: DefibrillatorData | null,
closeSidebar: () => void,
}

const PhotoUpload: FC<DefibrillatorDetailsProps> = (props) => {
const { t } = useTranslation();
const {
data, closeSidebar,
} = props;
const { authState: { auth }, setSidebarAction } = useAppContext();
const [selectedImage, setSelectedImage] = useState<File | null>(null);
if (data === null) return null;
const accessText = data.tags.access ? ` - ${t(`access.${data.tags.access}`)}` : "";

return (
<div className="sidebar" id="sidebar-div">
<Card>
<Card.Header
id="sidebar-header"
shadowless
className={accessColourClass(data.tags.access)}
alignItems="center"
>
<Image m={2} className="icon" src="./img/logo-aed.svg" color="white" alt="" size={48} />
<span
className="is-size-5 py-2 has-text-weight-light"
id="sidebar-caption"
>
{t("sidebar.caption_info") + accessText}
</span>
<CloseSidebarButton closeSidebarFunction={closeSidebar} />
</Card.Header>
<Card.Content>
<Button m={2} onClick={() => setSidebarAction(SidebarAction.showDetails)}>{t("cancel")}</Button>
{selectedImage !== null && (
<div>
<img
alt="not found"
width="250px"
src={URL.createObjectURL(selectedImage)}
/>
<br />
<Button onClick={() => setSelectedImage(null)}>{t("photo.remove")}</Button>
<Button
m={2}
onClick={() => {
console.log(selectedImage);
const fd = new FormData();
fd.append("node_id", data.osmId);
fd.append("file_license", "CC0");
fd.append(
"oauth2_credentials",
JSON.stringify({ access_token: auth?.options().access_token || "test" }),

This comment has been minimized.

Copy link
@Zaczero

Zaczero Sep 14, 2023

Member

This should be a complete credential set, not just access_token:

{"access_token":"<ACCESS_TOKEN>","token_type":"Bearer","scope":"read_prefs write_api","created_at":1646669786}
);
fd.append("file", selectedImage);
fetch(`${backendBaseUrl}/api/v1/photos/upload`, {
method: "POST",
body: fd,
})
.then(() => console.log("uploaded")) // todo: add error handling
.catch((error) => console.log(error));
}}
>
{t("photo.upload_photo")}
</Button>
</div>
)}
<input
type="file"
name="myImage"
onChange={(event) => {
const { files } = event.target;
console.log(files);
setSelectedImage(files !== null && files.length > 0 ? files[0] : null);
}}
/>
</Card.Content>
</Card>
</div>
);
};

export default PhotoUpload;
2 changes: 2 additions & 0 deletions src/model/defibrillatorData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ export interface NewDefibrillatorData {
export interface DefibrillatorData extends NewDefibrillatorData {
osmId: string,
osmType: string,
photoId: string | undefined,
photoRelativeUrl: string | undefined,
version: string,
}
2 changes: 2 additions & 0 deletions src/model/sidebarAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ enum SidebarAction {
showDetails,
addNode,
editNode,
reportPhoto,
uploadPhoto,
}
export default SidebarAction;
2 changes: 2 additions & 0 deletions src/osm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export async function fetchNodeData(url: string): Promise<DefibrillatorData | nu
osmType: "node",
lat: node.lat,
lon: node.lon,
photoId: node["@photo_id"],
photoRelativeUrl: node["@photo_url"],
tags: node.tags,
version: node.version,
};
Expand Down

0 comments on commit 4d16d6e

Please sign in to comment.