diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index fc967d81..2c65fac2 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -105,6 +105,7 @@ "Create gallery": "Create gallery", "Create post": "Create post", "Create video": "Create video", + "Crop image": "Crop image", "Culture": "Culture", "Date of Birth": "Date of Birth", "Decline": "Decline", diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index 5694bc44..ab5481e8 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -109,6 +109,7 @@ "Create gallery": "Создать галерею", "Create post": "Создать публикацию", "Create video": "Создать видео", + "Crop image": "Скадрируйте изображение", "Culture": "Культура", "Date of Birth": "Дата рождения", "Decline": "Отмена", diff --git a/src/components/ProfileSettings/ProfileSettings.tsx b/src/components/ProfileSettings/ProfileSettings.tsx index 1cc18e4d..176a7fa8 100644 --- a/src/components/ProfileSettings/ProfileSettings.tsx +++ b/src/components/ProfileSettings/ProfileSettings.tsx @@ -14,13 +14,18 @@ import { getImageUrl } from '../../utils/getImageUrl' import { handleImageUpload } from '../../utils/handleImageUpload' import { profileSocialLinks } from '../../utils/profileSocialLinks' import { validateUrl } from '../../utils/validateUrl' + +import { Modal } from '../Nav/Modal' import { Button } from '../_shared/Button' import { Icon } from '../_shared/Icon' import { Loading } from '../_shared/Loading' import { Popover } from '../_shared/Popover' import { SocialNetworkInput } from '../_shared/SocialNetworkInput' +import { ImageCropper } from '../_shared/ImageCropper' import { ProfileSettingsNavigation } from '../Nav/ProfileSettingsNavigation' +import { showModal, hideModal } from '../../stores/ui' + import styles from '../../pages/profile/Settings.module.scss' const SimplifiedEditor = lazy(() => import('../../components/Editor/SimplifiedEditor')) @@ -28,12 +33,14 @@ const GrowingTextarea = lazy(() => import('../../components/_shared/GrowingTexta export const ProfileSettings = () => { const { t } = useLocalize() + const [prevForm, setPrevForm] = createStore({}) const [isFormInitialized, setIsFormInitialized] = createSignal(false) const [social, setSocial] = createSignal([]) const [addLinkForm, setAddLinkForm] = createSignal(false) const [incorrectUrl, setIncorrectUrl] = createSignal(false) const [isUserpicUpdating, setIsUserpicUpdating] = createSignal(false) + const [userpicFile, setUserpicFile] = createSignal(null) const [uploadError, setUploadError] = createSignal(false) const [isFloatingPanelVisible, setIsFloatingPanelVisible] = createSignal(false) const [hostname, setHostname] = createSignal(null) @@ -115,23 +122,32 @@ export const ProfileSettings = () => { } } - const { selectFiles } = createFileUploader({ multiple: false, accept: 'image/*' }) + const handleCropAvatar = () => { + const { selectFiles } = createFileUploader({ multiple: false, accept: 'image/*' }) - const handleUploadAvatar = async () => { - selectFiles(async ([uploadFile]) => { - try { - setUploadError(false) - setIsUserpicUpdating(true) - const result = await handleImageUpload(uploadFile) - updateFormField('userpic', result.url) - setIsUserpicUpdating(false) - } catch (error) { - setUploadError(true) - console.error('[upload avatar] error', error) - } + selectFiles(([uploadFile]) => { + setUserpicFile(uploadFile) + + showModal('cropImage') }) } + const handleUploadAvatar = async (uploadFile) => { + try { + setUploadError(false) + setIsUserpicUpdating(true) + + const result = await handleImageUpload(uploadFile) + updateFormField('userpic', result.url) + + setUserpicFile(null) + setIsUserpicUpdating(false) + } catch (error) { + setUploadError(true) + console.error('[upload avatar] error', error) + } + } + onMount(() => { setHostname(window?.location.host) @@ -178,7 +194,7 @@ export const ProfileSettings = () => {
@@ -206,17 +222,19 @@ export const ProfileSettings = () => { )} - + + {/* @@TODO inspect popover below. onClick causes page refreshing */} + {/* {(triggerRef: (el) => void) => ( )} - + */}
@@ -365,6 +383,21 @@ export const ProfileSettings = () => {
+ setUserpicFile(null)}> +

{t('Crop image')}

+ + + { + handleUploadAvatar(data) + + hideModal() + }} + onDecline={() => hideModal()} + /> + +
) diff --git a/src/components/_shared/ImageCropper/ImageCropper.module.scss b/src/components/_shared/ImageCropper/ImageCropper.module.scss index e69de29b..19d5ff12 100644 --- a/src/components/_shared/ImageCropper/ImageCropper.module.scss +++ b/src/components/_shared/ImageCropper/ImageCropper.module.scss @@ -0,0 +1,6 @@ +.cropperControls { + display: flex; + justify-content: space-between; + + margin-top: 2rem; +} diff --git a/src/components/_shared/ImageCropper/ImageCropper.tsx b/src/components/_shared/ImageCropper/ImageCropper.tsx index 99d86faf..1d5caded 100644 --- a/src/components/_shared/ImageCropper/ImageCropper.tsx +++ b/src/components/_shared/ImageCropper/ImageCropper.tsx @@ -1,105 +1,78 @@ -import { createSignal, Show } from 'solid-js' -import { createStore } from 'solid-js/store' +import 'cropperjs/dist/cropper.css' + +import { createSignal, onMount, Show } from 'solid-js' import Cropper from 'cropperjs' +import { UploadFile } from '@solid-primitives/upload' + +import { useLocalize } from '../../../context/localize' + +import { Button } from '../Button' import styles from './ImageCropper.module.scss' -export const ImageCropper = (props) => { - let cropImage - const [state, setState] = createStore({ - error: null, - loading: false, - file: {}, - croppedImage: null, - }), - [dropZoneActive, setDropZoneActive] = createSignal(false), - [uploading, setUploading] = createSignal(false), - [preview, setPreview] = createSignal(null), - [cropper, setCropper] = createSignal(null), - noPropagate = (e) => { - e.preventDefault() - }, - uploadFile = async (file) => { - if (!file) return - setUploading(true) - setState('loading', true) - setState('file', file) - try { - const reader = new FileReader() - reader.onload = (e) => { - setPreview(e.target.result) - setCropper( - new Cropper(cropImage, { - aspectRatio: 1 / 1, - viewMode: 1, - rotatable: false, - }), - ) - } - reader.readAsDataURL(file) - } catch (e) { - console.error('upload failed', e) - const message = e instanceof Error ? e.message : String(e) - setState('error', message) - } - setState('loading', false) - setUploading(false) - }, - handleFileDrop = async (e) => { - e.preventDefault() - setDropZoneActive(false) - uploadFile(e.dataTransfer.files[0]) - }, - handleFileInput = async (e) => { - e.preventDefault() - uploadFile(e.currentTarget.files[0]) +interface CropperProps { + uploadFile: UploadFile + onSave: (any) => void + onDecline?: () => void +} + +export const ImageCropper = (props: CropperProps) => { + const { t } = useLocalize() + + const imageTagRef: { current: HTMLImageElement } = { + current: null, + } + + const [cropper, setCropper] = createSignal(null) + + onMount(() => { + if (imageTagRef.current) { + setCropper( + new Cropper(imageTagRef.current, { + viewMode: 1, + aspectRatio: 1, + guides: false, + background: false, + rotatable: false, + modal: true, + }), + ) } + }) return ( - <> - -
-
- cropper -
- -
-
- -
-
(uploading() ? undefined : setDropZoneActive(true))} - onDragLeave={() => setDropZoneActive(false)} - onDragOver={noPropagate} - onDrop={(event) => (uploading() ? noPropagate(event) : handleFileDrop(event))} - > -
upload
- -
-
- - - +
+
+ (imageTagRef.current = el)} + src={props.uploadFile.source} + alt="image crop panel" + /> +
+ +
+ +
+
) } diff --git a/src/stores/ui.ts b/src/stores/ui.ts index 7fcb4fe5..cfaf0c9e 100644 --- a/src/stores/ui.ts +++ b/src/stores/ui.ts @@ -25,6 +25,7 @@ export type ModalType = | 'following' | 'inviteCoAuthors' | 'share' + | 'cropImage' export const MODALS: Record = { auth: 'auth', @@ -42,6 +43,7 @@ export const MODALS: Record = { following: 'following', inviteCoAuthors: 'inviteCoAuthors', share: 'share', + cropImage: 'cropImage', } const [modal, setModal] = createSignal(null) diff --git a/src/styles/app.scss b/src/styles/app.scss index 2d3a7ac2..4efca363 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -1070,3 +1070,31 @@ iframe { .img-align-column { clear: both; } + +.cropper-modal { + background-color: #fff !important; +} + +.cropper-canvas { + filter: blur(2px); +} + +.cropper-view-box, +.cropper-crop-box, +.cropper-line, +.cropper-point { + box-shadow: none !important; + outline: none !important; + border: none !important; + background-color: transparent !important; +} + +.cropper-crop-box { + border: 2px solid #000 !important; + border-radius: 8px; +} + +.cropper-view-box, +.cropper-face { + border-radius: 50%; +}