From a18b6b9e6d3fe6985a49f4671b81d3f3fa36fefb Mon Sep 17 00:00:00 2001 From: Arkadzi Rakouski Date: Sat, 1 Jul 2023 16:05:59 +0300 Subject: [PATCH] feat: audio player (#110) * implement new audio player * refactor audoi player * fix lint errors * fix ts errors * remove unnecc yarn files * refactor by review comments * Article card feed mode * refactor by review comments --------- Co-authored-by: kvakazyambra --- public/icons/lyrics-media.svg | 6 + public/icons/pause.svg | 4 + public/icons/play.svg | 3 + public/icons/player-arrow.svg | 4 + public/icons/share-media.svg | 3 + public/icons/volume.svg | 5 + src/components/Article/Article.module.scss | 31 +- src/components/Article/AudioPlayer.tsx | 66 ---- .../AudioPlayer/AudioPlayer.module.scss | 298 ++++++++++++++++++ .../Article/AudioPlayer/AudioPlayer.tsx | 218 +++++++++++++ .../Article/AudioPlayer/PlayerHeader.tsx | 86 +++++ .../Article/AudioPlayer/PlayerPlaylist.tsx | 64 ++++ src/components/Article/CommentDate.tsx | 1 + src/components/Article/FullArticle.tsx | 86 +++-- src/components/Article/Soundwave.tsx | 109 ------- src/components/Author/Userpic.module.scss | 2 - src/components/Feed/ArticleCard.tsx | 1 - 17 files changed, 758 insertions(+), 229 deletions(-) create mode 100644 public/icons/lyrics-media.svg create mode 100644 public/icons/pause.svg create mode 100644 public/icons/play.svg create mode 100644 public/icons/player-arrow.svg create mode 100644 public/icons/share-media.svg create mode 100644 public/icons/volume.svg delete mode 100644 src/components/Article/AudioPlayer.tsx create mode 100644 src/components/Article/AudioPlayer/AudioPlayer.module.scss create mode 100644 src/components/Article/AudioPlayer/AudioPlayer.tsx create mode 100644 src/components/Article/AudioPlayer/PlayerHeader.tsx create mode 100644 src/components/Article/AudioPlayer/PlayerPlaylist.tsx delete mode 100644 src/components/Article/Soundwave.tsx diff --git a/public/icons/lyrics-media.svg b/public/icons/lyrics-media.svg new file mode 100644 index 00000000..f288d332 --- /dev/null +++ b/public/icons/lyrics-media.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/icons/pause.svg b/public/icons/pause.svg new file mode 100644 index 00000000..385db8a0 --- /dev/null +++ b/public/icons/pause.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/play.svg b/public/icons/play.svg new file mode 100644 index 00000000..681e0ee1 --- /dev/null +++ b/public/icons/play.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/player-arrow.svg b/public/icons/player-arrow.svg new file mode 100644 index 00000000..4f896299 --- /dev/null +++ b/public/icons/player-arrow.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/share-media.svg b/public/icons/share-media.svg new file mode 100644 index 00000000..55e34b3b --- /dev/null +++ b/public/icons/share-media.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/volume.svg b/public/icons/volume.svg new file mode 100644 index 00000000..a86050b0 --- /dev/null +++ b/public/icons/volume.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/Article/Article.module.scss b/src/components/Article/Article.module.scss index b502176e..f5c0a5cf 100644 --- a/src/components/Article/Article.module.scss +++ b/src/components/Article/Article.module.scss @@ -13,7 +13,7 @@ img { max-width: 100%; } -.shoutHeader { +.shoutTopic { margin-bottom: 2em; @include media-breakpoint-up(md) { @@ -21,15 +21,34 @@ img { } } +.shoutHeader { + display: flex; + flex-flow: row wrap; +} + .shoutCover { - background-size: cover; - height: 0; - padding-bottom: 56.2%; + width: 40%; + margin-left: auto; + + & img { + width: 100%; + height: auto; + } + + @include media-breakpoint-down(sm) { + width: 100%; + margin-left: 0; + } } .shoutBody { - font-size: 1.7rem; - line-height: 1.6; + margin-top: 32px; + + font-weight: 400; + font-size: 17px; + line-height: 28px; + letter-spacing: -0.01em; + color: #404040; img { display: block; diff --git a/src/components/Article/AudioPlayer.tsx b/src/components/Article/AudioPlayer.tsx deleted file mode 100644 index 584c9332..00000000 --- a/src/components/Article/AudioPlayer.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { createEffect, createMemo, createSignal, onMount, For } from 'solid-js' - -import type { Shout } from '../../graphql/types.gen' -import { Soundwave } from './Soundwave' - -type MediaItem = any - -export default (props: { shout: Shout }) => { - const media = createMemo(() => { - if (props.shout.media) { - console.debug(props.shout.media) - return [...JSON.parse(props.shout.media)] - } - return [] - }) - let audioRef: HTMLAudioElement - const [currentTrack] = createSignal(media()[0]) - const [paused, setPaused] = createSignal(true) - const togglePlayPause = () => setPaused(!paused()) - const playMedia = (m: MediaItem) => { - audioRef.src = m.get('src') - audioRef.play() - } - const [audioContext, setAudioContext] = createSignal() - onMount(() => setAudioContext(new AudioContext())) - createEffect(() => (paused() ? audioRef.play : audioRef.pause)()) - return ( -
-
- {props.shout.title} -
- -
-
-
{currentTrack().title}
- -
- - {`${audioRef.currentTime} / ${audioRef.duration}`} -
-
- -
    - - {(m: MediaItem) => ( -
  • -
    - playMedia(m)} /> -
    - {m.title} -
  • - )} -
    -
-
-
- ) -} diff --git a/src/components/Article/AudioPlayer/AudioPlayer.module.scss b/src/components/Article/AudioPlayer/AudioPlayer.module.scss new file mode 100644 index 00000000..29ba0a9b --- /dev/null +++ b/src/components/Article/AudioPlayer/AudioPlayer.module.scss @@ -0,0 +1,298 @@ +.allTracks { + color: red; +} + +.playerHeader { + width: 100%; + display: flex; + justify-content: space-between; + + @include media-breakpoint-down(sm) { + flex-direction: column; + } +} + +.playerTitle { + max-width: 50%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + @include media-breakpoint-down(sm) { + max-width: 100%; + } +} + +.playerControls { + display: flex; + min-width: 160px; + align-items: center; + margin-left: auto; + + & > button { + border: none; + cursor: pointer; + + &:disabled { + opacity: 0.5; + } + + &:hover { + opacity: 0.8; + } + } + + @include media-breakpoint-down(sm) { + margin-top: 20px; + margin-left: 0; + } +} + +.playButton { + display: flex; + align-items: center; + justify-content: center; + + width: 40px; + height: 40px; + background-color: #141414; + + & img { + width: 14px; + height: auto; + } +} + +.playButtonInvertPause img { + filter: invert(100%); +} + +.playButtonInvertPlay img { + filter: brightness(130%); +} + +.controlsButton { + margin-left: 19px; + + & img { + width: 15px; + height: auto; + } +} + +.controlsButtonNext { + margin-left: 26px; + transform: rotate(180deg); +} + +.volumeButton { + margin-left: 23px; + + & img { + width: 18px; + height: auto; + } +} + +.timeline { + display: flex; + flex-direction: column; + flex: 1; + align-items: center; + justify-content: space-between; + margin-top: 28px; + padding-left: 10px; +} + +.progress { + position: relative; + width: 100%; + cursor: pointer; + border-bottom: 2px solid #cccccc; +} + +.progressFilled { + position: absolute; + z-index: 2; + box-sizing: border-box; + border-bottom: 4px solid #141414; + + &::after { + content: ''; + display: block; + position: absolute; + bottom: -8px; + right: -8px; + + width: 8px; + height: 8px; + border-radius: 50%; + border: 4px solid #141414; + background-color: #fff; + } +} + +.progressTiming { + width: 100%; + padding-top: 14px; + display: flex; + justify-content: space-between; + + font-weight: 500; + font-size: 12px; + line-height: 16px; + letter-spacing: 0.003em; +} + +.volumeContainer { + position: relative; + display: flex; + align-items: center; +} + +.volume { + position: absolute; + z-index: 2; + right: 0; + bottom: 14px; + + height: 28px; + -webkit-appearance: none; + margin: 10px 0; + width: 80px; + background: transparent; + + &:focus { + outline: none; + } + + &::-moz-range-thumb, + &::-webkit-slider-thumb { + height: 16px; + width: 16px; + border-radius: 100px; + border: none; + cursor: pointer; + + margin-top: -4px; + } + &:focus::-webkit-slider-runnable-track, + &::-moz-range-thumb, + &::-webkit-slider-thumb, + &::-webkit-slider-runnable-track, + &::-moz-range-track, + &::-ms-track, + &::-ms-fill-lower, + &::-ms-fill-upper, + &::-ms-thumb { + background: #0e0e0e; + } + + &::-webkit-slider-thumb { + -webkit-appearance: none; + } + + &::-webkit-slider-runnable-track, + &::-moz-range-track, + &::-ms-track { + width: 100%; + height: 6px; + cursor: pointer; + animate: 0.2s; + border-radius: 10px; + } + + &::-ms-fill-lower, + &::-ms-fill-upper { + border-radius: 10px; + } + + &::-ms-thumb { + margin-top: 1px; + height: 15px; + width: 15px; + border-radius: 5px; + border: none; + cursor: pointer; + } + + &:focus::-ms-fill-lower, + &:focus::-ms-fill-upper { + background: #38bdf8; + } +} + +.playlist { + display: flex; + flex-direction: column; + + list-style-type: none; + margin: 32px 0 58px; + padding: 0; + + & > li { + border-top: 1px solid #e9e9ee; + } +} + +.playlistItem { + display: flex; + align-items: center; + + min-height: 56px; + padding: 16px 0; +} + +.playlistItemPlayButton { + border: none; + cursor: pointer; + width: 14px; + height: auto; +} + +.playlistItemTitle { + max-width: 254px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + margin-left: 17px; + font-weight: 400; + font-size: 16px; + line-height: 22px; + letter-spacing: -0.01em; + color: #000000; +} + +.playlistItemControls { + display: flex; + margin-left: auto; +} + +.playlistItemDuration { + margin-left: 22px; + font-weight: 400; + font-size: 16px; + line-height: 22px; + letter-spacing: -0.01em; + color: #000000; +} + +.timelinePlaceholder { + width: 100%; + height: 67px; +} + +.playerHeaderPlaceholder { + width: 100%; + height: 67px; +} + +.playlistPlaceholder { + width: 100%; + height: 67px; +} + +.shareMedia { + margin-left: auto; +} diff --git a/src/components/Article/AudioPlayer/AudioPlayer.tsx b/src/components/Article/AudioPlayer/AudioPlayer.tsx new file mode 100644 index 00000000..4007593c --- /dev/null +++ b/src/components/Article/AudioPlayer/AudioPlayer.tsx @@ -0,0 +1,218 @@ +import { createEffect, createSignal, onMount, Show } from 'solid-js' + +import { PlayerHeader } from './PlayerHeader' +import { PlayerPlaylist } from './PlayerPlaylist' + +import styles from './AudioPlayer.module.scss' + +export type MediaItem = { + id?: number + body: string + pic: string + title: string + url: string + isCurrent: boolean + isPlaying: boolean +} + +const prepareMedia = (media: MediaItem[]) => + media.map((item, index) => ({ + ...item, + id: index, + isCurrent: false, + isPlaying: false + })) + +const progressUpdate = (audioRef, progressFilledRef, duration) => { + progressFilledRef.style.width = `${(audioRef.currentTime / duration) * 100 || 0}%` +} + +const scrub = (event, progressRef, duration, audioRef) => { + audioRef.currentTime = (event.offsetX / progressRef.offsetWidth) * duration +} + +const getFormattedTime = (point) => new Date(point * 1000).toISOString().slice(14, -5) + +export default (props: { media: MediaItem[]; articleSlug: string; body: string }) => { + let audioRef: HTMLAudioElement + let progressRef: HTMLDivElement + let progressFilledRef: HTMLDivElement + + const [audioContext, setAudioContext] = createSignal() + const [gainNode, setGainNode] = createSignal() + + const [tracks, setTracks] = createSignal(prepareMedia(props.media)) + + const [duration, setDuration] = createSignal(0) + const [currentTimeContent, setCurrentTimeContent] = createSignal('00:00') + const [currentDurationContent, setCurrentDurationContent] = createSignal('00:00') + + const [mousedown, setMousedown] = createSignal(false) + + const getCurrentTrack = () => + tracks().find((track) => track.isCurrent) || + (() => { + setTracks( + tracks().map((track, index) => ({ + ...track, + isCurrent: index === 0 + })) + ) + + return tracks()[0] + })() + + createEffect(() => { + if (audioRef.src !== getCurrentTrack().url) { + audioRef.src = getCurrentTrack().url + + audioRef.load() + } + }) + + createEffect(() => { + if (getCurrentTrack() && duration()) { + setCurrentDurationContent(getFormattedTime(duration())) + } + }) + + const playMedia = async (m: MediaItem) => { + setTracks( + tracks().map((track) => ({ + ...track, + isCurrent: track.id === m.id ? true : false, + isPlaying: track.id === m.id ? !track.isPlaying : false + })) + ) + + progressUpdate(audioRef, progressFilledRef, duration()) + + if (audioContext().state === 'suspended') audioContext().resume() + + if (getCurrentTrack().isPlaying) { + await audioRef.play() + } else { + audioRef.pause() + } + } + + const setTimes = () => { + setCurrentTimeContent(getFormattedTime(audioRef.currentTime)) + } + + const handleAudioEnd = () => { + progressFilledRef.style.width = '0%' + audioRef.currentTime = 0 + } + + const handleAudioTimeUpdate = () => { + progressUpdate(audioRef, progressFilledRef, duration()) + + setTimes() + } + + onMount(() => { + setAudioContext(new AudioContext()) + setGainNode(audioContext().createGain()) + + setTimes() + + const track = audioContext().createMediaElementSource(audioRef) + track.connect(gainNode()).connect(audioContext().destination) + }) + + const playPrevTrack = () => { + const { id } = getCurrentTrack() + const currIndex = tracks().findIndex((track) => track.id === id) + + const getUpdatedStatus = (trackId) => + currIndex === 0 + ? trackId === tracks()[tracks().length - 1].id + : trackId === tracks()[currIndex - 1].id + + setTracks( + tracks().map((track) => ({ + ...track, + isCurrent: getUpdatedStatus(track.id), + isPlaying: getUpdatedStatus(track.id) + })) + ) + } + + const playNextTrack = () => { + const { id } = getCurrentTrack() + const currIndex = tracks().findIndex((track) => track.id === id) + + const getUpdatedStatus = (trackId) => + currIndex === tracks().length - 1 + ? trackId === tracks()[0].id + : trackId === tracks()[currIndex + 1].id + + setTracks( + tracks().map((track) => ({ + ...track, + isCurrent: getUpdatedStatus(track.id), + isPlaying: getUpdatedStatus(track.id) + })) + ) + } + + const handleOnAudioMetadataLoad = ({ target }) => { + setDuration(target.duration) + } + + return ( +
+ + playMedia(getCurrentTrack())} + getCurrentTrack={getCurrentTrack} + playPrevTrack={playPrevTrack} + playNextTrack={playNextTrack} + gainNode={gainNode()} + /> + + + +
+
scrub(e, progressRef, duration(), audioRef)} + onMouseMove={(e) => mousedown() && scrub(e, progressRef, duration(), audioRef)} + onMouseDown={() => setMousedown(true)} + onMouseUp={() => setMousedown(false)} + > +
+
+
+ {currentTimeContent()} + {currentDurationContent()} +
+
+
+ + + + +
+ ) +} diff --git a/src/components/Article/AudioPlayer/PlayerHeader.tsx b/src/components/Article/AudioPlayer/PlayerHeader.tsx new file mode 100644 index 00000000..b5c09d23 --- /dev/null +++ b/src/components/Article/AudioPlayer/PlayerHeader.tsx @@ -0,0 +1,86 @@ +import { createSignal, Show } from 'solid-js' +import { clsx } from 'clsx' + +import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler' + +import { Icon } from '../../_shared/Icon' +import styles from './AudioPlayer.module.scss' + +export const PlayerHeader = (props) => { + let volumeRef: HTMLInputElement + const volumeContainerRef: { current: HTMLDivElement } = { + current: null + } + + const { getCurrentTrack, onPlayMedia, gainNode, playPrevTrack, playNextTrack } = props + + const [isVolumeBarOpened, setIsVolumeBarOpened] = createSignal(false) + + const handleVolumeChange = () => { + gainNode.gain.value = Number(volumeRef.value) + } + + const toggleVolumeBar = () => { + setIsVolumeBarOpened(!isVolumeBarOpened()) + } + + useOutsideClickHandler({ + containerRef: volumeContainerRef, + predicate: () => isVolumeBarOpened(), + handler: () => toggleVolumeBar() + }) + + return ( +
+
{getCurrentTrack().title}
+
+ + + +
(volumeContainerRef.current = el)} class={styles.volumeContainer}> + + + + +
+
+
+ ) +} diff --git a/src/components/Article/AudioPlayer/PlayerPlaylist.tsx b/src/components/Article/AudioPlayer/PlayerPlaylist.tsx new file mode 100644 index 00000000..a780f342 --- /dev/null +++ b/src/components/Article/AudioPlayer/PlayerPlaylist.tsx @@ -0,0 +1,64 @@ +import { For } from 'solid-js' + +import { SharePopup, getShareUrl } from '../SharePopup' +import { getDescription } from '../../../utils/meta' + +import { useLocalize } from '../../../context/localize' + +import type { MediaItem } from './AudioPlayer' + +import { Popover } from '../../_shared/Popover' +import { Icon } from '../../_shared/Icon' + +import styles from './AudioPlayer.module.scss' + +export const PlayerPlaylist = (props) => { + const { t } = useLocalize() + + const { tracks, getCurrentTrack, playMedia, articleSlug, body } = props + + return ( +
    + + {(m: MediaItem) => ( +
  • + +
    {m.title}
    +
    + + {(triggerRef: (el) => void) => ( +
    + + +
    + } + /> +
    + )} + + +
  • + )} +
    +
+ ) +} diff --git a/src/components/Article/CommentDate.tsx b/src/components/Article/CommentDate.tsx index c0dc67f4..148c742b 100644 --- a/src/components/Article/CommentDate.tsx +++ b/src/components/Article/CommentDate.tsx @@ -15,6 +15,7 @@ type Props = { export const CommentDate = (props: Props) => { const { t } = useLocalize() + const formattedDate = (date) => { const formatDateOptions: Intl.DateTimeFormatOptions = props.isShort ? { month: 'long', day: 'numeric', year: 'numeric' } diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index 2dfa9599..f879d1e5 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -1,7 +1,9 @@ +import { createEffect, createMemo, createSignal, For, Match, onMount, Show, Switch } from 'solid-js' + import { capitalize, formatDate } from '../../utils' import { Icon } from '../_shared/Icon' import { AuthorCard } from '../Author/AuthorCard' -import { createEffect, createMemo, createSignal, For, Match, onMount, Show, Switch } from 'solid-js' +import AudioPlayer from './AudioPlayer/AudioPlayer' import type { Author, Shout } from '../../graphql/types.gen' import MD from './MD' import { SharePopup } from './SharePopup' @@ -93,13 +95,8 @@ export const FullArticle = (props: ArticleProps) => { }, 'bookmark') } - const body = createMemo(() => props.article.body) - - const media = createMemo(() => { - const mi = JSON.parse(props.article.media || '[]') - console.debug('!!! media items', mi) - return mi - }) + const media = createMemo(() => JSON.parse(props.article.media || '[]')) + const body = createMemo(() => props.article.body || '') const commentsRef: { current: HTMLDivElement } = { current: null } const scrollToComments = () => { @@ -144,7 +141,7 @@ export const FullArticle = (props: ArticleProps) => {
diff --git a/src/components/Article/Soundwave.tsx b/src/components/Article/Soundwave.tsx deleted file mode 100644 index ba04b9d0..00000000 --- a/src/components/Article/Soundwave.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { onMount } from 'solid-js' - -/** - * A utility function for drawing our line segments - * @param {AudioContext} ctx the audio context - * @param {number} x the x coordinate of the beginning of the line segment - * @param {number} height the desired height of the line segment - * @param {number} width the desired width of the line segment - * @param {boolean} isEven whether or not the segmented is even-numbered - */ -const drawLineSegment = (ctx, x, height, width, isEven) => { - ctx.lineWidth = 1 // how thick the line is - ctx.strokeStyle = '#fff' // what color our line is - ctx.beginPath() - const h = isEven ? height : -height - ctx.moveTo(x, 0) - ctx.lineTo(x, h) - ctx.arc(x + width / 2, h, width / 2, Math.PI, 0, isEven) - ctx.lineTo(x + width, 0) - ctx.stroke() -} - -/** - * Filters the AudioBuffer retrieved from an external source - * @param {AudioBuffer} audioBuffer the AudioBuffer from drawAudio() - * @returns {Array} an array of floating point numbers - */ -const filterData = (audioBuffer) => { - const rawData = audioBuffer.getChannelData(0) // We only need to work with one channel of data - const samples = 70 // Number of samples we want to have in our final data set - const blockSize = Math.floor(rawData.length / samples) // the number of samples in each subdivision - const filteredData = [] - for (let i = 0; i < samples; i++) { - const blockStart = blockSize * i // the location of the first sample in the block - let sum = 0 - for (let j = 0; j < blockSize; j++) { - sum = sum + Math.abs(rawData[blockStart + j]) // find the sum of all the samples in the block - } - filteredData.push(sum / blockSize) // divide the sum by the block size to get the average - } - return filteredData -} - -/** - * Normalizes the audio data to make a cleaner illustration - * @param {Array} filteredData the data from filterData() - * @returns {Array} an normalized array of floating point numbers - */ -const normalizeData = (filteredData) => { - const multiplier = Math.pow(Math.max(...filteredData), -1) - return filteredData.map((n) => n * multiplier) -} - -interface SoundwaveProps { - url: string - context: AudioContext -} - -export const Soundwave = (props: SoundwaveProps) => { - let canvasRef: HTMLCanvasElement - - /** - * Draws the audio file into a canvas element. - * @param {Array} normalizedData The filtered array returned from filterData() - * @returns {Array} a normalized array of data - */ - const draw = (normalizedData) => { - // set up the canvas - const canvas = canvasRef - const dpr = window.devicePixelRatio || 1 - const padding = 20 - canvas.width = canvas.offsetWidth * dpr - canvas.height = (canvas.offsetHeight + padding * 2) * dpr - const ctx = canvas.getContext('2d') - ctx.scale(dpr, dpr) - ctx.translate(0, canvas.offsetHeight / 2 + padding) // set Y = 0 to be in the middle of the canvas - - // draw the line segments - const width = canvas.offsetWidth / normalizedData.length - // eslint-disable-next-line unicorn/no-for-loop - for (let i = 0; i < normalizedData.length; i++) { - const x = width * i - let height = normalizedData[i] * canvas.offsetHeight - padding - if (height < 0) { - height = 0 - } else if (height > canvas.offsetHeight / 2) { - height = height - canvas.offsetHeight / 2 - } - drawLineSegment(ctx, x, height, width, (i + 1) % 2) - } - } - - /** - * Retrieves audio from an external source, the initializes the drawing function - * @param {AudioContext} audioContext the audio context - * @param {String} url the url of the audio we'd like to fetch - */ - const drawAudio = (audioContext, url) => { - fetch(url) - .then((response) => response.arrayBuffer()) - .then((arrayBuffer) => audioContext.decodeAudioData(arrayBuffer)) - .then((audioBuffer) => draw(normalizeData(filterData(audioBuffer)))) - .catch(console.error) - } - onMount(() => { - drawAudio(props.context, props.url) - }) - return -} diff --git a/src/components/Author/Userpic.module.scss b/src/components/Author/Userpic.module.scss index 94fc7f63..8d6c541e 100644 --- a/src/components/Author/Userpic.module.scss +++ b/src/components/Author/Userpic.module.scss @@ -78,8 +78,6 @@ .feedMode { .userpic { - font-size: 0.8rem; - line-height: 12px; min-width: 16px; max-width: 16px; } diff --git a/src/components/Feed/ArticleCard.tsx b/src/components/Feed/ArticleCard.tsx index 9ca6a19f..7ba125f3 100644 --- a/src/components/Feed/ArticleCard.tsx +++ b/src/components/Feed/ArticleCard.tsx @@ -196,7 +196,6 @@ export const ArticleCard = (props: ArticleCardProps) => { >