Skip to content

Commit

Permalink
refactor: image file upload
Browse files Browse the repository at this point in the history
  • Loading branch information
deeonwuli committed May 27, 2024
1 parent 5827b38 commit 865fc9e
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 56 deletions.
2 changes: 1 addition & 1 deletion src/domain/entities/Action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export const getPageActions = (

const hasUserAccess = actionUsers.includes(user.id);
const hasUserGroupAccess = _.intersection(actionUserGroups, userGroupIds).length > 0;
const hasPublicAccess = action.publicAccess && action.publicAccess !== "--------";
const hasPublicAccess = Boolean(action.publicAccess) && action.publicAccess !== "--------";

return hasUserAccess || hasUserGroupAccess || hasPublicAccess;
})
Expand Down
12 changes: 6 additions & 6 deletions src/domain/entities/LandingNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,17 +103,17 @@ export const updateLandingNodes = (
const applyFavicon = (parent: LandingNode): LandingNode => {
const spreadFaviconToChildren = (children: LandingNode[], favicon: string): LandingNode[] => {
return _.map(children, child => {
const updatedChild: LandingNode = { ...child, favicon };
if (child.children) {
updatedChild.children = spreadFaviconToChildren(child.children, favicon);
}
return updatedChild;
return {
...child,
favicon: favicon,
children: spreadFaviconToChildren(child.children, favicon),
};
});
};

return {
...parent,
children: parent.children ? spreadFaviconToChildren(parent.children, parent.favicon) : [],
children: spreadFaviconToChildren(parent.children, parent.favicon),
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
useSnackbar,
} from "@eyeseetea/d2-ui-components";
import { Button, Switch, TextField } from "@material-ui/core";
import React, { ChangeEvent, useCallback, useMemo, useState } from "react";
import React, { useCallback, useMemo, useState } from "react";
import styled from "styled-components";
import { generateUid } from "../../../data/utils/uid";
import { LandingNode, LandingNodePageRendering, LandingNodeType } from "../../../domain/entities/LandingNode";
Expand All @@ -15,6 +15,8 @@ import { MarkdownEditor } from "../markdown-editor/MarkdownEditor";
import { MarkdownViewer } from "../markdown-viewer/MarkdownViewer";
import { LandingBody } from "../landing-layout";
import { ColorPicker } from "../color-picker/ColorPicker";
import _ from "lodash";
import useImageFileUpload from "./useImageFileUpload";

const buildDefaultNode = (
type: LandingNodeType,
Expand Down Expand Up @@ -55,8 +57,8 @@ export const LandingPageEditDialog: React.FC<LandingPageEditDialogProps> = props
);
const [iconLocation, setIconLocation] = useState(value.iconLocation === "bottom");
const [pageRendering, setPageRendering] = useState(value.pageRendering === "single");
const [warnings, setWarnings] = useState<string[]>([]);

const { faviconWarnings, uploadFavicon, uploadIcon } = useImageFileUpload(setValue);
const items = useMemo(
() =>
actions
Expand Down Expand Up @@ -113,45 +115,9 @@ export const LandingPageEditDialog: React.FC<LandingPageEditDialogProps> = props
setValue(value => ({ ...value, pageRendering: event.target.checked ? "single" : "multiple" }));
};

const handleFileUpload = useCallback(
(event: ChangeEvent<HTMLInputElement>, fileType: keyof LandingNode) => {
const file = event.target.files ? event.target.files[0] : undefined;

file?.arrayBuffer().then(async data => {
const reader = new FileReader();
reader.onload = e => {
const img = new Image();
img.onload = () => {
const width = img.width;
const height = img.height;
const aspectRatio = width / height;

setWarnings([]);
const newWarnings: string[] = [];

if (fileType === "favicon") {
if (aspectRatio !== FAVICON_ASPECT_RATIO) {
newWarnings.push("Please ensure that your favicon has a 1:1 aspect ratio.");
}

if (width > FAVICON_MAX_SIZE || height > FAVICON_MAX_SIZE) {
newWarnings.push("Please use an icon of 128x128 pixels or smaller.");
}

newWarnings.length !== 0 && setWarnings(newWarnings);
}
};
if (e.target?.result) {
img.src = e.target.result as string;
}
};
reader.readAsDataURL(file);
const icon = await compositionRoot.instance.uploadFile(data, file.name);
setValue(node => ({ ...node, [fileType]: icon }));
});
},
[compositionRoot]
);
const onChangeIconSize = useCallback(size => {
setValue(value => ({ ...value, iconSize: size }));
}, []);

return (
<ConfirmationDialog fullWidth={true} {...props} maxWidth={"md"} onSave={save}>
Expand Down Expand Up @@ -218,7 +184,7 @@ export const LandingPageEditDialog: React.FC<LandingPageEditDialogProps> = props
</IconContainer>
) : null}

<FileInput type="file" onChange={event => handleFileUpload(event, "icon")} />
<FileInput type="file" onChange={uploadIcon} />
</IconUpload>

<div>
Expand All @@ -245,7 +211,7 @@ export const LandingPageEditDialog: React.FC<LandingPageEditDialogProps> = props
}
variant="contained"
value={size}
onClick={() => setValue(value => ({ ...value, iconSize: size }))}
onClick={() => onChangeIconSize(size)}
>
{size}
</Button>
Expand All @@ -264,12 +230,12 @@ export const LandingPageEditDialog: React.FC<LandingPageEditDialogProps> = props
</IconContainer>
) : null}

<FileInput type="file" onChange={event => handleFileUpload(event, "favicon")} />
<FileInput type="file" onChange={uploadFavicon} />
</IconUpload>

{warnings.length > 0 && (
{!_.isEmpty(faviconWarnings) && (
<WarningText>
{warnings.map(warning => (
{faviconWarnings.map(warning => (
<p key={warning}>{i18n.t(warning)}</p>
))}
</WarningText>
Expand Down Expand Up @@ -351,9 +317,6 @@ export const LandingPageEditDialog: React.FC<LandingPageEditDialogProps> = props
);
};

const FAVICON_ASPECT_RATIO = 1;
const FAVICON_MAX_SIZE = 128;

export interface LandingPageEditDialogProps extends Omit<ConfirmationDialogProps, "onSave"> {
initialNode?: LandingNode;
type: LandingNodeType;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { ChangeEvent, useCallback, useState } from "react";
import { useAppContext } from "../../contexts/app-context";
import { LandingNode } from "../../../domain/entities/LandingNode";
import i18n from "../../../locales";

export default function useImageFileUpload(updateNode: (value: React.SetStateAction<LandingNode>) => void) {
const { compositionRoot } = useAppContext();

const [faviconWarnings, setFaviconWarnings] = useState<string[]>([]);

const processImageFile = (file: File, fileType: keyof LandingNode) => {
const reader = new FileReader();
reader.onload = e => {
const img = new Image();
img.onload = () => {
const width = img.width;
const height = img.height;
const aspectRatio = width / height;

setFaviconWarnings([]);
if (fileType === "favicon") {
const aspectRatioWarnings = [
...(!isAspectRatioWithinMargin(aspectRatio)
? [
i18n.t("Please ensure that your favicon has a {{aspectRatio}} aspect ratio.", {
aspectRatio: FAVICON_ASPECT_RATIO.fraction,
}),
]
: []),
];

const imageDimensionWarnings = [
...(!isWithinMargin(width) || !isWithinMargin(height)
? [
i18n.t("Please use an icon of {{max}}x{{max}} pixels or smaller.", {
max: FAVICON_MAX_SIZE,
}),
]
: []),
];

const warnings = [...aspectRatioWarnings, ...imageDimensionWarnings];
setFaviconWarnings(warnings);
}
};
if (e.target?.result) {
img.src = e.target.result as string;
}
};
reader.readAsDataURL(file);
};

const handleImageFileUpload = useCallback(
(event: ChangeEvent<HTMLInputElement>, fileType: keyof LandingNode) => {
const file = event.target.files ? event.target.files[0] : undefined;

file?.arrayBuffer().then(async data => {
const icon = await compositionRoot.instance.uploadFile(data, file.name);
processImageFile(file, fileType);
updateNode(node => ({ ...node, [fileType]: icon }));
});
},
[compositionRoot.instance, updateNode]
);

const uploadIcon = useCallback(
(event: ChangeEvent<HTMLInputElement>) => handleImageFileUpload(event, "icon"),
[handleImageFileUpload]
);
const uploadFavicon = useCallback(
(event: ChangeEvent<HTMLInputElement>) => handleImageFileUpload(event, "favicon"),
[handleImageFileUpload]
);

return {
faviconWarnings,
uploadFavicon,
uploadIcon,
};
}

const FAVICON_ASPECT_RATIO = { fraction: "1:1", value: 1 };
const FAVICON_MAX_SIZE = 128;

const ALLOWED_MARGIN = 5;
const ASPECT_RATIO_MARGIN = 0.015;

const isWithinMargin = (dimension: number): boolean => {
return dimension >= FAVICON_MAX_SIZE - ALLOWED_MARGIN && dimension <= FAVICON_MAX_SIZE + ALLOWED_MARGIN;
};
const isAspectRatioWithinMargin = (aspectRatio: number): boolean =>
aspectRatio >= FAVICON_ASPECT_RATIO.value - ASPECT_RATIO_MARGIN &&
aspectRatio <= FAVICON_ASPECT_RATIO.value + ASPECT_RATIO_MARGIN;

0 comments on commit 865fc9e

Please sign in to comment.