commit
49729614dc
6
package-lock.json
generated
6
package-lock.json
generated
|
@ -9,6 +9,7 @@
|
||||||
"version": "0.8.0",
|
"version": "0.8.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"cropperjs": "1.6.1",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"i18next": "22.4.15",
|
"i18next": "22.4.15",
|
||||||
"i18next-icu": "2.3.0",
|
"i18next-icu": "2.3.0",
|
||||||
|
@ -7530,6 +7531,11 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/cropperjs": {
|
||||||
|
"version": "1.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.6.1.tgz",
|
||||||
|
"integrity": "sha512-F4wsi+XkDHCOMrHMYjrTEE4QBOrsHHN5/2VsVAaRq8P7E5z7xQpT75S+f/9WikmBEailas3+yo+6zPIomW+NOA=="
|
||||||
|
},
|
||||||
"node_modules/cross-env": {
|
"node_modules/cross-env": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
"typecheck:watch": "tsc --noEmit --watch"
|
"typecheck:watch": "tsc --noEmit --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"cropperjs": "1.6.1",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"i18next": "22.4.15",
|
"i18next": "22.4.15",
|
||||||
"i18next-icu": "2.3.0",
|
"i18next-icu": "2.3.0",
|
||||||
|
|
|
@ -105,6 +105,7 @@
|
||||||
"Create gallery": "Create gallery",
|
"Create gallery": "Create gallery",
|
||||||
"Create post": "Create post",
|
"Create post": "Create post",
|
||||||
"Create video": "Create video",
|
"Create video": "Create video",
|
||||||
|
"Crop image": "Crop image",
|
||||||
"Culture": "Culture",
|
"Culture": "Culture",
|
||||||
"Date of Birth": "Date of Birth",
|
"Date of Birth": "Date of Birth",
|
||||||
"Decline": "Decline",
|
"Decline": "Decline",
|
||||||
|
|
|
@ -109,6 +109,7 @@
|
||||||
"Create gallery": "Создать галерею",
|
"Create gallery": "Создать галерею",
|
||||||
"Create post": "Создать публикацию",
|
"Create post": "Создать публикацию",
|
||||||
"Create video": "Создать видео",
|
"Create video": "Создать видео",
|
||||||
|
"Crop image": "Кадрировать изображение",
|
||||||
"Culture": "Культура",
|
"Culture": "Культура",
|
||||||
"Date of Birth": "Дата рождения",
|
"Date of Birth": "Дата рождения",
|
||||||
"Decline": "Отмена",
|
"Decline": "Отмена",
|
||||||
|
|
|
@ -14,13 +14,18 @@ import { getImageUrl } from '../../utils/getImageUrl'
|
||||||
import { handleImageUpload } from '../../utils/handleImageUpload'
|
import { handleImageUpload } from '../../utils/handleImageUpload'
|
||||||
import { profileSocialLinks } from '../../utils/profileSocialLinks'
|
import { profileSocialLinks } from '../../utils/profileSocialLinks'
|
||||||
import { validateUrl } from '../../utils/validateUrl'
|
import { validateUrl } from '../../utils/validateUrl'
|
||||||
|
|
||||||
|
import { Modal } from '../Nav/Modal'
|
||||||
import { Button } from '../_shared/Button'
|
import { Button } from '../_shared/Button'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
import { Loading } from '../_shared/Loading'
|
import { Loading } from '../_shared/Loading'
|
||||||
import { Popover } from '../_shared/Popover'
|
import { Popover } from '../_shared/Popover'
|
||||||
import { SocialNetworkInput } from '../_shared/SocialNetworkInput'
|
import { SocialNetworkInput } from '../_shared/SocialNetworkInput'
|
||||||
|
import { ImageCropper } from '../_shared/ImageCropper'
|
||||||
import { ProfileSettingsNavigation } from '../Nav/ProfileSettingsNavigation'
|
import { ProfileSettingsNavigation } from '../Nav/ProfileSettingsNavigation'
|
||||||
|
|
||||||
|
import { showModal, hideModal } from '../../stores/ui'
|
||||||
|
|
||||||
import styles from '../../pages/profile/Settings.module.scss'
|
import styles from '../../pages/profile/Settings.module.scss'
|
||||||
|
|
||||||
const SimplifiedEditor = lazy(() => import('../../components/Editor/SimplifiedEditor'))
|
const SimplifiedEditor = lazy(() => import('../../components/Editor/SimplifiedEditor'))
|
||||||
|
@ -28,12 +33,14 @@ const GrowingTextarea = lazy(() => import('../../components/_shared/GrowingTexta
|
||||||
|
|
||||||
export const ProfileSettings = () => {
|
export const ProfileSettings = () => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
|
||||||
const [prevForm, setPrevForm] = createStore({})
|
const [prevForm, setPrevForm] = createStore({})
|
||||||
const [isFormInitialized, setIsFormInitialized] = createSignal(false)
|
const [isFormInitialized, setIsFormInitialized] = createSignal(false)
|
||||||
const [social, setSocial] = createSignal([])
|
const [social, setSocial] = createSignal([])
|
||||||
const [addLinkForm, setAddLinkForm] = createSignal<boolean>(false)
|
const [addLinkForm, setAddLinkForm] = createSignal<boolean>(false)
|
||||||
const [incorrectUrl, setIncorrectUrl] = createSignal<boolean>(false)
|
const [incorrectUrl, setIncorrectUrl] = createSignal<boolean>(false)
|
||||||
const [isUserpicUpdating, setIsUserpicUpdating] = createSignal(false)
|
const [isUserpicUpdating, setIsUserpicUpdating] = createSignal(false)
|
||||||
|
const [userpicFile, setUserpicFile] = createSignal<any | null>(null)
|
||||||
const [uploadError, setUploadError] = createSignal(false)
|
const [uploadError, setUploadError] = createSignal(false)
|
||||||
const [isFloatingPanelVisible, setIsFloatingPanelVisible] = createSignal(false)
|
const [isFloatingPanelVisible, setIsFloatingPanelVisible] = createSignal(false)
|
||||||
const [hostname, setHostname] = createSignal<string | null>(null)
|
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(([uploadFile]) => {
|
||||||
selectFiles(async ([uploadFile]) => {
|
setUserpicFile(uploadFile)
|
||||||
try {
|
|
||||||
setUploadError(false)
|
showModal('cropImage')
|
||||||
setIsUserpicUpdating(true)
|
|
||||||
const result = await handleImageUpload(uploadFile)
|
|
||||||
updateFormField('userpic', result.url)
|
|
||||||
setIsUserpicUpdating(false)
|
|
||||||
} catch (error) {
|
|
||||||
setUploadError(true)
|
|
||||||
console.error('[upload avatar] error', error)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(() => {
|
onMount(() => {
|
||||||
setHostname(window?.location.host)
|
setHostname(window?.location.host)
|
||||||
|
|
||||||
|
@ -178,7 +194,7 @@ export const ProfileSettings = () => {
|
||||||
<div class="pretty-form__item">
|
<div class="pretty-form__item">
|
||||||
<div
|
<div
|
||||||
class={clsx(styles.userpic, { [styles.hasControls]: form.userpic })}
|
class={clsx(styles.userpic, { [styles.hasControls]: form.userpic })}
|
||||||
onClick={!form.userpic && handleUploadAvatar}
|
onClick={handleCropAvatar}
|
||||||
>
|
>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={isUserpicUpdating()}>
|
<Match when={isUserpicUpdating()}>
|
||||||
|
@ -206,17 +222,19 @@ export const ProfileSettings = () => {
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Popover>
|
</Popover>
|
||||||
<Popover content={t('Upload userpic')}>
|
|
||||||
|
{/* @@TODO inspect popover below. onClick causes page refreshing */}
|
||||||
|
{/* <Popover content={t('Upload userpic')}>
|
||||||
{(triggerRef: (el) => void) => (
|
{(triggerRef: (el) => void) => (
|
||||||
<button
|
<button
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
class={styles.control}
|
class={styles.control}
|
||||||
onClick={handleUploadAvatar}
|
onClick={() => handleCropAvatar()}
|
||||||
>
|
>
|
||||||
<Icon name="user-image-black" />
|
<Icon name="user-image-black" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Popover>
|
</Popover> */}
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={!form.userpic}>
|
<Match when={!form.userpic}>
|
||||||
|
@ -365,6 +383,21 @@ export const ProfileSettings = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</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>
|
</Show>
|
||||||
)
|
)
|
||||||
|
|
10
src/components/_shared/ImageCropper/ImageCropper.module.scss
Normal file
10
src/components/_shared/ImageCropper/ImageCropper.module.scss
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
.cropperContainer {
|
||||||
|
max-height: 55vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cropperControls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
79
src/components/_shared/ImageCropper/ImageCropper.tsx
Normal file
79
src/components/_shared/ImageCropper/ImageCropper.tsx
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
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'
|
||||||
|
|
||||||
|
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,
|
||||||
|
autoCropArea: 1,
|
||||||
|
modal: true,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div class={styles.cropperContainer}>
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
}
|
1
src/components/_shared/ImageCropper/index.tsx
Normal file
1
src/components/_shared/ImageCropper/index.tsx
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { ImageCropper } from './ImageCropper'
|
|
@ -26,6 +26,7 @@ export type ModalType =
|
||||||
| 'search'
|
| 'search'
|
||||||
| 'inviteCoAuthors'
|
| 'inviteCoAuthors'
|
||||||
| 'share'
|
| 'share'
|
||||||
|
| 'cropImage'
|
||||||
|
|
||||||
export const MODALS: Record<ModalType, ModalType> = {
|
export const MODALS: Record<ModalType, ModalType> = {
|
||||||
auth: 'auth',
|
auth: 'auth',
|
||||||
|
@ -44,6 +45,7 @@ export const MODALS: Record<ModalType, ModalType> = {
|
||||||
search: 'search',
|
search: 'search',
|
||||||
inviteCoAuthors: 'inviteCoAuthors',
|
inviteCoAuthors: 'inviteCoAuthors',
|
||||||
share: 'share',
|
share: 'share',
|
||||||
|
cropImage: 'cropImage',
|
||||||
}
|
}
|
||||||
|
|
||||||
const [modal, setModal] = createSignal<ModalType>(null)
|
const [modal, setModal] = createSignal<ModalType>(null)
|
||||||
|
|
|
@ -1074,3 +1074,31 @@ iframe {
|
||||||
.img-align-column {
|
.img-align-column {
|
||||||
clear: both;
|
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