Skip to content

Commit

Permalink
🎉 add back cropper (#151)
Browse files Browse the repository at this point in the history
  • Loading branch information
casperiv0 authored Nov 13, 2021
1 parent bf899c8 commit 3e422d9
Show file tree
Hide file tree
Showing 12 changed files with 303 additions and 22 deletions.
30 changes: 30 additions & 0 deletions .github/workflows/test-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Test builds

on: [push, pull_request]

jobs:
test-client-build:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [16.x]

steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Build config
run: "yarn workspace @snailycad/config build"

- name: Build schemas
run: "yarn workspace @snailycad/schemas build"

- name: Run build
run: "yarn workspace @snailycad/client build"
1 change: 1 addition & 0 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"react-bootstrap-icons": "^1.6.1",
"react-colorful": "^5.5.0",
"react-cool-onclickoutside": "^1.7.0",
"react-cropper": "^2.1.8",
"react-dom": "^17.0.2",
"react-hot-toast": "^2.1.1",
"react-markdown": "^7.1.0",
Expand Down
26 changes: 25 additions & 1 deletion packages/client/src/components/bleeter/ManageBleetModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,24 @@ import { Textarea } from "components/form/Textarea";
import { handleValidate } from "lib/handleValidate";
import { BLEETER_SCHEMA } from "@snailycad/schemas";
import { Error } from "components/form/Error";
import { CropImageModal } from "components/modal/CropImageModal";

interface Props {
post: BleeterPost | null;
}

export const ManageBleetModal = ({ post }: Props) => {
const { state, execute } = useFetch();
const { isOpen, closeModal } = useModal();
const { openModal, isOpen, closeModal } = useModal();
const t = useTranslations("Bleeter");
const common = useTranslations("Common");
const router = useRouter();

function onCropSuccess(url: Blob, filename: string, setImage: any) {
setImage(new File([url], filename, { type: url.type }));
closeModal(ModalIds.CropImageModal);
}

async function onSubmit(values: typeof INITIAL_VALUES) {
let json: any = {};

Expand Down Expand Up @@ -90,13 +96,23 @@ export const ManageBleetModal = ({ post }: Props) => {
<FormField label={t("headerImage")}>
<div className="flex">
<Input
style={{ width: "95%", marginRight: "0.5em" }}
type="file"
id="image"
hasError={!!errors.image}
onChange={(e) => {
setFieldValue("image", e.currentTarget.files?.[0]);
}}
/>
<Button
className="mr-2"
type="button"
onClick={() => {
openModal(ModalIds.CropImageModal);
}}
>
Crop
</Button>
</div>
</FormField>

Expand Down Expand Up @@ -138,6 +154,14 @@ export const ManageBleetModal = ({ post }: Props) => {
{post ? common("save") : t("createBleet")}
</Button>
</footer>

<CropImageModal
isOpen={isOpen(ModalIds.CropImageModal)}
onClose={() => closeModal(ModalIds.CropImageModal)}
image={values.image}
onSuccess={(...data) => onCropSuccess(...data, (d: any) => setFieldValue("image", d))}
options={{ height: 500, aspectRatio: 16 / 9 }}
/>
</form>
)}
</Formik>
Expand Down
35 changes: 31 additions & 4 deletions packages/client/src/components/ems-fd/modals/ManageDeputyModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useTranslations } from "use-intl";
import { AllowedFileExtension, allowedFileExtensions } from "@snailycad/config";
import { FormRow } from "components/form/FormRow";
import { useCitizen } from "context/CitizenContext";
import { CropImageModal } from "components/modal/CropImageModal";

interface Props {
deputy: FullDeputy | null;
Expand All @@ -27,7 +28,8 @@ interface Props {
}

export const ManageDeputyModal = ({ deputy, onClose, onUpdate, onCreate }: Props) => {
const { isOpen, closeModal } = useModal();
const [image, setImage] = React.useState<File | null>(null);
const { openModal, isOpen, closeModal } = useModal();
const common = useTranslations("Common");
const t = useTranslations();
const formRef = React.useRef<HTMLFormElement>(null);
Expand All @@ -41,18 +43,24 @@ export const ManageDeputyModal = ({ deputy, onClose, onUpdate, onCreate }: Props
onClose?.();
}

function onCropSuccess(url: Blob, filename: string) {
setImage(new File([url], filename, { type: url.type }));
closeModal(ModalIds.CropImageModal);
}

async function onSubmit(
values: typeof INITIAL_VALUES,
helpers: FormikHelpers<typeof INITIAL_VALUES>,
) {
const fd = formRef.current && new FormData(formRef.current);
const image = fd?.get("image") as File;
const fd = new FormData();

if (image && image.size && image.name) {
if (!allowedFileExtensions.includes(image.type as AllowedFileExtension)) {
helpers.setFieldError("image", `Only ${allowedFileExtensions.join(", ")} are supported`);
return;
}

fd.set("image", image, image.name);
}

let deputyId;
Expand Down Expand Up @@ -119,11 +127,23 @@ export const ManageDeputyModal = ({ deputy, onClose, onUpdate, onCreate }: Props
<div className="flex">
<Input
style={{ width: "95%", marginRight: "0.5em" }}
onChange={handleChange}
onChange={(e) => {
handleChange(e);
setImage(e.target.files?.[0] ?? null);
}}
type="file"
name="image"
value={values.image ?? ""}
/>
<Button
className="mr-2"
type="button"
onClick={() => {
openModal(ModalIds.CropImageModal);
}}
>
Crop
</Button>
<Button
type="button"
className="bg-red-400 hover:bg-red-500"
Expand Down Expand Up @@ -241,6 +261,13 @@ export const ManageDeputyModal = ({ deputy, onClose, onUpdate, onCreate }: Props
{deputy ? common("save") : common("create")}
</Button>
</footer>

<CropImageModal
isOpen={isOpen(ModalIds.CropImageModal)}
onClose={() => closeModal(ModalIds.CropImageModal)}
image={image}
onSuccess={onCropSuccess}
/>
</form>
)}
</Formik>
Expand Down
35 changes: 31 additions & 4 deletions packages/client/src/components/leo/modals/ManageOfficerModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { ModalIds } from "types/ModalIds";
import { useTranslations } from "use-intl";
import { AllowedFileExtension, allowedFileExtensions } from "@snailycad/config";
import { FormRow } from "components/form/FormRow";
import { CropImageModal } from "components/modal/CropImageModal";

interface Props {
officer: FullOfficer | null;
Expand All @@ -27,7 +28,8 @@ interface Props {
}

export const ManageOfficerModal = ({ officer, onClose, onUpdate, onCreate }: Props) => {
const { isOpen, closeModal } = useModal();
const [image, setImage] = React.useState<File | null>(null);
const { openModal, isOpen, closeModal } = useModal();
const common = useTranslations("Common");
const t = useTranslations("Leo");
const { citizens } = useCitizen();
Expand All @@ -41,18 +43,24 @@ export const ManageOfficerModal = ({ officer, onClose, onUpdate, onCreate }: Pro
onClose?.();
}

function onCropSuccess(url: Blob, filename: string) {
setImage(new File([url], filename, { type: url.type }));
closeModal(ModalIds.CropImageModal);
}

async function onSubmit(
values: typeof INITIAL_VALUES,
helpers: FormikHelpers<typeof INITIAL_VALUES>,
) {
const fd = formRef.current && new FormData(formRef.current);
const image = fd?.get("image") as File;
const fd = new FormData();

if (image && image.size && image.name) {
if (!allowedFileExtensions.includes(image.type as AllowedFileExtension)) {
helpers.setFieldError("image", `Only ${allowedFileExtensions.join(", ")} are supported`);
return;
}

fd.set("image", image, image.name);
}

let officerId;
Expand Down Expand Up @@ -118,11 +126,23 @@ export const ManageOfficerModal = ({ officer, onClose, onUpdate, onCreate }: Pro
<div className="flex">
<Input
style={{ width: "95%", marginRight: "0.5em" }}
onChange={handleChange}
onChange={(e) => {
handleChange(e);
setImage(e.target.files?.[0] ?? null);
}}
type="file"
name="image"
value={values.image ?? ""}
/>
<Button
className="mr-2"
type="button"
onClick={() => {
openModal(ModalIds.CropImageModal);
}}
>
Crop
</Button>
<Button
type="button"
variant="danger"
Expand Down Expand Up @@ -238,6 +258,13 @@ export const ManageOfficerModal = ({ officer, onClose, onUpdate, onCreate }: Pro
{officer ? common("save") : common("create")}
</Button>
</footer>

<CropImageModal
isOpen={isOpen(ModalIds.CropImageModal)}
onClose={() => closeModal(ModalIds.CropImageModal)}
image={image}
onSuccess={onCropSuccess}
/>
</form>
)}
</Formik>
Expand Down
83 changes: 83 additions & 0 deletions packages/client/src/components/modal/CropImageModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import * as React from "react";
import Cropper from "react-cropper";
import type CropperJS from "cropperjs";
import { Button } from "components/Button";
import { useTranslations } from "use-intl";
import { Modal } from "./Modal";

interface Props {
onSuccess: (url: Blob, filename: string) => void;
onClose: () => void;
isOpen: boolean;
image: File | null;
options?: { width?: number; height?: number; aspectRatio: number };
}

export const CropImageModal = ({ onSuccess, image, isOpen = false, onClose, options }: Props) => {
const common = useTranslations("Common");
const t = useTranslations("Errors");

const [src, setSrc] = React.useState(null);
const [cropper, setCropper] = React.useState<CropperJS>();
const width = !src ? 450 : options?.width ?? 900;
const height = options?.height ?? 400;
const aspectRatio = options?.aspectRatio ?? 1;

React.useEffect(() => {
if (!image) return;

const reader = new FileReader();
reader.onload = () => {
setSrc(reader.result as any);
};
reader.readAsDataURL(image);
}, [image]);

const getCropData = () => {
if (!image) return;

if (typeof cropper !== "undefined") {
cropper.getCroppedCanvas().toBlob((blob) => {
if (!blob) return;

onSuccess?.(blob, image.name);
});
}
};

return (
<Modal modalStyles={{ width }} title="Crop image" isOpen={isOpen} onClose={onClose}>
{src ? (
<Cropper
style={{ height, width: "100%" }}
zoomTo={0.5}
initialAspectRatio={aspectRatio}
src={src}
viewMode={1}
aspectRatio={aspectRatio}
minCropBoxHeight={80}
minCropBoxWidth={80}
background={false}
responsive
autoCropArea={1}
checkOrientation={false} // https://github.com/fengyuanchen/cropperjs/issues/671
onInitialized={(instance) => {
setCropper(instance);
}}
guides
/>
) : (
<p className="my-3">{t("selectImageFirst")}</p>
)}

<div className="flex items-center justify-end gap-2 mt-2">
<Button variant="cancel" onClick={onClose}>
{common("cancel")}
</Button>
<Button disabled={!image} className="flex items-center" onClick={getCropData}>
{common("save")}
</Button>
</div>
</Modal>
);
};
13 changes: 11 additions & 2 deletions packages/client/src/components/modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,22 @@ import { X } from "react-bootstrap-icons";
import { useModal } from "context/ModalContext";

export interface ModalProps {
modalStyles?: React.CSSProperties;
title: string;
children: React.ReactNode;
isOpen: boolean;
className?: string;
onClose: () => void;
}

export const Modal = ({ title, children, isOpen, className, onClose }: ModalProps) => {
export const Modal = ({
modalStyles = {},
title,
children,
isOpen,
className,
onClose,
}: ModalProps) => {
const { canBeClosed } = useModal();

function handleClose() {
Expand Down Expand Up @@ -56,11 +64,12 @@ export const Modal = ({ title, children, isOpen, className, onClose }: ModalProp
leaveTo="opacity-0 scale-95"
>
<div
style={modalStyles}
className={`z-30 max-w-[100%] inline-block p-4 px-6 my-8 overflow-auto text-left align-middle transition-all transform bg-white dark:bg-dark-bg dark:text-white shadow-xl rounded-lg ${className}`}
>
<Dialog.Title
as="h3"
className="text-xl font-semibold text-gray-900 dark:text-white flex items-center justify-between mb-2"
className="flex items-center justify-between mb-2 text-xl font-semibold text-gray-900 dark:text-white"
>
{title}

Expand Down
1 change: 1 addition & 0 deletions packages/client/src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AppProps } from "next/app";
import "cropperjs/dist/cropper.css";
import { Toaster } from "react-hot-toast";
import { NextIntlProvider } from "next-intl";
import { AuthProvider } from "context/AuthContext";
Expand Down
Loading

0 comments on commit 3e422d9

Please sign in to comment.