Feature/thumbor (#284)
* thumbor integration * disabled lazy loading for some images * add profile userpic upload error message --------- Co-authored-by: Igor Lobanov <igor.lobanov@onetwotrip.com>
This commit is contained in:
parent
933a2bb71a
commit
a54d592038
|
@ -1,24 +0,0 @@
|
||||||
import fetch from 'node-fetch'
|
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
|
||||||
const imageUrl = req.query.url
|
|
||||||
|
|
||||||
if (!imageUrl) {
|
|
||||||
return res.status(400).send('Missing URL parameter')
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const imageRes = await fetch(imageUrl)
|
|
||||||
|
|
||||||
if (!imageRes.ok) {
|
|
||||||
return res.status(404).send('Image not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
res.setHeader('Content-Type', imageRes.headers.get('content-type'))
|
|
||||||
|
|
||||||
imageRes.body.pipe(res)
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
return res.status(404).send('Error')
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -34,8 +34,7 @@
|
||||||
"i18next-icu": "2.3.0",
|
"i18next-icu": "2.3.0",
|
||||||
"intl-messageformat": "10.5.3",
|
"intl-messageformat": "10.5.3",
|
||||||
"just-throttle": "4.2.0",
|
"just-throttle": "4.2.0",
|
||||||
"mailgun.js": "8.2.1",
|
"mailgun.js": "8.2.1"
|
||||||
"node-fetch": "3.3.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.21.8",
|
"@babel/core": "7.21.8",
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import styles from './AudioHeader.module.scss'
|
import styles from './AudioHeader.module.scss'
|
||||||
import { imageProxy } from '../../../utils/imageProxy'
|
|
||||||
import { MediaItem } from '../../../pages/types'
|
import { MediaItem } from '../../../pages/types'
|
||||||
import { createSignal, Show } from 'solid-js'
|
import { createSignal, Show } from 'solid-js'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
import { Topic } from '../../../graphql/types.gen'
|
import { Topic } from '../../../graphql/types.gen'
|
||||||
import { CardTopic } from '../../Feed/CardTopic'
|
import { CardTopic } from '../../Feed/CardTopic'
|
||||||
|
import { Image } from '../../_shared/Image'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string
|
title: string
|
||||||
|
@ -19,7 +19,7 @@ export const AudioHeader = (props: Props) => {
|
||||||
return (
|
return (
|
||||||
<div class={clsx(styles.AudioHeader, { [styles.expandedImage]: expandedImage() })}>
|
<div class={clsx(styles.AudioHeader, { [styles.expandedImage]: expandedImage() })}>
|
||||||
<div class={styles.cover}>
|
<div class={styles.cover}>
|
||||||
<img class={styles.image} src={imageProxy(props.cover)} alt={props.title} />
|
<Image class={styles.image} src={props.cover} alt={props.title} width={200} />
|
||||||
<Show when={props.cover}>
|
<Show when={props.cover}>
|
||||||
<button type="button" class={styles.expand} onClick={() => setExpandedImage(!expandedImage())}>
|
<button type="button" class={styles.expand} onClick={() => setExpandedImage(!expandedImage())}>
|
||||||
<Icon name="expand-circle" />
|
<Icon name="expand-circle" />
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { PlayerHeader } from './PlayerHeader'
|
||||||
import { PlayerPlaylist } from './PlayerPlaylist'
|
import { PlayerPlaylist } from './PlayerPlaylist'
|
||||||
import styles from './AudioPlayer.module.scss'
|
import styles from './AudioPlayer.module.scss'
|
||||||
import { MediaItem } from '../../../pages/types'
|
import { MediaItem } from '../../../pages/types'
|
||||||
import { imageProxy } from '../../../utils/imageProxy'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
media: MediaItem[]
|
media: MediaItem[]
|
||||||
|
@ -145,8 +144,7 @@ export const AudioPlayer = (props: Props) => {
|
||||||
<audio
|
<audio
|
||||||
ref={(el) => (audioRef.current = el)}
|
ref={(el) => (audioRef.current = el)}
|
||||||
onTimeUpdate={handleAudioTimeUpdate}
|
onTimeUpdate={handleAudioTimeUpdate}
|
||||||
// TEMP SOLUTION for http/https
|
src={currentTack().url}
|
||||||
src={currentTack().url.startsWith('https') ? currentTack().url : imageProxy(currentTack().url)}
|
|
||||||
onCanPlay={() => {
|
onCanPlay={() => {
|
||||||
// start to play the next track on src change
|
// start to play the next track on src change
|
||||||
if (isPlaying()) {
|
if (isPlaying()) {
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { useReactions } from '../../context/reactions'
|
||||||
import { MediaItem } from '../../pages/types'
|
import { MediaItem } from '../../pages/types'
|
||||||
import { DEFAULT_HEADER_OFFSET, router, useRouter } from '../../stores/router'
|
import { DEFAULT_HEADER_OFFSET, router, useRouter } from '../../stores/router'
|
||||||
import { getDescription } from '../../utils/meta'
|
import { getDescription } from '../../utils/meta'
|
||||||
import { imageProxy } from '../../utils/imageProxy'
|
|
||||||
import { TableOfContents } from '../TableOfContents'
|
import { TableOfContents } from '../TableOfContents'
|
||||||
import { AudioPlayer } from './AudioPlayer'
|
import { AudioPlayer } from './AudioPlayer'
|
||||||
import { SharePopup } from './SharePopup'
|
import { SharePopup } from './SharePopup'
|
||||||
|
@ -26,6 +25,7 @@ import styles from './Article.module.scss'
|
||||||
import { CardTopic } from '../Feed/CardTopic'
|
import { CardTopic } from '../Feed/CardTopic'
|
||||||
import { createPopper } from '@popperjs/core'
|
import { createPopper } from '@popperjs/core'
|
||||||
import { AuthorBadge } from '../Author/AuthorBadge'
|
import { AuthorBadge } from '../Author/AuthorBadge'
|
||||||
|
import { getImageUrl } from '../../utils/getImageUrl'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
article: Shout
|
article: Shout
|
||||||
|
@ -266,7 +266,9 @@ export const FullArticle = (props: Props) => {
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class={styles.shoutCover}
|
class={styles.shoutCover}
|
||||||
style={{ 'background-image': `url('${imageProxy(props.article.cover)}')` }}
|
style={{
|
||||||
|
'background-image': `url('${getImageUrl(props.article.cover, { width: 1600 })}')`
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -28,13 +28,6 @@
|
||||||
box-shadow: 0 0 0 1px var(--background-color-invert) inset;
|
box-shadow: 0 0 0 1px var(--background-color-invert) inset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.anonymous {
|
|
||||||
height: 17px !important;
|
|
||||||
object-fit: contain;
|
|
||||||
width: 20px !important;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:link,
|
a:link,
|
||||||
a:visited {
|
a:visited {
|
||||||
border: none;
|
border: none;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { createSignal, Show } from 'solid-js'
|
import { createMemo, Show } from 'solid-js'
|
||||||
import styles from './Userpic.module.scss'
|
import styles from './Userpic.module.scss'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { imageProxy } from '../../../utils/imageProxy'
|
|
||||||
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
|
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
|
||||||
import { Loading } from '../../_shared/Loading'
|
import { Loading } from '../../_shared/Loading'
|
||||||
|
import { Image } from '../../_shared/Image'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
name: string
|
name: string
|
||||||
|
@ -17,38 +17,31 @@ type Props = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Userpic = (props: Props) => {
|
export const Userpic = (props: Props) => {
|
||||||
const [userpicUrl, setUserpicUrl] = createSignal<string>()
|
|
||||||
const letters = () => {
|
const letters = () => {
|
||||||
if (!props.name) return
|
if (!props.name) return
|
||||||
const names = props.name ? props.name.split(' ') : []
|
const names = props.name ? props.name.split(' ') : []
|
||||||
return names[0][0] + (names.length > 1 ? names[1][0] : '')
|
return names[0][0] + (names.length > 1 ? names[1][0] : '')
|
||||||
}
|
}
|
||||||
|
|
||||||
const comutedAvatarSize = () => {
|
const avatarSize = createMemo(() => {
|
||||||
switch (props.size) {
|
switch (props.size) {
|
||||||
case 'XS': {
|
case 'XS': {
|
||||||
return '40x40'
|
return 40
|
||||||
}
|
}
|
||||||
case 'S': {
|
case 'S': {
|
||||||
return '56x56'
|
return 56
|
||||||
}
|
}
|
||||||
case 'L': {
|
case 'L': {
|
||||||
return '80x80'
|
return 80
|
||||||
}
|
}
|
||||||
case 'XL': {
|
case 'XL': {
|
||||||
return '336x336'
|
return 336
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return '64x64'
|
return 64
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
setUserpicUrl(
|
|
||||||
props.userpic && props.userpic.includes('100x')
|
|
||||||
? props.userpic.replace('100x', comutedAvatarSize())
|
|
||||||
: props.userpic
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -62,18 +55,8 @@ export const Userpic = (props: Props) => {
|
||||||
condition={props.hasLink}
|
condition={props.hasLink}
|
||||||
wrapper={(children) => <a href={`/author/${props.slug}`}>{children}</a>}
|
wrapper={(children) => <a href={`/author/${props.slug}`}>{children}</a>}
|
||||||
>
|
>
|
||||||
<Show
|
<Show when={props.userpic} fallback={<div class={styles.letters}>{letters()}</div>}>
|
||||||
when={!props.userpic}
|
<Image src={props.userpic} width={avatarSize()} height={avatarSize()} alt={props.name} />
|
||||||
fallback={
|
|
||||||
<img
|
|
||||||
class={clsx({ [styles.anonymous]: !props.userpic })}
|
|
||||||
src={imageProxy(userpicUrl()) || '/icons/user-default.svg'}
|
|
||||||
alt={props.name || ''}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class={styles.letters}>{letters()}</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
</ConditionalWrapper>
|
</ConditionalWrapper>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -44,9 +44,8 @@ import { EditorFloatingMenu } from './EditorFloatingMenu'
|
||||||
import './Prosemirror.scss'
|
import './Prosemirror.scss'
|
||||||
import { Image } from '@tiptap/extension-image'
|
import { Image } from '@tiptap/extension-image'
|
||||||
import { Footnote } from './extensions/Footnote'
|
import { Footnote } from './extensions/Footnote'
|
||||||
import { handleFileUpload } from '../../utils/handleFileUpload'
|
|
||||||
import { imageProxy } from '../../utils/imageProxy'
|
|
||||||
import { useSnackbar } from '../../context/snackbar'
|
import { useSnackbar } from '../../context/snackbar'
|
||||||
|
import { handleImageUpload } from '../../utils/handleImageUpload'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
shoutId: number
|
shoutId: number
|
||||||
|
@ -154,7 +153,7 @@ export const Editor = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
showSnackbar({ body: t('Uploading image') })
|
showSnackbar({ body: t('Uploading image') })
|
||||||
const result = await handleFileUpload(uplFile)
|
const result = await handleImageUpload(uplFile)
|
||||||
|
|
||||||
editor()
|
editor()
|
||||||
.chain()
|
.chain()
|
||||||
|
@ -174,7 +173,7 @@ export const Editor = (props: Props) => {
|
||||||
{
|
{
|
||||||
type: 'image',
|
type: 'image',
|
||||||
attrs: {
|
attrs: {
|
||||||
src: imageProxy(result.url)
|
src: result.url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -21,7 +21,6 @@ import { Modal } from '../Nav/Modal'
|
||||||
import { hideModal, showModal } from '../../stores/ui'
|
import { hideModal, showModal } from '../../stores/ui'
|
||||||
import { Blockquote } from '@tiptap/extension-blockquote'
|
import { Blockquote } from '@tiptap/extension-blockquote'
|
||||||
import { UploadModalContent } from './UploadModalContent'
|
import { UploadModalContent } from './UploadModalContent'
|
||||||
import { imageProxy } from '../../utils/imageProxy'
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import styles from './SimplifiedEditor.module.scss'
|
import styles from './SimplifiedEditor.module.scss'
|
||||||
import { Placeholder } from '@tiptap/extension-placeholder'
|
import { Placeholder } from '@tiptap/extension-placeholder'
|
||||||
|
@ -174,7 +173,7 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
{
|
{
|
||||||
type: 'image',
|
type: 'image',
|
||||||
attrs: {
|
attrs: {
|
||||||
src: imageProxy(image.url)
|
src: image.url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,11 +6,11 @@ import { createSignal, Show } from 'solid-js'
|
||||||
import { InlineForm } from '../InlineForm'
|
import { InlineForm } from '../InlineForm'
|
||||||
import { hideModal } from '../../../stores/ui'
|
import { hideModal } from '../../../stores/ui'
|
||||||
import { createDropzone, createFileUploader, UploadFile } from '@solid-primitives/upload'
|
import { createDropzone, createFileUploader, UploadFile } from '@solid-primitives/upload'
|
||||||
import { handleFileUpload } from '../../../utils/handleFileUpload'
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { Loading } from '../../_shared/Loading'
|
import { Loading } from '../../_shared/Loading'
|
||||||
import { verifyImg } from '../../../utils/verifyImg'
|
import { verifyImg } from '../../../utils/verifyImg'
|
||||||
import { UploadedFile } from '../../../pages/types'
|
import { UploadedFile } from '../../../pages/types'
|
||||||
|
import { handleImageUpload } from '../../../utils/handleImageUpload'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClose: (image?: UploadedFile) => void
|
onClose: (image?: UploadedFile) => void
|
||||||
|
@ -24,10 +24,10 @@ export const UploadModalContent = (props: Props) => {
|
||||||
const [dragError, setDragError] = createSignal<string | undefined>()
|
const [dragError, setDragError] = createSignal<string | undefined>()
|
||||||
|
|
||||||
const { selectFiles } = createFileUploader({ multiple: false, accept: 'image/*' })
|
const { selectFiles } = createFileUploader({ multiple: false, accept: 'image/*' })
|
||||||
const runUpload = async (file) => {
|
const runUpload = async (file: UploadFile) => {
|
||||||
try {
|
try {
|
||||||
setIsUploading(true)
|
setIsUploading(true)
|
||||||
const result = await handleFileUpload(file)
|
const result = await handleImageUpload(file)
|
||||||
props.onClose(result)
|
props.onClose(result)
|
||||||
setIsUploading(false)
|
setIsUploading(false)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -41,7 +41,7 @@ export const UploadModalContent = (props: Props) => {
|
||||||
try {
|
try {
|
||||||
const data = await fetch(value)
|
const data = await fetch(value)
|
||||||
const blob = await data.blob()
|
const blob = await data.blob()
|
||||||
const file = await new File([blob], 'convertedFromUrl', { type: data.headers.get('Content-Type') })
|
const file = new File([blob], 'convertedFromUrl', { type: data.headers.get('Content-Type') })
|
||||||
const fileToUpload: UploadFile = {
|
const fileToUpload: UploadFile = {
|
||||||
source: blob.toString(),
|
source: blob.toString(),
|
||||||
name: file.name,
|
name: file.name,
|
||||||
|
@ -55,7 +55,7 @@ export const UploadModalContent = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUpload = async () => {
|
const handleUpload = async () => {
|
||||||
await selectFiles(async ([uploadFile]) => {
|
selectFiles(async ([uploadFile]) => {
|
||||||
await runUpload(uploadFile)
|
await runUpload(uploadFile)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -72,7 +72,7 @@ export const UploadModalContent = (props: Props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const handleDrag = (event) => {
|
const handleDrag = (event: MouseEvent) => {
|
||||||
if (event.type === 'dragenter' || event.type === 'dragover') {
|
if (event.type === 'dragenter' || event.type === 'dragover') {
|
||||||
setDragActive(true)
|
setDragActive(true)
|
||||||
} else if (event.type === 'dragleave') {
|
} else if (event.type === 'dragleave') {
|
||||||
|
@ -80,6 +80,15 @@ export const UploadModalContent = (props: Props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleValidate = async (value: string) => {
|
||||||
|
const validationResult = await verifyImg(value)
|
||||||
|
if (!validationResult) {
|
||||||
|
return t('Invalid image URL')
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={styles.uploadModalContent}>
|
<div class={styles.uploadModalContent}>
|
||||||
<Show when={!isUploading()} fallback={<Loading />}>
|
<Show when={!isUploading()} fallback={<Loading />}>
|
||||||
|
@ -113,7 +122,7 @@ export const UploadModalContent = (props: Props) => {
|
||||||
hideModal()
|
hideModal()
|
||||||
props.onClose()
|
props.onClose()
|
||||||
}}
|
}}
|
||||||
validate={async (value) => ((await verifyImg(value)) ? '' : t('Invalid image URL'))}
|
validate={handleValidate}
|
||||||
onSubmit={handleImageFormSubmit}
|
onSubmit={handleImageFormSubmit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -117,6 +117,7 @@
|
||||||
|
|
||||||
.shoutAuthor {
|
.shoutAuthor {
|
||||||
@include font-size(1.4rem);
|
@include font-size(1.4rem);
|
||||||
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-right: 1.6rem;
|
margin-right: 1.6rem;
|
||||||
|
|
||||||
|
|
|
@ -2,21 +2,21 @@ import { createMemo, createSignal, For, Show } from 'solid-js'
|
||||||
import type { Shout } from '../../../graphql/types.gen'
|
import type { Shout } from '../../../graphql/types.gen'
|
||||||
import { capitalize } from '../../../utils/capitalize'
|
import { capitalize } from '../../../utils/capitalize'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
import styles from './ArticleCard.module.scss'
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { CardTopic } from '../CardTopic'
|
import { CardTopic } from '../CardTopic'
|
||||||
import { ShoutRatingControl } from '../../Article/ShoutRatingControl'
|
import { ShoutRatingControl } from '../../Article/ShoutRatingControl'
|
||||||
import { getShareUrl, SharePopup } from '../../Article/SharePopup'
|
import { getShareUrl, SharePopup } from '../../Article/SharePopup'
|
||||||
import stylesHeader from '../../Nav/Header/Header.module.scss'
|
|
||||||
import { getDescription } from '../../../utils/meta'
|
import { getDescription } from '../../../utils/meta'
|
||||||
import { FeedArticlePopup } from '../FeedArticlePopup'
|
import { FeedArticlePopup } from '../FeedArticlePopup'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { getPagePath, openPage } from '@nanostores/router'
|
import { getPagePath, openPage } from '@nanostores/router'
|
||||||
import { router, useRouter } from '../../../stores/router'
|
import { router, useRouter } from '../../../stores/router'
|
||||||
import { imageProxy } from '../../../utils/imageProxy'
|
|
||||||
import { Popover } from '../../_shared/Popover'
|
import { Popover } from '../../_shared/Popover'
|
||||||
|
import { Image } from '../../_shared/Image'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
import { AuthorLink } from '../../Author/AhtorLink'
|
import { AuthorLink } from '../../Author/AhtorLink'
|
||||||
|
import stylesHeader from '../../Nav/Header/Header.module.scss'
|
||||||
|
import styles from './ArticleCard.module.scss'
|
||||||
|
|
||||||
interface ArticleCardProps {
|
interface ArticleCardProps {
|
||||||
settings?: {
|
settings?: {
|
||||||
|
@ -118,7 +118,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
<div class={styles.shoutCardCoverContainer}>
|
<div class={styles.shoutCardCoverContainer}>
|
||||||
<div class={styles.shoutCardCover}>
|
<div class={styles.shoutCardCover}>
|
||||||
<Show when={props.article.cover}>
|
<Show when={props.article.cover}>
|
||||||
<img src={imageProxy(props.article.cover)} alt={title || ''} loading="lazy" />
|
<Image src={props.article.cover} alt={title} width={600} />
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -211,7 +211,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div class={styles.shoutCardCover}>
|
<div class={styles.shoutCardCover}>
|
||||||
<img src={imageProxy(props.article.cover)} alt={title || ''} loading="lazy" />
|
<Image src={props.article.cover} alt={title} width={600} loading="lazy" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Show, createMemo } from 'solid-js'
|
||||||
import './DialogCard.module.scss'
|
import './DialogCard.module.scss'
|
||||||
import styles from './DialogAvatar.module.scss'
|
import styles from './DialogAvatar.module.scss'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { imageProxy } from '../../utils/imageProxy'
|
import { getImageUrl } from '../../utils/getImageUrl'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
name: string
|
name: string
|
||||||
|
@ -47,7 +47,10 @@ const DialogAvatar = (props: Props) => {
|
||||||
style={{ 'background-color': `${randomBg()}` }}
|
style={{ 'background-color': `${randomBg()}` }}
|
||||||
>
|
>
|
||||||
<Show when={Boolean(props.url)} fallback={<div class={styles.letter}>{nameFirstLetter()}</div>}>
|
<Show when={Boolean(props.url)} fallback={<div class={styles.letter}>{nameFirstLetter()}</div>}>
|
||||||
<div class={styles.imageHolder} style={{ 'background-image': `url(${imageProxy(props.url)})` }} />
|
<div
|
||||||
|
class={styles.imageHolder}
|
||||||
|
style={{ 'background-image': `url(${getImageUrl(props.url, { width: 40, height: 40 })})` }}
|
||||||
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -114,6 +114,7 @@
|
||||||
|
|
||||||
.mainNavigationWrapper {
|
.mainNavigationWrapper {
|
||||||
@include font-size(1.7rem);
|
@include font-size(1.7rem);
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
@include media-breakpoint-down(lg) {
|
@include media-breakpoint-down(lg) {
|
||||||
|
@ -127,7 +128,7 @@
|
||||||
|
|
||||||
.mainNavigation {
|
.mainNavigation {
|
||||||
font-size: 1.4rem !important;
|
font-size: 1.4rem !important;
|
||||||
//margin: 0 0 0 -0.4rem !important;
|
// margin: 0 0 0 -0.4rem !important;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: opacity 0.3s;
|
transition: opacity 0.3s;
|
||||||
|
|
||||||
|
@ -202,7 +203,7 @@
|
||||||
li {
|
li {
|
||||||
margin-bottom: 0 !important;
|
margin-bottom: 0 !important;
|
||||||
|
|
||||||
&:first-letter {
|
&::first-letter {
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -293,7 +294,7 @@
|
||||||
.burgerContainer {
|
.burgerContainer {
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
//float: right;
|
// float: right;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
|
|
||||||
@include media-breakpoint-up(sm) {
|
@include media-breakpoint-up(sm) {
|
||||||
|
@ -383,6 +384,7 @@
|
||||||
|
|
||||||
.articleHeader {
|
.articleHeader {
|
||||||
@include font-size(1.4rem);
|
@include font-size(1.4rem);
|
||||||
|
|
||||||
left: $container-padding-x;
|
left: $container-padding-x;
|
||||||
margin: 0.2em 0;
|
margin: 0.2em 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -624,7 +626,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
//background-color: var(--link-hover-background) !important;
|
// background-color: var(--link-hover-background) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,12 @@ import { clsx } from 'clsx'
|
||||||
import styles from './TopicBadge.module.scss'
|
import styles from './TopicBadge.module.scss'
|
||||||
import { FollowingEntity, Topic } from '../../../graphql/types.gen'
|
import { FollowingEntity, Topic } from '../../../graphql/types.gen'
|
||||||
import { createMemo, createSignal, Show } from 'solid-js'
|
import { createMemo, createSignal, Show } from 'solid-js'
|
||||||
import { imageProxy } from '../../../utils/imageProxy'
|
|
||||||
import { Button } from '../../_shared/Button'
|
import { Button } from '../../_shared/Button'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { follow, unfollow } from '../../../stores/zine/common'
|
import { follow, unfollow } from '../../../stores/zine/common'
|
||||||
import { CheckButton } from '../../_shared/CheckButton'
|
import { CheckButton } from '../../_shared/CheckButton'
|
||||||
|
import { getImageUrl } from '../../../utils/getImageUrl'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
topic: Topic
|
topic: Topic
|
||||||
|
@ -43,7 +43,11 @@ export const TopicBadge = (props: Props) => {
|
||||||
<a
|
<a
|
||||||
href={`/topic/${props.topic.slug}`}
|
href={`/topic/${props.topic.slug}`}
|
||||||
class={clsx(styles.picture, { [styles.withImage]: props.topic.pic })}
|
class={clsx(styles.picture, { [styles.withImage]: props.topic.pic })}
|
||||||
style={props.topic.pic && { 'background-image': `url('${imageProxy(props.topic.pic)}')` }}
|
style={
|
||||||
|
props.topic.pic && {
|
||||||
|
'background-image': `url('${getImageUrl(props.topic.pic, { width: 40, height: 40 })}')`
|
||||||
|
}
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<a href={`/topic/${props.topic.slug}`} class={styles.info}>
|
<a href={`/topic/${props.topic.slug}`} class={styles.info}>
|
||||||
<span class={styles.title}>{props.topic.title}</span>
|
<span class={styles.title}>{props.topic.title}</span>
|
||||||
|
|
|
@ -30,12 +30,14 @@
|
||||||
|
|
||||||
.ratingContainer {
|
.ratingContainer {
|
||||||
@include font-size(1.5rem);
|
@include font-size(1.5rem);
|
||||||
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ratingControl {
|
.ratingControl {
|
||||||
@include font-size(1.5rem);
|
@include font-size(1.5rem);
|
||||||
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
margin-left: 1em;
|
margin-left: 1em;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
@ -103,10 +105,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.longBioExpandedControl {
|
.longBioExpandedControl {
|
||||||
|
@include font-size(1.6rem);
|
||||||
|
|
||||||
border-radius: 1.2rem;
|
border-radius: 1.2rem;
|
||||||
display: block;
|
display: block;
|
||||||
height: auto;
|
height: auto;
|
||||||
@include font-size(1.6rem);
|
|
||||||
padding-bottom: 1.2rem;
|
padding-bottom: 1.2rem;
|
||||||
padding-top: 1.2rem;
|
padding-top: 1.2rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -17,7 +17,6 @@ import { Comment } from '../../Article/Comment'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { AuthorRatingControl } from '../../Author/AuthorRatingControl'
|
import { AuthorRatingControl } from '../../Author/AuthorRatingControl'
|
||||||
import { getPagePath } from '@nanostores/router'
|
import { getPagePath } from '@nanostores/router'
|
||||||
import { useSession } from '../../../context/session'
|
|
||||||
import { Loading } from '../../_shared/Loading'
|
import { Loading } from '../../_shared/Loading'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { ShoutForm, useEditorContext } from '../../context/editor'
|
||||||
import { Editor, Panel } from '../Editor'
|
import { Editor, Panel } from '../Editor'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
import styles from './Edit.module.scss'
|
import styles from './Edit.module.scss'
|
||||||
import { imageProxy } from '../../utils/imageProxy'
|
|
||||||
import { GrowingTextarea } from '../_shared/GrowingTextarea'
|
import { GrowingTextarea } from '../_shared/GrowingTextarea'
|
||||||
import { VideoUploader } from '../Editor/VideoUploader'
|
import { VideoUploader } from '../Editor/VideoUploader'
|
||||||
import { AudioUploader } from '../Editor/AudioUploader'
|
import { AudioUploader } from '../Editor/AudioUploader'
|
||||||
|
@ -24,6 +23,7 @@ import { createStore } from 'solid-js/store'
|
||||||
import SimplifiedEditor from '../Editor/SimplifiedEditor'
|
import SimplifiedEditor from '../Editor/SimplifiedEditor'
|
||||||
import { isDesktop } from '../../utils/media-query'
|
import { isDesktop } from '../../utils/media-query'
|
||||||
import { TableOfContents } from '../TableOfContents'
|
import { TableOfContents } from '../TableOfContents'
|
||||||
|
import { getImageUrl } from '../../utils/getImageUrl'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
shout: Shout
|
shout: Shout
|
||||||
|
@ -362,7 +362,9 @@ export const EditView = (props: Props) => {
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class={styles.cover}
|
class={styles.cover}
|
||||||
style={{ 'background-image': `url(${imageProxy(form.coverImageUrl)})` }}
|
style={{
|
||||||
|
'background-image': `url(${getImageUrl(form.coverImageUrl, { width: 1600 })})`
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { createSignal, onMount, Show } from 'solid-js'
|
||||||
import { TopicSelect, UploadModalContent } from '../../Editor'
|
import { TopicSelect, UploadModalContent } from '../../Editor'
|
||||||
import { Button } from '../../_shared/Button'
|
import { Button } from '../../_shared/Button'
|
||||||
import { hideModal, showModal } from '../../../stores/ui'
|
import { hideModal, showModal } from '../../../stores/ui'
|
||||||
import { imageProxy } from '../../../utils/imageProxy'
|
|
||||||
import { ShoutForm, useEditorContext } from '../../../context/editor'
|
import { ShoutForm, useEditorContext } from '../../../context/editor'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { Modal } from '../../Nav/Modal'
|
import { Modal } from '../../Nav/Modal'
|
||||||
|
@ -20,6 +19,7 @@ import { GrowingTextarea } from '../../_shared/GrowingTextarea'
|
||||||
import { createStore } from 'solid-js/store'
|
import { createStore } from 'solid-js/store'
|
||||||
import { UploadedFile } from '../../../pages/types'
|
import { UploadedFile } from '../../../pages/types'
|
||||||
import SimplifiedEditor, { MAX_DESCRIPTION_LIMIT } from '../../Editor/SimplifiedEditor'
|
import SimplifiedEditor, { MAX_DESCRIPTION_LIMIT } from '../../Editor/SimplifiedEditor'
|
||||||
|
import { Image } from '../../_shared/Image'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
shoutId: number
|
shoutId: number
|
||||||
|
@ -141,11 +141,7 @@ export const PublishSettings = (props: Props) => {
|
||||||
>
|
>
|
||||||
<Show when={settingsForm.coverImageUrl ?? initialData.coverImageUrl}>
|
<Show when={settingsForm.coverImageUrl ?? initialData.coverImageUrl}>
|
||||||
<div class={styles.shoutCardCover}>
|
<div class={styles.shoutCardCover}>
|
||||||
<img
|
<Image src={settingsForm.coverImageUrl} alt={initialData.title} width={1600} />
|
||||||
src={imageProxy(settingsForm.coverImageUrl)}
|
|
||||||
alt={initialData.title}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div class={styles.text}>
|
<div class={styles.text}>
|
||||||
|
|
|
@ -21,9 +21,11 @@
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--background-color-invert);
|
background: var(--background-color-invert);
|
||||||
color: var(--default-color-invert);
|
color: var(--default-color-invert);
|
||||||
|
|
||||||
.check {
|
.check {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close {
|
.close {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { validateFiles } from '../../../utils/validateFile'
|
||||||
import type { FileTypeToUpload } from '../../../pages/types'
|
import type { FileTypeToUpload } from '../../../pages/types'
|
||||||
import { handleFileUpload } from '../../../utils/handleFileUpload'
|
import { handleFileUpload } from '../../../utils/handleFileUpload'
|
||||||
import { UploadedFile } from '../../../pages/types'
|
import { UploadedFile } from '../../../pages/types'
|
||||||
|
import { handleImageUpload } from '../../../utils/handleImageUpload'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
class?: string
|
class?: string
|
||||||
|
@ -30,7 +31,8 @@ export const DropArea = (props: Props) => {
|
||||||
|
|
||||||
const results: UploadedFile[] = []
|
const results: UploadedFile[] = []
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const result = await handleFileUpload(file)
|
const handler = props.fileType === 'image' ? handleImageUpload : handleFileUpload
|
||||||
|
const result = await handler(file)
|
||||||
results.push(result)
|
results.push(result)
|
||||||
}
|
}
|
||||||
props.onUpload(results)
|
props.onUpload(results)
|
||||||
|
|
|
@ -3,18 +3,14 @@
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|
||||||
display: none;
|
display: none;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
max-width: 430px;
|
max-width: 430px;
|
||||||
width: auto;
|
width: auto;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
|
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
border: 2px solid black;
|
border: 2px solid black;
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
import { splitProps } from 'solid-js'
|
import { splitProps } from 'solid-js'
|
||||||
import type { JSX } from 'solid-js'
|
import type { JSX } from 'solid-js'
|
||||||
import { imageProxy } from '../../../utils/imageProxy'
|
import { getImageUrl } from '../../../utils/getImageUrl'
|
||||||
|
|
||||||
export const Image = (props: JSX.ImgHTMLAttributes<HTMLImageElement>) => {
|
type Props = JSX.ImgHTMLAttributes<HTMLImageElement> & {
|
||||||
const [local, others] = splitProps(props, ['src'])
|
width: number
|
||||||
|
alt: string
|
||||||
return <img src={imageProxy(local.src)} {...others} />
|
}
|
||||||
|
|
||||||
|
export const Image = (props: Props) => {
|
||||||
|
const [local, others] = splitProps(props, ['src', 'alt'])
|
||||||
|
|
||||||
|
const src = getImageUrl(local.src, { width: others.width })
|
||||||
|
|
||||||
|
return <img src={src} alt={local.alt} {...others} />
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
|
|
||||||
&.bordered {
|
&.bordered {
|
||||||
@include font-size(1.6rem);
|
@include font-size(1.6rem);
|
||||||
|
|
||||||
border: 2px solid #000;
|
border: 2px solid #000;
|
||||||
padding: 2.4rem;
|
padding: 2.4rem;
|
||||||
|
|
||||||
|
@ -45,6 +46,7 @@
|
||||||
|
|
||||||
&.tiny {
|
&.tiny {
|
||||||
@include font-size(1.4rem);
|
@include font-size(1.4rem);
|
||||||
|
|
||||||
box-shadow: 0 4px 60px rgb(0 0 0 / 10%);
|
box-shadow: 0 4px 60px rgb(0 0 0 / 10%);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
|
||||||
|
|
|
@ -9,14 +9,15 @@ import { createFileUploader } from '@solid-primitives/upload'
|
||||||
import SwiperCore, { Manipulation, Navigation, Pagination } from 'swiper'
|
import SwiperCore, { Manipulation, Navigation, Pagination } from 'swiper'
|
||||||
import { SwiperRef } from './swiper'
|
import { SwiperRef } from './swiper'
|
||||||
import { validateFiles } from '../../../utils/validateFile'
|
import { validateFiles } from '../../../utils/validateFile'
|
||||||
import { handleFileUpload } from '../../../utils/handleFileUpload'
|
|
||||||
import { useSnackbar } from '../../../context/snackbar'
|
import { useSnackbar } from '../../../context/snackbar'
|
||||||
import { Loading } from '../Loading'
|
import { Loading } from '../Loading'
|
||||||
import { imageProxy } from '../../../utils/imageProxy'
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import styles from './Swiper.module.scss'
|
import styles from './Swiper.module.scss'
|
||||||
import { composeMediaItems } from '../../../utils/composeMediaItems'
|
import { composeMediaItems } from '../../../utils/composeMediaItems'
|
||||||
import SimplifiedEditor from '../../Editor/SimplifiedEditor'
|
import SimplifiedEditor from '../../Editor/SimplifiedEditor'
|
||||||
|
import { handleImageUpload } from '../../../utils/handleImageUpload'
|
||||||
|
import { getImageUrl } from '../../../utils/getImageUrl'
|
||||||
|
import { Image } from '../Image'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
images: MediaItem[]
|
images: MediaItem[]
|
||||||
|
@ -95,7 +96,7 @@ export const SolidSwiper = (props: Props) => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const results: UploadedFile[] = []
|
const results: UploadedFile[] = []
|
||||||
for (const file of selectedFiles) {
|
for (const file of selectedFiles) {
|
||||||
const result = await handleFileUpload(file)
|
const result = await handleImageUpload(file)
|
||||||
results.push(result)
|
results.push(result)
|
||||||
}
|
}
|
||||||
props.onImagesAdd(composeMediaItems(results))
|
props.onImagesAdd(composeMediaItems(results))
|
||||||
|
@ -172,7 +173,7 @@ export const SolidSwiper = (props: Props) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
<swiper-slide lazy="true" virtual-index={index()}>
|
<swiper-slide lazy="true" virtual-index={index()}>
|
||||||
<div class={styles.image}>
|
<div class={styles.image}>
|
||||||
<img src={imageProxy(slide.url)} alt={slide.title} />
|
<Image src={slide.url} alt={slide.title} width={1600} />
|
||||||
<Show when={props.editorMode}>
|
<Show when={props.editorMode}>
|
||||||
<Popover content={t('Delete')}>
|
<Popover content={t('Delete')}>
|
||||||
{(triggerRef: (el) => void) => (
|
{(triggerRef: (el) => void) => (
|
||||||
|
@ -232,7 +233,9 @@ export const SolidSwiper = (props: Props) => {
|
||||||
<swiper-slide virtual-index={index()} style={{ width: 'auto', height: 'auto' }}>
|
<swiper-slide virtual-index={index()} style={{ width: 'auto', height: 'auto' }}>
|
||||||
<div
|
<div
|
||||||
class={clsx(styles.imageThumb)}
|
class={clsx(styles.imageThumb)}
|
||||||
style={{ 'background-image': `url(${imageProxy(slide.url)})` }}
|
style={{
|
||||||
|
'background-image': `url(${getImageUrl(slide.url, { width: 110, height: 75 })})`
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Show when={props.editorMode}>
|
<Show when={props.editorMode}>
|
||||||
<div class={styles.thumbAction}>
|
<div class={styles.thumbAction}>
|
||||||
|
|
|
@ -82,6 +82,10 @@ $navigation-reserve: 32px;
|
||||||
|
|
||||||
&.editorMode {
|
&.editorMode {
|
||||||
color: #0d0d0d;
|
color: #0d0d0d;
|
||||||
|
|
||||||
|
.holder {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.action {
|
.action {
|
||||||
|
@ -112,6 +116,7 @@ $navigation-reserve: 32px;
|
||||||
|
|
||||||
.counter {
|
.counter {
|
||||||
@include font-size(1.2rem);
|
@include font-size(1.2rem);
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
top: 477px;
|
top: 477px;
|
||||||
|
@ -139,11 +144,6 @@ $navigation-reserve: 32px;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.editorMode {
|
|
||||||
.holder {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.navigation {
|
.navigation {
|
||||||
background: rgb(0 0 0 / 40%);
|
background: rgb(0 0 0 / 40%);
|
||||||
|
@ -253,7 +253,9 @@ $navigation-reserve: 32px;
|
||||||
background-color: var(--placeholder-color-semi);
|
background-color: var(--placeholder-color-semi);
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
filter: grayscale(1);
|
filter: grayscale(1);
|
||||||
transition: filter 0.3s ease-in-out, opacity 0.5s ease-in-out;
|
transition:
|
||||||
|
filter 0.3s ease-in-out,
|
||||||
|
opacity 0.5s ease-in-out;
|
||||||
|
|
||||||
.thumbAction {
|
.thumbAction {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -18,8 +18,10 @@ export const HomePage = (props: PageProps) => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadShouts({ filters: { visibility: 'public' }, limit: PRERENDERED_ARTICLES_COUNT })
|
await Promise.all([
|
||||||
await loadRandomTopics({ amount: RANDOM_TOPICS_COUNT })
|
loadShouts({ filters: { visibility: 'public' }, limit: PRERENDERED_ARTICLES_COUNT }),
|
||||||
|
loadRandomTopics({ amount: RANDOM_TOPICS_COUNT })
|
||||||
|
])
|
||||||
|
|
||||||
setIsLoaded(true)
|
setIsLoaded(true)
|
||||||
})
|
})
|
||||||
|
|
|
@ -17,6 +17,14 @@ h5 {
|
||||||
margin-top: 3rem;
|
margin-top: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
@include font-size(1.6rem);
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
color: var(--danger-color);
|
||||||
|
margin-top: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
.multipleControlsItem {
|
.multipleControlsItem {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
@ -133,7 +141,9 @@ h5 {
|
||||||
color: #000;
|
color: #000;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0.8em 1em;
|
padding: 0.8em 1em;
|
||||||
transition: background-color 0.3s, color 0.3s;
|
transition:
|
||||||
|
background-color 0.3s,
|
||||||
|
color 0.3s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #000;
|
background: #000;
|
||||||
|
|
|
@ -12,19 +12,20 @@ import { useSession } from '../../context/session'
|
||||||
import FloatingPanel from '../../components/_shared/FloatingPanel/FloatingPanel'
|
import FloatingPanel from '../../components/_shared/FloatingPanel/FloatingPanel'
|
||||||
import { useSnackbar } from '../../context/snackbar'
|
import { useSnackbar } from '../../context/snackbar'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { handleFileUpload } from '../../utils/handleFileUpload'
|
|
||||||
import { Userpic } from '../../components/Author/Userpic'
|
import { Userpic } from '../../components/Author/Userpic'
|
||||||
import { createStore } from 'solid-js/store'
|
import { createStore } from 'solid-js/store'
|
||||||
import { clone } from '../../utils/clone'
|
import { clone } from '../../utils/clone'
|
||||||
import SimplifiedEditor from '../../components/Editor/SimplifiedEditor'
|
import SimplifiedEditor from '../../components/Editor/SimplifiedEditor'
|
||||||
import { GrowingTextarea } from '../../components/_shared/GrowingTextarea'
|
import { GrowingTextarea } from '../../components/_shared/GrowingTextarea'
|
||||||
import { AuthGuard } from '../../components/AuthGuard'
|
import { AuthGuard } from '../../components/AuthGuard'
|
||||||
|
import { handleImageUpload } from '../../utils/handleImageUpload'
|
||||||
|
|
||||||
export const ProfileSettingsPage = () => {
|
export const ProfileSettingsPage = () => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
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 [uploadError, setUploadError] = createSignal(false)
|
||||||
const [isFloatingPanelVisible, setIsFloatingPanelVisible] = createSignal(false)
|
const [isFloatingPanelVisible, setIsFloatingPanelVisible] = createSignal(false)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -63,14 +64,16 @@ export const ProfileSettingsPage = () => {
|
||||||
const { selectFiles } = createFileUploader({ multiple: false, accept: 'image/*' })
|
const { selectFiles } = createFileUploader({ multiple: false, accept: 'image/*' })
|
||||||
|
|
||||||
const handleAvatarClick = async () => {
|
const handleAvatarClick = async () => {
|
||||||
await selectFiles(async ([uploadFile]) => {
|
selectFiles(async ([uploadFile]) => {
|
||||||
try {
|
try {
|
||||||
|
setUploadError(false)
|
||||||
setIsUserpicUpdating(true)
|
setIsUserpicUpdating(true)
|
||||||
const result = await handleFileUpload(uploadFile)
|
const result = await handleImageUpload(uploadFile)
|
||||||
updateFormField('userpic', result.url)
|
updateFormField('userpic', result.url)
|
||||||
setIsUserpicUpdating(false)
|
setIsUserpicUpdating(false)
|
||||||
setIsFloatingPanelVisible(true)
|
setIsFloatingPanelVisible(true)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
setUploadError(true)
|
||||||
console.error('[upload avatar] error', error)
|
console.error('[upload avatar] error', error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -131,6 +134,9 @@ export const ProfileSettingsPage = () => {
|
||||||
onClick={handleAvatarClick}
|
onClick={handleAvatarClick}
|
||||||
loading={isUserpicUpdating()}
|
loading={isUserpicUpdating()}
|
||||||
/>
|
/>
|
||||||
|
<Show when={uploadError()}>
|
||||||
|
<div class={styles.error}>{t('Upload error')}</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<h4>{t('Name')}</h4>
|
<h4>{t('Name')}</h4>
|
||||||
<p class="description">
|
<p class="description">
|
||||||
|
|
|
@ -3,4 +3,7 @@ export const isDev = import.meta.env.MODE === 'development'
|
||||||
const defaultApiUrl = 'https://testapi.discours.io'
|
const defaultApiUrl = 'https://testapi.discours.io'
|
||||||
export const apiBaseUrl = import.meta.env.PUBLIC_API_URL || defaultApiUrl
|
export const apiBaseUrl = import.meta.env.PUBLIC_API_URL || defaultApiUrl
|
||||||
|
|
||||||
|
const defaultThumborUrl = 'https://images.discours.io'
|
||||||
|
export const thumborUrl = import.meta.env.PUBLIC_THUMBOR_URL || defaultThumborUrl
|
||||||
|
|
||||||
export const SENTRY_DSN = import.meta.env.PUBLIC_SENTRY_DSN || ''
|
export const SENTRY_DSN = import.meta.env.PUBLIC_SENTRY_DSN || ''
|
||||||
|
|
29
src/utils/getImageUrl.ts
Normal file
29
src/utils/getImageUrl.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { thumborUrl } from './config'
|
||||||
|
|
||||||
|
const getSizeUrlPart = (options: { width?: number; height?: number } = {}) => {
|
||||||
|
const widthString = options.width ? options.width.toString() : ''
|
||||||
|
const heightString = options.height ? options.height.toString() : ''
|
||||||
|
|
||||||
|
if (!widthString && !heightString) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${widthString}x${heightString}/`
|
||||||
|
}
|
||||||
|
|
||||||
|
// I'm not proud of this
|
||||||
|
export const getImageUrl = (src: string, options: { width?: number; height?: number } = {}) => {
|
||||||
|
const sizeUrlPart = getSizeUrlPart(options)
|
||||||
|
|
||||||
|
if (!src.startsWith(thumborUrl)) {
|
||||||
|
return `${thumborUrl}/unsafe/${sizeUrlPart}${src}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (src.startsWith(`${thumborUrl}/unsafe`)) {
|
||||||
|
const thumborKey = src.replace(`${thumborUrl}/unsafe`, '')
|
||||||
|
return `${thumborUrl}/unsafe/${sizeUrlPart}${thumborKey}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const thumborKey = src.replace(`${thumborUrl}`, '')
|
||||||
|
return `${thumborUrl}/${sizeUrlPart}${thumborKey}`
|
||||||
|
}
|
22
src/utils/handleImageUpload.ts
Normal file
22
src/utils/handleImageUpload.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { UploadFile } from '@solid-primitives/upload'
|
||||||
|
import { UploadedFile } from '../pages/types'
|
||||||
|
import { thumborUrl } from './config'
|
||||||
|
|
||||||
|
export const handleImageUpload = async (uploadFile: UploadFile): Promise<UploadedFile> => {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('media', uploadFile.file, uploadFile.name)
|
||||||
|
const response = await fetch(`${thumborUrl}/image`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
|
||||||
|
const location = response.headers.get('Location')
|
||||||
|
|
||||||
|
const url = `${thumborUrl}/unsafe/production${location.slice(0, location.lastIndexOf('/'))}`
|
||||||
|
const originalFilename = location.slice(location.lastIndexOf('/') + 1)
|
||||||
|
|
||||||
|
return {
|
||||||
|
originalFilename,
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +0,0 @@
|
||||||
import { isDev } from './config'
|
|
||||||
export const imageProxy = (url: string) => {
|
|
||||||
return `${isDev ? 'https://new.discours.io' : ''}/api/image?url=${encodeURI(url)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export const audioProxy = (url: string) => {
|
|
||||||
return `${isDev ? 'https://new.discours.io' : ''}/api/audio?url=${encodeURI(url)}`
|
|
||||||
}
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { UploadedFile } from '../pages/types'
|
import { UploadedFile } from '../pages/types'
|
||||||
import { imageProxy } from './imageProxy'
|
|
||||||
import { hideModal } from '../stores/ui'
|
import { hideModal } from '../stores/ui'
|
||||||
import { Editor } from '@tiptap/core'
|
import { Editor } from '@tiptap/core'
|
||||||
|
|
||||||
|
@ -22,7 +21,7 @@ export const renderUploadedImage = (editor: Editor, image: UploadedFile) => {
|
||||||
{
|
{
|
||||||
type: 'image',
|
type: 'image',
|
||||||
attrs: {
|
attrs: {
|
||||||
src: imageProxy(image.url)
|
src: image.url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in New Issue
Block a user