implement image crop
This commit is contained in:
parent
3429b36502
commit
591fd2ecbf
|
@ -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",
|
||||
|
|
|
@ -109,6 +109,7 @@
|
|||
"Create gallery": "Создать галерею",
|
||||
"Create post": "Создать публикацию",
|
||||
"Create video": "Создать видео",
|
||||
"Crop image": "Скадрируйте изображение",
|
||||
"Culture": "Культура",
|
||||
"Date of Birth": "Дата рождения",
|
||||
"Decline": "Отмена",
|
||||
|
|
|
@ -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<boolean>(false)
|
||||
const [incorrectUrl, setIncorrectUrl] = createSignal<boolean>(false)
|
||||
const [isUserpicUpdating, setIsUserpicUpdating] = createSignal(false)
|
||||
const [userpicFile, setUserpicFile] = createSignal<any | null>(null)
|
||||
const [uploadError, setUploadError] = createSignal(false)
|
||||
const [isFloatingPanelVisible, setIsFloatingPanelVisible] = createSignal(false)
|
||||
const [hostname, setHostname] = createSignal<string | null>(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 = () => {
|
|||
<div class="pretty-form__item">
|
||||
<div
|
||||
class={clsx(styles.userpic, { [styles.hasControls]: form.userpic })}
|
||||
onClick={!form.userpic && handleUploadAvatar}
|
||||
onClick={handleCropAvatar}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={isUserpicUpdating()}>
|
||||
|
@ -206,17 +222,19 @@ export const ProfileSettings = () => {
|
|||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
<Popover content={t('Upload userpic')}>
|
||||
|
||||
{/* @@TODO inspect popover below. onClick causes page refreshing */}
|
||||
{/* <Popover content={t('Upload userpic')}>
|
||||
{(triggerRef: (el) => void) => (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
class={styles.control}
|
||||
onClick={handleUploadAvatar}
|
||||
onClick={() => handleCropAvatar()}
|
||||
>
|
||||
<Icon name="user-image-black" />
|
||||
</button>
|
||||
)}
|
||||
</Popover>
|
||||
</Popover> */}
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={!form.userpic}>
|
||||
|
@ -365,6 +383,21 @@ export const ProfileSettings = () => {
|
|||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<Modal variant="medium" name="cropImage" onClose={() => setUserpicFile(null)}>
|
||||
<h2>{t('Crop image')}</h2>
|
||||
|
||||
<Show when={userpicFile()}>
|
||||
<ImageCropper
|
||||
uploadFile={userpicFile()}
|
||||
onSave={(data) => {
|
||||
handleUploadAvatar(data)
|
||||
|
||||
hideModal()
|
||||
}}
|
||||
onDecline={() => hideModal()}
|
||||
/>
|
||||
</Show>
|
||||
</Modal>
|
||||
</>
|
||||
</Show>
|
||||
)
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
.cropperControls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
margin-top: 2rem;
|
||||
}
|
|
@ -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 (
|
||||
<>
|
||||
<Show when={preview() !== null}>
|
||||
<div>
|
||||
<div>
|
||||
<img ref={cropImage} src={preview()} alt="cropper" class="block max-w-full h-96 w-96" />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-x-1.5 rounded-md bg-indigo-600 py-2 px-3 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 mt-2"
|
||||
onClick={() => {
|
||||
setState('croppedImage', cropper().getCroppedCanvas().toDataURL(state.file.type))
|
||||
props.saveImage(state)
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={preview() === null}>
|
||||
<form class="min-h-96 min-w-96">
|
||||
<div
|
||||
id="dropzone"
|
||||
class={`${dropZoneActive() ? 'bg-green-100' : ''} ${
|
||||
uploading() && 'opacity-50'
|
||||
} place-content-center place-items-center h-96 w-96 border-2 border-gray-300 border-dashed rounded-md sm:flex p-2 m-2`}
|
||||
onDragEnter={() => (uploading() ? undefined : setDropZoneActive(true))}
|
||||
onDragLeave={() => setDropZoneActive(false)}
|
||||
onDragOver={noPropagate}
|
||||
onDrop={(event) => (uploading() ? noPropagate(event) : handleFileDrop(event))}
|
||||
>
|
||||
<div class="">upload</div>
|
||||
<input
|
||||
id="image-upload"
|
||||
name="file"
|
||||
type="file"
|
||||
disabled={uploading()}
|
||||
multiple={false}
|
||||
onInput={handleFileInput}
|
||||
class="sr-only"
|
||||
/>
|
||||
</div>
|
||||
<div class="h-8" />
|
||||
</form>
|
||||
</Show>
|
||||
</>
|
||||
<div>
|
||||
<div>
|
||||
<img
|
||||
ref={(el) => (imageTagRef.current = el)}
|
||||
src={props.uploadFile.source}
|
||||
alt="image crop panel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={styles.cropperControls}>
|
||||
<Show when={props.onDecline}>
|
||||
<Button variant="secondary" onClick={props.onDecline} value={t('Decline')} />
|
||||
</Show>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
cropper()
|
||||
.getCroppedCanvas()
|
||||
.toBlob((blob) => {
|
||||
const formData = new FormData()
|
||||
formData.append('media', blob, props.uploadFile.file.name)
|
||||
|
||||
props.onSave({
|
||||
...props.uploadFile,
|
||||
file: formData.get('media'),
|
||||
})
|
||||
})
|
||||
}}
|
||||
value={t('Save')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ export type ModalType =
|
|||
| 'following'
|
||||
| 'inviteCoAuthors'
|
||||
| 'share'
|
||||
| 'cropImage'
|
||||
|
||||
export const MODALS: Record<ModalType, ModalType> = {
|
||||
auth: 'auth',
|
||||
|
@ -42,6 +43,7 @@ export const MODALS: Record<ModalType, ModalType> = {
|
|||
following: 'following',
|
||||
inviteCoAuthors: 'inviteCoAuthors',
|
||||
share: 'share',
|
||||
cropImage: 'cropImage',
|
||||
}
|
||||
|
||||
const [modal, setModal] = createSignal<ModalType>(null)
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user