feat: add image crop modal

This commit is contained in:
Kentai Radiquum 2024-08-17 19:28:35 +05:00
parent 530fc1aad0
commit 5cde53c1d3
Signed by: Radiquum
GPG key ID: 858E8EE696525EED
5 changed files with 202 additions and 21 deletions

View file

@ -116,8 +116,22 @@ const months = [
export function unixToDate(unix: number, type: string = "short") {
const date = new Date(unix * 1000);
if (type === "short") return date.getDate() + " " + months[date.getMonth()] + " " + date.getFullYear();
if (type === "full") return date.getDate() + " " + months[date.getMonth()] + " " + date.getFullYear() + ", " + date.getHours() + ":" + date.getMinutes();
if (type === "short")
return (
date.getDate() + " " + months[date.getMonth()] + " " + date.getFullYear()
);
if (type === "full")
return (
date.getDate() +
" " +
months[date.getMonth()] +
" " +
date.getFullYear() +
", " +
date.getHours() +
":" +
date.getMinutes()
);
}
export const getSeasonFromUnix = (unix: number) => {
@ -148,7 +162,9 @@ export function sinceUnixDate(unixInSeconds: number) {
if (dateDifferenceSeconds < 86400) return `${hours} ${hoursName} назад`;
if (dateDifferenceSeconds < 2592000) return `${days} ${daysName} назад`;
return date.getDate() + " " + months[date.getMonth()] + " " + date.getFullYear();
return (
date.getDate() + " " + months[date.getMonth()] + " " + date.getFullYear()
);
}
export function minutesToTime(min: number) {
@ -248,3 +264,31 @@ export const SortList = {
alphabet_descending: 5,
alphabet_ascending: 6,
};
export function b64toBlob(
b64Data: string,
contentType: string,
sliceSize?: number
) {
contentType = contentType || "";
sliceSize = sliceSize || 512;
var byteCharacters = atob(b64Data);
var byteArrays = [];
for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
var slice = byteCharacters.slice(offset, offset + sliceSize);
var byteNumbers = new Array(slice.length);
for (var i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
var byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
var blob = new Blob(byteArrays, { type: contentType });
return blob;
}

View file

@ -0,0 +1,115 @@
import React, { useRef } from "react";
import Cropper, { ReactCropperElement } from "react-cropper";
import "cropperjs/dist/cropper.css";
import { Button, Modal } from "flowbite-react";
import { b64toBlob } from "#/api/utils";
type Props = {
src: string;
setSrc: (src: string) => void;
setTempSrc: (src: string) => void;
setImageData: (src: string) => void;
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
height: number;
width: number;
aspectRatio: number;
guides: boolean;
quality: number;
forceAspect?: boolean;
};
export const CropModal: React.FC<Props> = (props) => {
const cropperRef = useRef<ReactCropperElement>(null);
const getCropData = () => {
if (typeof cropperRef.current?.cropper !== "undefined") {
props.setSrc(cropperRef.current?.cropper.getCroppedCanvas().toDataURL());
let block = cropperRef.current?.cropper
.getCroppedCanvas({
width: props.width,
height: props.height,
maxWidth: props.width,
maxHeight: props.height,
})
.toDataURL("image/jpeg", props.quality)
.split(";");
let contentType = block[0].split(":")[1];
let realData = block[1].split(",")[1];
const blob = b64toBlob(realData, contentType);
const handleFileRead = (e, fileReader) => {
const content = fileReader.result;
props.setImageData(content);
};
const handleFileText = (file) => {
const fileReader = new FileReader();
fileReader.onloadend = (e) => {
handleFileRead(e, fileReader);
};
fileReader.readAsText(file);
};
handleFileText(blob);
props.setTempSrc("");
}
};
return (
<Modal
dismissible
show={props.isOpen}
onClose={() => props.setIsOpen(false)}
size={"7xl"}
>
<Modal.Header>Обрезать изображение</Modal.Header>
<Modal.Body>
<Cropper
src={props.src}
style={{ height: 400, width: "100%" }}
responsive={true}
// Cropper.js options
initialAspectRatio={props.aspectRatio}
aspectRatio={props.forceAspect ? props.aspectRatio : undefined}
guides={props.guides}
ref={cropperRef}
/>
<div className="mt-4">
<h2 className="font-bold text-md">Управление</h2>
<p>Тяните за углы что-бы выбрать область</p>
<p>
Нажмите 2 раза на пустое место, что бы поменять режим выбора области
на перемещение и обратно
</p>
<p>Используйте колёсико мыши что-бы изменить масштаб</p>
</div>
</Modal.Body>
<Modal.Footer>
<Button
color={"blue"}
onClick={() => {
getCropData();
props.setIsOpen(false);
}}
>
Сохранить
</Button>
<Button
color={"red"}
onClick={() => {
props.setSrc(null);
props.setTempSrc(null);
props.setImageData(null);
props.setIsOpen(false);
}}
>
Удалить
</Button>
</Modal.Footer>
</Modal>
);
};

View file

@ -15,8 +15,8 @@ import {
Label,
Modal,
} from "flowbite-react";
import { ReleaseSection } from "#/components/ReleaseSection/ReleaseSection";
import { ReleaseLink } from "#/components/ReleaseLink/ReleaseLink";
import { CropModal } from "#/components/CropModal/CropModal";
const fetcher = async (url: string) => {
const res = await fetch(url);
@ -41,6 +41,7 @@ export const CreateCollectionPage = () => {
const [imageData, setImageData] = useState<string>(null);
const [imageUrl, setImageUrl] = useState<string>(null);
const [tempImageUrl, setTempImageUrl] = useState<string>(null);
const [isPrivate, setIsPrivate] = useState(false);
const [collectionInfo, setCollectionInfo] = useState({
title: "",
@ -53,6 +54,7 @@ export const CreateCollectionPage = () => {
const [addedReleases, setAddedReleases] = useState([]);
const [addedReleasesIds, setAddedReleasesIds] = useState([]);
const [releasesEditModalOpen, setReleasesEditModalOpen] = useState(false);
const [cropModalOpen, setCropModalOpen] = useState(false);
const collection_id = searchParams.get("id") || null;
const mode = searchParams.get("mode") || null;
@ -78,31 +80,19 @@ export const CreateCollectionPage = () => {
}
}, [userStore.user]);
const handleFileRead = (e, fileReader, type) => {
const handleFileRead = (e, fileReader) => {
const content = fileReader.result;
if (type === "URL") {
setImageUrl(content);
} else {
setImageData(content);
}
setTempImageUrl(content);
};
const handleFilePreview = (file) => {
const fileReader = new FileReader();
fileReader.onloadend = (e) => {
handleFileRead(e, fileReader, "URL");
handleFileRead(e, fileReader);
};
fileReader.readAsDataURL(file);
};
const handleFileLoad = (file) => {
const fileReader = new FileReader();
fileReader.onloadend = (e) => {
handleFileRead(e, fileReader, "TEXT");
};
fileReader.readAsText(file);
};
function handleInput(e) {
const regex = /[^a-zA-Zа-яА-Я0-9_.,:()!? \[\]]/g;
setCollectionInfo({
@ -195,7 +185,7 @@ export const CreateCollectionPage = () => {
accept="image/jpg, image/jpeg, image/png"
onChange={(e) => {
handleFilePreview(e.target.files[0]);
handleFileLoad(e.target.files[0]);
setCropModalOpen(true);
}}
/>
</Label>
@ -298,6 +288,20 @@ export const CreateCollectionPage = () => {
setReleases={setAddedReleases}
setReleasesIds={setAddedReleasesIds}
/>
<CropModal
src={tempImageUrl}
setSrc={setImageUrl}
setTempSrc={setTempImageUrl}
setImageData={setImageData}
aspectRatio={600 / 337}
guides={false}
quality={0.9}
isOpen={cropModalOpen}
setIsOpen={setCropModalOpen}
forceAspect={true}
width={600}
height={337}
/>
</main>
);
};
@ -317,7 +321,7 @@ export const ReleasesEditModal = (props: {
const url = new URL("/api/search", window.location.origin);
url.searchParams.set("page", pageIndex.toString());
if (!query) return null
if (!query) return null;
url.searchParams.set("q", query);
return url.toString();
};

17
package-lock.json generated
View file

@ -14,6 +14,7 @@
"markdown-to-jsx": "^7.4.7",
"next": "14.2.5",
"react": "^18",
"react-cropper": "^2.3.3",
"react-dom": "^18",
"swiper": "^11.1.4",
"swr": "^2.2.5",
@ -1685,6 +1686,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cropperjs": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.6.2.tgz",
"integrity": "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA=="
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -4563,6 +4569,17 @@
"node": ">=0.10.0"
}
},
"node_modules/react-cropper": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/react-cropper/-/react-cropper-2.3.3.tgz",
"integrity": "sha512-zghiEYkUb41kqtu+2jpX2Ntigf+Jj1dF9ew4lAobPzI2adaPE31z0p+5TcWngK6TvmWQUwK3lj4G+NDh1PDQ1w==",
"dependencies": {
"cropperjs": "^1.5.13"
},
"peerDependencies": {
"react": ">=17.0.2"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",

View file

@ -15,6 +15,7 @@
"markdown-to-jsx": "^7.4.7",
"next": "14.2.5",
"react": "^18",
"react-cropper": "^2.3.3",
"react-dom": "^18",
"swiper": "^11.1.4",
"swr": "^2.2.5",