Skip to content

Commit

Permalink
feat: add articles audio player (#305)
Browse files Browse the repository at this point in the history
  • Loading branch information
shahin-hq authored Oct 31, 2023
1 parent 9f3db89 commit 75fa393
Show file tree
Hide file tree
Showing 12 changed files with 169 additions and 3 deletions.
2 changes: 2 additions & 0 deletions app/Data/Articles/ArticleData.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public function __construct(
public string $slug,
#[LiteralTypeScriptType('App.Enums.ArticleCategoryEnum')]
public ArticleCategoryEnum $category,
public ?string $audioSrc,
public string $content,
/** @var array{ small: string, small2x: string, medium: string, medium2x: string, large: string, large2x: string } */
#[LiteralTypeScriptType('{ small: string, small2x: string, medium: string, medium2x: string, large: string, large2x: string }')]
Expand All @@ -53,6 +54,7 @@ public static function fromModel(Article $article): self
title: $article->title,
slug: $article->slug,
category: $article->category,
audioSrc: $article->audio_file_url,
content: $article->content,
image: [
'small' => $article->getFirstMediaUrl('cover', 'small'),
Expand Down
1 change: 1 addition & 0 deletions lang/en/pages.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
'header_title' => 'Explore our collection of',
'header_suffix_one' => 'published article',
'header_suffix_other' => 'published articles',
'audio_version' => 'Audio version',
'consists_of_collections' => '{0} This article highlights :count collections|{1} This article highlights :count collection|[2,*] This article highlights :count collections',
],
'collections' => [
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@
"string-hash": "^1.1.3",
"tailwind-merge": "^1.14.0",
"tippy.js": "^6.3.7",
"unified": "^11.0.3"
"unified": "^11.0.3",
"wavesurfer.js": "^7.4.2"
}
}
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions resources/icons/audio-pause.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions resources/icons/audio-play.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion resources/js/I18n/Locales/en.json

Large diffs are not rendered by default.

138 changes: 138 additions & 0 deletions resources/js/Pages/Articles/Components/WaveSurferPlayer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import React, { type RefObject, useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import WaveSurfer, { type WaveSurferOptions } from "wavesurfer.js";
import { Button } from "@/Components/Buttons";
import { Icon } from "@/Components/Icon";

const useWavesurfer = (containerReference: RefObject<HTMLElement | null>, url?: string): WaveSurfer | null => {
const [wavesurfer, setWavesurfer] = useState<WaveSurfer | null>(null);

useEffect(() => {
if (containerReference.current === null) return;

const ws = WaveSurfer.create({
url,
container: containerReference.current,
barWidth: 2,
barGap: 2,
barRadius: 10,
progressColor: "#212B83",
height: 24,
cursorWidth: 0,
waveColor: "#CFD4FF",
dragToSeek: true,
hideScrollbar: true,
normalize: true,
});

setWavesurfer(ws);

return () => {
ws.destroy();
};
}, [containerReference]);

return wavesurfer;
};

const formatDuration = (time: number): string => {
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
};

export const WaveSurferPlayer = (properties: Pick<WaveSurferOptions, "url">): JSX.Element => {
const containerReference = useRef<HTMLDivElement | null>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [isReady, setReady] = useState(false);
const wavesurfer = useWavesurfer(containerReference, properties.url);

const { t } = useTranslation();

const togglePlay = useCallback(() => {
wavesurfer?.isPlaying() === true ? wavesurfer.pause() : wavesurfer?.play();
}, [wavesurfer]);

useEffect(() => {
if (wavesurfer === null) return;

setCurrentTime(0);

setIsPlaying(false);
const subscriptions = [
wavesurfer.on("play", () => {
setIsPlaying(true);
}),
wavesurfer.on("pause", () => {
setIsPlaying(false);
}),
wavesurfer.on("timeupdate", (currentTime) => {
setCurrentTime(currentTime);
}),

wavesurfer.on("ready", () => {
setReady(true);
setDuration(wavesurfer.getDuration());
}),
];

return () => {
for (const unsub of subscriptions) unsub();
};
}, [wavesurfer]);

return (
<div className="overflow-hidden rounded-lg bg-theme-secondary-100">
<div className="rounded-t-lg bg-theme-secondary-200 pb-1.5 pl-4 pt-1">
<div className="text-xs font-medium leading-4.5 text-theme-secondary-700">
{t("pages.articles.audio_version")}
</div>
</div>
<div className="px-4 py-3">
<div className="flex flex-col sm:flex-row sm:items-center">
<div className="mb-3 flex items-end justify-between sm:mb-0">
<div className="mr-4">
<Button
processing={!isReady}
variant="icon"
icon={isPlaying ? "AudioPause" : "AudioPlay"}
iconClass="h-5 w-5 text-theme-primary-600 group-hover:text-white transition-all"
className="h-8 w-8 bg-theme-primary-200 transition-colors hover:border-theme-primary-700 hover:bg-theme-primary-700"
onClick={togglePlay}
/>
</div>

<div className="text-xs font-medium leading-4.5 text-theme-secondary-700 sm:hidden">
{formatDuration(currentTime)} / {formatDuration(duration)}
</div>
</div>

<div className="mr-2 hidden text-xs font-medium leading-4.5 text-theme-secondary-700 sm:block">
{formatDuration(currentTime)}
</div>

<div
ref={containerReference}
className="h-6 w-full transition-all"
>
{!isReady && (
<div className="flex h-6 items-center justify-center transition-all">
<Icon
name="Spinner"
className="animate-spin text-theme-primary-600"
size="lg"
/>
</div>
)}
</div>

<div className="ml-2 hidden text-xs font-medium leading-4.5 text-theme-secondary-700 sm:block">
{formatDuration(duration)}
</div>
</div>
</div>
</div>
);
};
11 changes: 10 additions & 1 deletion resources/js/Pages/Articles/Show.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { FeaturedCollectionsBanner } from "@/Components/FeaturedCollectionsBanne
import { Heading } from "@/Components/Heading";
import { Img } from "@/Components/Image";
import { DefaultLayout } from "@/Layouts/DefaultLayout";
import { WaveSurferPlayer } from "@/Pages/Articles/Components/WaveSurferPlayer";
import { ArticlesScroll } from "@/Pages/Collections/Components/Articles/ArticlesScroll";
import { isTruthy } from "@/Utils/is-truthy";
import { tp } from "@/Utils/TranslatePlural";

interface Properties {
Expand Down Expand Up @@ -42,8 +44,15 @@ const ArticlesShow = ({ article, popularArticles }: Properties): JSX.Element =>
className="absolute -ml-[68px] flex flex-col space-y-2"
/>
</div>
<div>
{isTruthy(article.audioSrc) && (
<div className="mb-4 border-theme-secondary-300 sm:border-b sm:pb-4">
<WaveSurferPlayer url={article.audioSrc} />
</div>
)}

<ArticleContent article={article} />
<ArticleContent article={article} />
</div>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default class ArticleDataFactory extends ModelFactory<App.Data.Articles.A
large: faker.image.imageUrl(),
large2x: faker.image.imageUrl(),
},
audioSrc: null,
userId: faker.datatype.number({ min: 1, max: 100000 }),
content: faker.lorem.paragraph(),
category: "news",
Expand Down
4 changes: 4 additions & 0 deletions resources/js/icons.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ReactComponent as ArrowDownWithLine } from "@icons/arrow-down-with-line.svg";
import { ReactComponent as ArrowRight } from "@icons/arrow-right.svg";
import { ReactComponent as ArrowUpArrowDown } from "@icons/arrow-up-arrow-down.svg";
import { ReactComponent as AudioPause } from "@icons/audio-pause.svg";
import { ReactComponent as AudioPlay } from "@icons/audio-play.svg";
import { ReactComponent as Bars } from "@icons/bars.svg";
import { ReactComponent as Bell } from "@icons/bell.svg";
import { ReactComponent as BookmarkOutline } from "@icons/bookmark-outline.svg";
Expand Down Expand Up @@ -214,4 +216,6 @@ export const SvgCollection = {
Moralis,
Mnemonic,
Menu,
AudioPause,
AudioPlay,
};
1 change: 1 addition & 0 deletions resources/types/generated.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ declare namespace App.Data.Articles {
title: string;
slug: string;
category: App.Enums.ArticleCategoryEnum;
audioSrc: string | null;
content: string;
image: { small: string; small2x: string; medium: string; medium2x: string; large: string; large2x: string };
publishedAt: number;
Expand Down

0 comments on commit 75fa393

Please sign in to comment.