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:
Ilya Y 2023-10-27 21:50:13 +03:00 committed by GitHub
parent 933a2bb71a
commit a54d592038
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 186 additions and 141 deletions

View File

@ -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')
}
}

View File

@ -34,8 +34,7 @@
"i18next-icu": "2.3.0",
"intl-messageformat": "10.5.3",
"just-throttle": "4.2.0",
"mailgun.js": "8.2.1",
"node-fetch": "3.3.1"
"mailgun.js": "8.2.1"
},
"devDependencies": {
"@babel/core": "7.21.8",

View File

@ -1,11 +1,11 @@
import { clsx } from 'clsx'
import styles from './AudioHeader.module.scss'
import { imageProxy } from '../../../utils/imageProxy'
import { MediaItem } from '../../../pages/types'
import { createSignal, Show } from 'solid-js'
import { Icon } from '../../_shared/Icon'
import { Topic } from '../../../graphql/types.gen'
import { CardTopic } from '../../Feed/CardTopic'
import { Image } from '../../_shared/Image'
type Props = {
title: string
@ -19,7 +19,7 @@ export const AudioHeader = (props: Props) => {
return (
<div class={clsx(styles.AudioHeader, { [styles.expandedImage]: expandedImage() })}>
<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}>
<button type="button" class={styles.expand} onClick={() => setExpandedImage(!expandedImage())}>
<Icon name="expand-circle" />

View File

@ -3,7 +3,6 @@ import { PlayerHeader } from './PlayerHeader'
import { PlayerPlaylist } from './PlayerPlaylist'
import styles from './AudioPlayer.module.scss'
import { MediaItem } from '../../../pages/types'
import { imageProxy } from '../../../utils/imageProxy'
type Props = {
media: MediaItem[]
@ -145,8 +144,7 @@ export const AudioPlayer = (props: Props) => {
<audio
ref={(el) => (audioRef.current = el)}
onTimeUpdate={handleAudioTimeUpdate}
// TEMP SOLUTION for http/https
src={currentTack().url.startsWith('https') ? currentTack().url : imageProxy(currentTack().url)}
src={currentTack().url}
onCanPlay={() => {
// start to play the next track on src change
if (isPlaying()) {

View File

@ -10,7 +10,6 @@ import { useReactions } from '../../context/reactions'
import { MediaItem } from '../../pages/types'
import { DEFAULT_HEADER_OFFSET, router, useRouter } from '../../stores/router'
import { getDescription } from '../../utils/meta'
import { imageProxy } from '../../utils/imageProxy'
import { TableOfContents } from '../TableOfContents'
import { AudioPlayer } from './AudioPlayer'
import { SharePopup } from './SharePopup'
@ -26,6 +25,7 @@ import styles from './Article.module.scss'
import { CardTopic } from '../Feed/CardTopic'
import { createPopper } from '@popperjs/core'
import { AuthorBadge } from '../Author/AuthorBadge'
import { getImageUrl } from '../../utils/getImageUrl'
type Props = {
article: Shout
@ -266,7 +266,9 @@ export const FullArticle = (props: Props) => {
>
<div
class={styles.shoutCover}
style={{ 'background-image': `url('${imageProxy(props.article.cover)}')` }}
style={{
'background-image': `url('${getImageUrl(props.article.cover, { width: 1600 })}')`
}}
/>
</Show>
</div>

View File

@ -28,13 +28,6 @@
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:visited {
border: none;

View File

@ -1,9 +1,9 @@
import { createSignal, Show } from 'solid-js'
import { createMemo, Show } from 'solid-js'
import styles from './Userpic.module.scss'
import { clsx } from 'clsx'
import { imageProxy } from '../../../utils/imageProxy'
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
import { Loading } from '../../_shared/Loading'
import { Image } from '../../_shared/Image'
type Props = {
name: string
@ -17,38 +17,31 @@ type Props = {
}
export const Userpic = (props: Props) => {
const [userpicUrl, setUserpicUrl] = createSignal<string>()
const letters = () => {
if (!props.name) return
const names = props.name ? props.name.split(' ') : []
return names[0][0] + (names.length > 1 ? names[1][0] : '')
}
const comutedAvatarSize = () => {
const avatarSize = createMemo(() => {
switch (props.size) {
case 'XS': {
return '40x40'
return 40
}
case 'S': {
return '56x56'
return 56
}
case 'L': {
return '80x80'
return 80
}
case 'XL': {
return '336x336'
return 336
}
default: {
return '64x64'
return 64
}
}
}
setUserpicUrl(
props.userpic && props.userpic.includes('100x')
? props.userpic.replace('100x', comutedAvatarSize())
: props.userpic
)
})
return (
<div
@ -62,18 +55,8 @@ export const Userpic = (props: Props) => {
condition={props.hasLink}
wrapper={(children) => <a href={`/author/${props.slug}`}>{children}</a>}
>
<Show
when={!props.userpic}
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 when={props.userpic} fallback={<div class={styles.letters}>{letters()}</div>}>
<Image src={props.userpic} width={avatarSize()} height={avatarSize()} alt={props.name} />
</Show>
</ConditionalWrapper>
</Show>

View File

@ -44,9 +44,8 @@ import { EditorFloatingMenu } from './EditorFloatingMenu'
import './Prosemirror.scss'
import { Image } from '@tiptap/extension-image'
import { Footnote } from './extensions/Footnote'
import { handleFileUpload } from '../../utils/handleFileUpload'
import { imageProxy } from '../../utils/imageProxy'
import { useSnackbar } from '../../context/snackbar'
import { handleImageUpload } from '../../utils/handleImageUpload'
type Props = {
shoutId: number
@ -154,7 +153,7 @@ export const Editor = (props: Props) => {
}
showSnackbar({ body: t('Uploading image') })
const result = await handleFileUpload(uplFile)
const result = await handleImageUpload(uplFile)
editor()
.chain()
@ -174,7 +173,7 @@ export const Editor = (props: Props) => {
{
type: 'image',
attrs: {
src: imageProxy(result.url)
src: result.url
}
}
]

View File

@ -21,7 +21,6 @@ import { Modal } from '../Nav/Modal'
import { hideModal, showModal } from '../../stores/ui'
import { Blockquote } from '@tiptap/extension-blockquote'
import { UploadModalContent } from './UploadModalContent'
import { imageProxy } from '../../utils/imageProxy'
import { clsx } from 'clsx'
import styles from './SimplifiedEditor.module.scss'
import { Placeholder } from '@tiptap/extension-placeholder'
@ -174,7 +173,7 @@ const SimplifiedEditor = (props: Props) => {
{
type: 'image',
attrs: {
src: imageProxy(image.url)
src: image.url
}
}
]

View File

@ -6,11 +6,11 @@ import { createSignal, Show } from 'solid-js'
import { InlineForm } from '../InlineForm'
import { hideModal } from '../../../stores/ui'
import { createDropzone, createFileUploader, UploadFile } from '@solid-primitives/upload'
import { handleFileUpload } from '../../../utils/handleFileUpload'
import { useLocalize } from '../../../context/localize'
import { Loading } from '../../_shared/Loading'
import { verifyImg } from '../../../utils/verifyImg'
import { UploadedFile } from '../../../pages/types'
import { handleImageUpload } from '../../../utils/handleImageUpload'
type Props = {
onClose: (image?: UploadedFile) => void
@ -24,10 +24,10 @@ export const UploadModalContent = (props: Props) => {
const [dragError, setDragError] = createSignal<string | undefined>()
const { selectFiles } = createFileUploader({ multiple: false, accept: 'image/*' })
const runUpload = async (file) => {
const runUpload = async (file: UploadFile) => {
try {
setIsUploading(true)
const result = await handleFileUpload(file)
const result = await handleImageUpload(file)
props.onClose(result)
setIsUploading(false)
} catch (error) {
@ -41,7 +41,7 @@ export const UploadModalContent = (props: Props) => {
try {
const data = await fetch(value)
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 = {
source: blob.toString(),
name: file.name,
@ -55,7 +55,7 @@ export const UploadModalContent = (props: Props) => {
}
const handleUpload = async () => {
await selectFiles(async ([uploadFile]) => {
selectFiles(async ([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') {
setDragActive(true)
} 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 (
<div class={styles.uploadModalContent}>
<Show when={!isUploading()} fallback={<Loading />}>
@ -113,7 +122,7 @@ export const UploadModalContent = (props: Props) => {
hideModal()
props.onClose()
}}
validate={async (value) => ((await verifyImg(value)) ? '' : t('Invalid image URL'))}
validate={handleValidate}
onSubmit={handleImageFormSubmit}
/>
</div>

View File

@ -117,6 +117,7 @@
.shoutAuthor {
@include font-size(1.4rem);
font-weight: 500;
margin-right: 1.6rem;

View File

@ -2,21 +2,21 @@ import { createMemo, createSignal, For, Show } from 'solid-js'
import type { Shout } from '../../../graphql/types.gen'
import { capitalize } from '../../../utils/capitalize'
import { Icon } from '../../_shared/Icon'
import styles from './ArticleCard.module.scss'
import { clsx } from 'clsx'
import { CardTopic } from '../CardTopic'
import { ShoutRatingControl } from '../../Article/ShoutRatingControl'
import { getShareUrl, SharePopup } from '../../Article/SharePopup'
import stylesHeader from '../../Nav/Header/Header.module.scss'
import { getDescription } from '../../../utils/meta'
import { FeedArticlePopup } from '../FeedArticlePopup'
import { useLocalize } from '../../../context/localize'
import { getPagePath, openPage } from '@nanostores/router'
import { router, useRouter } from '../../../stores/router'
import { imageProxy } from '../../../utils/imageProxy'
import { Popover } from '../../_shared/Popover'
import { Image } from '../../_shared/Image'
import { useSession } from '../../../context/session'
import { AuthorLink } from '../../Author/AhtorLink'
import stylesHeader from '../../Nav/Header/Header.module.scss'
import styles from './ArticleCard.module.scss'
interface ArticleCardProps {
settings?: {
@ -118,7 +118,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
<div class={styles.shoutCardCoverContainer}>
<div class={styles.shoutCardCover}>
<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>
</div>
</div>
@ -211,7 +211,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
</div>
</Show>
<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>
</Show>

View File

@ -2,7 +2,7 @@ import { Show, createMemo } from 'solid-js'
import './DialogCard.module.scss'
import styles from './DialogAvatar.module.scss'
import { clsx } from 'clsx'
import { imageProxy } from '../../utils/imageProxy'
import { getImageUrl } from '../../utils/getImageUrl'
type Props = {
name: string
@ -47,7 +47,10 @@ const DialogAvatar = (props: Props) => {
style={{ 'background-color': `${randomBg()}` }}
>
<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>
</div>
)

View File

@ -114,6 +114,7 @@
.mainNavigationWrapper {
@include font-size(1.7rem);
position: relative;
@include media-breakpoint-down(lg) {
@ -127,7 +128,7 @@
.mainNavigation {
font-size: 1.4rem !important;
//margin: 0 0 0 -0.4rem !important;
// margin: 0 0 0 -0.4rem !important;
opacity: 1;
transition: opacity 0.3s;
@ -202,7 +203,7 @@
li {
margin-bottom: 0 !important;
&:first-letter {
&::first-letter {
text-transform: capitalize;
}
}
@ -293,7 +294,7 @@
.burgerContainer {
box-sizing: content-box;
display: inline-flex;
//float: right;
// float: right;
padding-left: 0;
@include media-breakpoint-up(sm) {
@ -383,6 +384,7 @@
.articleHeader {
@include font-size(1.4rem);
left: $container-padding-x;
margin: 0.2em 0;
overflow: hidden;
@ -624,7 +626,7 @@
}
a:hover {
//background-color: var(--link-hover-background) !important;
// background-color: var(--link-hover-background) !important;
}
}

View File

@ -2,12 +2,12 @@ import { clsx } from 'clsx'
import styles from './TopicBadge.module.scss'
import { FollowingEntity, Topic } from '../../../graphql/types.gen'
import { createMemo, createSignal, Show } from 'solid-js'
import { imageProxy } from '../../../utils/imageProxy'
import { Button } from '../../_shared/Button'
import { useSession } from '../../../context/session'
import { useLocalize } from '../../../context/localize'
import { follow, unfollow } from '../../../stores/zine/common'
import { CheckButton } from '../../_shared/CheckButton'
import { getImageUrl } from '../../../utils/getImageUrl'
type Props = {
topic: Topic
@ -43,7 +43,11 @@ export const TopicBadge = (props: Props) => {
<a
href={`/topic/${props.topic.slug}`}
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}>
<span class={styles.title}>{props.topic.title}</span>

View File

@ -30,12 +30,14 @@
.ratingContainer {
@include font-size(1.5rem);
display: inline-flex;
vertical-align: top;
}
.ratingControl {
@include font-size(1.5rem);
display: inline-flex;
margin-left: 1em;
vertical-align: middle;
@ -103,10 +105,11 @@
}
.longBioExpandedControl {
@include font-size(1.6rem);
border-radius: 1.2rem;
display: block;
height: auto;
@include font-size(1.6rem);
padding-bottom: 1.2rem;
padding-top: 1.2rem;
position: relative;

View File

@ -17,7 +17,6 @@ import { Comment } from '../../Article/Comment'
import { useLocalize } from '../../../context/localize'
import { AuthorRatingControl } from '../../Author/AuthorRatingControl'
import { getPagePath } from '@nanostores/router'
import { useSession } from '../../../context/session'
import { Loading } from '../../_shared/Loading'
type Props = {

View File

@ -8,7 +8,6 @@ import { ShoutForm, useEditorContext } from '../../context/editor'
import { Editor, Panel } from '../Editor'
import { Icon } from '../_shared/Icon'
import styles from './Edit.module.scss'
import { imageProxy } from '../../utils/imageProxy'
import { GrowingTextarea } from '../_shared/GrowingTextarea'
import { VideoUploader } from '../Editor/VideoUploader'
import { AudioUploader } from '../Editor/AudioUploader'
@ -24,6 +23,7 @@ import { createStore } from 'solid-js/store'
import SimplifiedEditor from '../Editor/SimplifiedEditor'
import { isDesktop } from '../../utils/media-query'
import { TableOfContents } from '../TableOfContents'
import { getImageUrl } from '../../utils/getImageUrl'
type Props = {
shout: Shout
@ -362,7 +362,9 @@ export const EditView = (props: Props) => {
>
<div
class={styles.cover}
style={{ 'background-image': `url(${imageProxy(form.coverImageUrl)})` }}
style={{
'background-image': `url(${getImageUrl(form.coverImageUrl, { width: 1600 })})`
}}
/>
</Show>
</Show>

View File

@ -4,7 +4,6 @@ import { createSignal, onMount, Show } from 'solid-js'
import { TopicSelect, UploadModalContent } from '../../Editor'
import { Button } from '../../_shared/Button'
import { hideModal, showModal } from '../../../stores/ui'
import { imageProxy } from '../../../utils/imageProxy'
import { ShoutForm, useEditorContext } from '../../../context/editor'
import { useLocalize } from '../../../context/localize'
import { Modal } from '../../Nav/Modal'
@ -20,6 +19,7 @@ import { GrowingTextarea } from '../../_shared/GrowingTextarea'
import { createStore } from 'solid-js/store'
import { UploadedFile } from '../../../pages/types'
import SimplifiedEditor, { MAX_DESCRIPTION_LIMIT } from '../../Editor/SimplifiedEditor'
import { Image } from '../../_shared/Image'
type Props = {
shoutId: number
@ -141,11 +141,7 @@ export const PublishSettings = (props: Props) => {
>
<Show when={settingsForm.coverImageUrl ?? initialData.coverImageUrl}>
<div class={styles.shoutCardCover}>
<img
src={imageProxy(settingsForm.coverImageUrl)}
alt={initialData.title}
loading="lazy"
/>
<Image src={settingsForm.coverImageUrl} alt={initialData.title} width={1600} />
</div>
</Show>
<div class={styles.text}>

View File

@ -21,9 +21,11 @@
&:hover {
background: var(--background-color-invert);
color: var(--default-color-invert);
.check {
display: none;
}
.close {
display: block;
}

View File

@ -7,6 +7,7 @@ import { validateFiles } from '../../../utils/validateFile'
import type { FileTypeToUpload } from '../../../pages/types'
import { handleFileUpload } from '../../../utils/handleFileUpload'
import { UploadedFile } from '../../../pages/types'
import { handleImageUpload } from '../../../utils/handleImageUpload'
type Props = {
class?: string
@ -30,7 +31,8 @@ export const DropArea = (props: Props) => {
const results: UploadedFile[] = []
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)
}
props.onUpload(results)

View File

@ -3,18 +3,14 @@
bottom: 20px;
left: 0;
right: 0;
display: none;
align-items: center;
justify-content: space-between;
max-width: 430px;
width: auto;
height: auto;
margin: 0 auto;
padding: 14px;
background-color: var(--background-color);
border: 2px solid black;

View File

@ -1,9 +1,16 @@
import { splitProps } 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>) => {
const [local, others] = splitProps(props, ['src'])
return <img src={imageProxy(local.src)} {...others} />
type Props = JSX.ImgHTMLAttributes<HTMLImageElement> & {
width: number
alt: string
}
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} />
}

View File

@ -31,6 +31,7 @@
&.bordered {
@include font-size(1.6rem);
border: 2px solid #000;
padding: 2.4rem;
@ -45,6 +46,7 @@
&.tiny {
@include font-size(1.4rem);
box-shadow: 0 4px 60px rgb(0 0 0 / 10%);
padding: 1rem;

View File

@ -9,14 +9,15 @@ import { createFileUploader } from '@solid-primitives/upload'
import SwiperCore, { Manipulation, Navigation, Pagination } from 'swiper'
import { SwiperRef } from './swiper'
import { validateFiles } from '../../../utils/validateFile'
import { handleFileUpload } from '../../../utils/handleFileUpload'
import { useSnackbar } from '../../../context/snackbar'
import { Loading } from '../Loading'
import { imageProxy } from '../../../utils/imageProxy'
import { clsx } from 'clsx'
import styles from './Swiper.module.scss'
import { composeMediaItems } from '../../../utils/composeMediaItems'
import SimplifiedEditor from '../../Editor/SimplifiedEditor'
import { handleImageUpload } from '../../../utils/handleImageUpload'
import { getImageUrl } from '../../../utils/getImageUrl'
import { Image } from '../Image'
type Props = {
images: MediaItem[]
@ -95,7 +96,7 @@ export const SolidSwiper = (props: Props) => {
setLoading(true)
const results: UploadedFile[] = []
for (const file of selectedFiles) {
const result = await handleFileUpload(file)
const result = await handleImageUpload(file)
results.push(result)
}
props.onImagesAdd(composeMediaItems(results))
@ -172,7 +173,7 @@ export const SolidSwiper = (props: Props) => {
// @ts-ignore
<swiper-slide lazy="true" virtual-index={index()}>
<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}>
<Popover content={t('Delete')}>
{(triggerRef: (el) => void) => (
@ -232,7 +233,9 @@ export const SolidSwiper = (props: Props) => {
<swiper-slide virtual-index={index()} style={{ width: 'auto', height: 'auto' }}>
<div
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}>
<div class={styles.thumbAction}>

View File

@ -82,6 +82,10 @@ $navigation-reserve: 32px;
&.editorMode {
color: #0d0d0d;
.holder {
width: 100%;
}
}
.action {
@ -112,6 +116,7 @@ $navigation-reserve: 32px;
.counter {
@include font-size(1.2rem);
position: absolute;
z-index: 2;
top: 477px;
@ -139,11 +144,6 @@ $navigation-reserve: 32px;
display: flex;
}
}
&.editorMode {
.holder {
width: 100%;
}
}
.navigation {
background: rgb(0 0 0 / 40%);
@ -253,7 +253,9 @@ $navigation-reserve: 32px;
background-color: var(--placeholder-color-semi);
opacity: 0.5;
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 {
display: none;

View File

@ -18,8 +18,10 @@ export const HomePage = (props: PageProps) => {
return
}
await loadShouts({ filters: { visibility: 'public' }, limit: PRERENDERED_ARTICLES_COUNT })
await loadRandomTopics({ amount: RANDOM_TOPICS_COUNT })
await Promise.all([
loadShouts({ filters: { visibility: 'public' }, limit: PRERENDERED_ARTICLES_COUNT }),
loadRandomTopics({ amount: RANDOM_TOPICS_COUNT })
])
setIsLoaded(true)
})

View File

@ -17,6 +17,14 @@ h5 {
margin-top: 3rem;
}
.error {
@include font-size(1.6rem);
text-align: center;
color: var(--danger-color);
margin-top: 1.6rem;
}
.multipleControlsItem {
position: relative;
@ -133,7 +141,9 @@ h5 {
color: #000;
display: flex;
padding: 0.8em 1em;
transition: background-color 0.3s, color 0.3s;
transition:
background-color 0.3s,
color 0.3s;
&:hover {
background: #000;

View File

@ -12,19 +12,20 @@ import { useSession } from '../../context/session'
import FloatingPanel from '../../components/_shared/FloatingPanel/FloatingPanel'
import { useSnackbar } from '../../context/snackbar'
import { useLocalize } from '../../context/localize'
import { handleFileUpload } from '../../utils/handleFileUpload'
import { Userpic } from '../../components/Author/Userpic'
import { createStore } from 'solid-js/store'
import { clone } from '../../utils/clone'
import SimplifiedEditor from '../../components/Editor/SimplifiedEditor'
import { GrowingTextarea } from '../../components/_shared/GrowingTextarea'
import { AuthGuard } from '../../components/AuthGuard'
import { handleImageUpload } from '../../utils/handleImageUpload'
export const ProfileSettingsPage = () => {
const { t } = useLocalize()
const [addLinkForm, setAddLinkForm] = createSignal<boolean>(false)
const [incorrectUrl, setIncorrectUrl] = createSignal<boolean>(false)
const [isUserpicUpdating, setIsUserpicUpdating] = createSignal(false)
const [uploadError, setUploadError] = createSignal(false)
const [isFloatingPanelVisible, setIsFloatingPanelVisible] = createSignal(false)
const {
@ -63,14 +64,16 @@ export const ProfileSettingsPage = () => {
const { selectFiles } = createFileUploader({ multiple: false, accept: 'image/*' })
const handleAvatarClick = async () => {
await selectFiles(async ([uploadFile]) => {
selectFiles(async ([uploadFile]) => {
try {
setUploadError(false)
setIsUserpicUpdating(true)
const result = await handleFileUpload(uploadFile)
const result = await handleImageUpload(uploadFile)
updateFormField('userpic', result.url)
setIsUserpicUpdating(false)
setIsFloatingPanelVisible(true)
} catch (error) {
setUploadError(true)
console.error('[upload avatar] error', error)
}
})
@ -131,6 +134,9 @@ export const ProfileSettingsPage = () => {
onClick={handleAvatarClick}
loading={isUserpicUpdating()}
/>
<Show when={uploadError()}>
<div class={styles.error}>{t('Upload error')}</div>
</Show>
</div>
<h4>{t('Name')}</h4>
<p class="description">

View File

@ -3,4 +3,7 @@ export const isDev = import.meta.env.MODE === 'development'
const defaultApiUrl = 'https://testapi.discours.io'
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 || ''

29
src/utils/getImageUrl.ts Normal file
View 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}`
}

View 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
}
}

View File

@ -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)}`
}

View File

@ -1,5 +1,4 @@
import { UploadedFile } from '../pages/types'
import { imageProxy } from './imageProxy'
import { hideModal } from '../stores/ui'
import { Editor } from '@tiptap/core'
@ -22,7 +21,7 @@ export const renderUploadedImage = (editor: Editor, image: UploadedFile) => {
{
type: 'image',
attrs: {
src: imageProxy(image.url)
src: image.url
}
}
]