Added basic video functionnality

This commit is contained in:
DrMint 2022-04-01 20:47:42 +02:00
parent 73ad43fe7d
commit e3c2e4da1d
11 changed files with 488 additions and 10 deletions

View File

@ -14,7 +14,7 @@ module.exports = {
defaultLocale: "en", defaultLocale: "en",
}, },
images: { images: {
domains: ["img.accords-library.com"], domains: ["img.accords-library.com", "watch.accords-library.com"],
}, },
serverRuntimeConfig: { serverRuntimeConfig: {
locales: locales, locales: locales,

View File

@ -3,7 +3,7 @@ import { AppStaticProps } from "queries/getAppStaticProps";
import { prettyLanguage } from "queries/helpers"; import { prettyLanguage } from "queries/helpers";
import Button from "./Button"; import Button from "./Button";
type HorizontalLineProps = { type Props = {
className?: string; className?: string;
locales: (string | undefined)[]; locales: (string | undefined)[];
languages: AppStaticProps["languages"]; languages: AppStaticProps["languages"];
@ -11,8 +11,8 @@ type HorizontalLineProps = {
href?: string; href?: string;
}; };
export default function HorizontalLine( export default function LanguageSwitcher(
props: HorizontalLineProps props: Props
): JSX.Element { ): JSX.Element {
const { locales, langui, href } = props; const { locales, langui, href } = props;
const router = useRouter(); const router = useRouter();

View File

@ -207,20 +207,14 @@ export default function MainPanel(props: MainPanelProps): JSX.Element {
onClick={() => appLayout.setMainPanelOpen(false)} onClick={() => appLayout.setMainPanelOpen(false)}
/> />
{/*
<NavOption <NavOption
url="/archives" url="/archives"
icon="inventory" icon="inventory"
title={langui.archives} title={langui.archives}
reduced={appLayout.mainPanelReduced && isDesktop} reduced={appLayout.mainPanelReduced && isDesktop}
onClick={() => appLayout.setMainPanelOpen(false)} onClick={() => appLayout.setMainPanelOpen(false)}
/> />
*/}
<NavOption <NavOption
url="/about-us" url="/about-us"
icon="info" icon="info"

View File

@ -0,0 +1,80 @@
import Chip from "components/Chip";
import { GetVideosPreviewQuery } from "graphql/generated";
import Link from "next/link";
import {
getVideoThumbnailURL,
prettyDate,
prettyDuration,
prettyShortenNumber,
} from "queries/helpers";
export type Props = {
video: Exclude<
Exclude<
GetVideosPreviewQuery["videos"],
null | undefined
>["data"][number]["attributes"],
null | undefined
>;
};
export default function PostPreview(props: Props): JSX.Element {
const { video } = props;
return (
<Link href={`/archives/videos/${video.uid}`} passHref>
<div className="drop-shadow-shade-xl cursor-pointer grid items-end hover:scale-[1.02] [--bg-opacity:0] hover:[--bg-opacity:0.5] [--play-opacity:0] hover:[--play-opacity:100] transition-transform">
<div className="relative">
<img
className="aspect-video rounded-t-lg"
src={getVideoThumbnailURL(video.uid)}
alt={video.title}
/>
<div className="absolute inset-0 text-light grid place-content-center drop-shadow-shade-lg bg-shade bg-opacity-[var(--bg-opacity)] transition-colors">
<span className="material-icons text-6xl opacity-[var(--play-opacity)] transition-opacity">
play_circle_outline
</span>
</div>
<div className="absolute right-2 bottom-2 text-light bg-black bg-opacity-60 px-2 rounded-full">
{prettyDuration(video.duration)}
</div>
</div>
<div className="linearbg-obi fine:drop-shadow-shade-lg rounded-b-md top-full transition-opacity z-20 grid p-4 gap-2">
<div className="flex flex-row flex-wrap gap-x-3 w-full">
<p className="mobile:text-xs text-sm">
<span className="material-icons !text-base translate-y-[.15em] mr-1">
event
</span>
{prettyDate(video.published_date)}
</p>
<p className="mobile:text-xs text-sm">
<span className="material-icons !text-base translate-y-[.15em] mr-1">
visibility
</span>
{prettyShortenNumber(video.views)}
</p>
{video.channel?.data?.attributes && (
<p className="mobile:text-xs text-sm">
<span className="material-icons !text-base translate-y-[.15em] mr-1">
person
</span>
{video.channel.data.attributes.title}
</p>
)}
</div>
<div>
<h1 className="text-xl">{video.title}</h1>
</div>
<div className="grid grid-flow-col gap-1 overflow-x-scroll webkit-scrollbar:w-0 [scrollbar-width:none] place-content-start">
{video.categories?.data.map((category) => (
<Chip key={category.id} className="text-sm">
{category.attributes?.short}
</Chip>
))}
</div>
</div>
</div>
</Link>
);
}

View File

@ -0,0 +1,55 @@
query getVideo($uid: String) {
videos(filters: { uid: { eq: $uid } }) {
data {
id
attributes {
uid
title
description
published_date {
year
month
day
}
channel {
data {
attributes {
uid
title
subscribers
}
}
}
categories {
data {
id
attributes {
short
}
}
}
views
likes
source
audio_languages {
data {
id
attributes {
code
}
}
}
sub_languages {
data {
id
attributes {
code
}
}
}
width
height
}
}
}
}

View File

@ -0,0 +1,32 @@
query getVideosPreview {
videos(pagination: { limit: -1 }) {
data {
id
attributes {
uid
title
views
duration
categories {
data {
id
attributes {
short
}
}
}
published_date {
...datePicker
}
channel {
data {
attributes {
uid
title
}
}
}
}
}
}
}

View File

@ -0,0 +1,10 @@
query getVideosSlugs {
videos(pagination: {limit:-1}) {
data {
id
attributes {
uid
}
}
}
}

View File

@ -1,4 +1,5 @@
import AppLayout from "components/AppLayout"; import AppLayout from "components/AppLayout";
import NavOption from "components/PanelComponents/NavOption";
import PanelHeader from "components/PanelComponents/PanelHeader"; import PanelHeader from "components/PanelComponents/PanelHeader";
import SubPanel from "components/Panels/SubPanel"; import SubPanel from "components/Panels/SubPanel";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
@ -15,6 +16,7 @@ export default function Archives(props: ArchivesProps): JSX.Element {
title={langui.archives} title={langui.archives}
description={langui.archives_description} description={langui.archives_description}
/> />
<NavOption title={"Videos"} url="/archives/videos/" border />
</SubPanel> </SubPanel>
); );
return ( return (

View File

@ -0,0 +1,193 @@
import AppLayout from "components/AppLayout";
import Button from "components/Button";
import HorizontalLine from "components/HorizontalLine";
import InsetBox from "components/InsetBox";
import NavOption from "components/PanelComponents/NavOption";
import ReturnButton, {
ReturnButtonType,
} from "components/PanelComponents/ReturnButton";
import ContentPanel, {
ContentPanelWidthSizes,
} from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel";
import { useAppLayout } from "contexts/AppLayoutContext";
import { GetVideoQuery } from "graphql/generated";
import { getReadySdk } from "graphql/sdk";
import { useMediaMobile } from "hooks/useMediaQuery";
import {
GetStaticPathsContext,
GetStaticPathsResult,
GetStaticPropsContext,
} from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { getVideoFile, prettyDate, prettyShortenNumber } from "queries/helpers";
interface Props extends AppStaticProps {
video: Exclude<
Exclude<
GetVideoQuery["videos"],
null | undefined
>["data"][number]["attributes"],
null | undefined
>;
}
export default function Video(props: Props): JSX.Element {
const { langui, video } = props;
const isMobile = useMediaMobile();
const appLayout = useAppLayout();
const subPanel = (
<SubPanel>
<ReturnButton
href="/archives/videos/"
title={"Videos"}
langui={langui}
displayOn={ReturnButtonType.desktop}
className="mb-10"
/>
<HorizontalLine />
<NavOption
title={langui.video}
url="#video"
border
onClick={() => appLayout.setSubPanelOpen(false)}
/>
<NavOption
title={"Channel"}
url="#channel"
border
onClick={() => appLayout.setSubPanelOpen(false)}
/>
<NavOption
title={"Description"}
url="#description"
border
onClick={() => appLayout.setSubPanelOpen(false)}
/>
</SubPanel>
);
const contentPanel = (
<ContentPanel width={ContentPanelWidthSizes.large}>
<ReturnButton
href="/library/"
title={langui.library}
langui={langui}
displayOn={ReturnButtonType.mobile}
className="mb-10"
/>
<div className="grid gap-12 place-items-center">
<div
id="video"
className="w-full rounded-xl shadow-shade shadow-lg overflow-hidden"
>
<video className="w-full" src={getVideoFile(video.uid)} controls></video>
<div className="p-6 mt-2">
<h1 className="text-2xl">{video.title}</h1>
<div className="flex flex-row flex-wrap gap-x-6 w-full">
<p>
<span className="material-icons !text-base translate-y-[.15em] mr-1">
event
</span>
{prettyDate(video.published_date)}
</p>
<p>
<span className="material-icons !text-base translate-y-[.15em] mr-1">
visibility
</span>
{isMobile
? prettyShortenNumber(video.views)
: video.views.toLocaleString()}
</p>
{video.channel?.data?.attributes && (
<p>
<span className="material-icons !text-base translate-y-[.15em] mr-1">
thumb_up
</span>
{isMobile
? prettyShortenNumber(video.likes)
: video.likes.toLocaleString()}
</p>
)}
<Button href="" className="!py-0 !px-3">{`View on ${video.source}`}</Button>
</div>
</div>
</div>
{video.channel?.data?.attributes && (
<InsetBox id="channel" className="grid place-items-center">
<div className="w-[clamp(0px,100%,42rem)] grid place-items-center gap-4 text-center">
<h2 className="text-2xl">{"Channel"}</h2>
<div>
<Button href="#">
<h3>{video.channel.data.attributes.title}</h3>
</Button>
<p>
{video.channel.data.attributes.subscribers.toLocaleString()}{" "}
subscribers
</p>
</div>
</div>
</InsetBox>
)}
<InsetBox id="description" className="grid place-items-center">
<div className="w-[clamp(0px,100%,42rem)] grid place-items-center gap-8">
<h2 className="text-2xl">{"Description"}</h2>
<p className="whitespace-pre-line">{video.description}</p>
</div>
</InsetBox>
</div>
</ContentPanel>
);
return (
<AppLayout
navTitle={langui.archives}
subPanel={subPanel}
contentPanel={contentPanel}
{...props}
/>
);
}
export async function getStaticProps(
context: GetStaticPropsContext
): Promise<{ notFound: boolean } | { props: Props }> {
const sdk = getReadySdk();
const videos = await sdk.getVideo({
uid: context.params?.uid ? context.params.uid.toString() : "",
});
if (!videos.videos?.data[0].attributes) return { notFound: true };
const props: Props = {
...(await getAppStaticProps(context)),
video: videos.videos.data[0].attributes,
};
return {
props: props,
};
}
export async function getStaticPaths(
context: GetStaticPathsContext
): Promise<GetStaticPathsResult> {
const sdk = getReadySdk();
const videos = await sdk.getVideo();
const paths: GetStaticPathsResult["paths"] = [];
if (videos.videos?.data)
videos.videos.data.map((video) => {
context.locales?.map((local) => {
if (video.attributes)
paths.push({ params: { uid: video.attributes.uid }, locale: local });
});
});
return {
paths,
fallback: "blocking",
};
}

View File

@ -0,0 +1,73 @@
import AppLayout from "components/AppLayout";
import HorizontalLine from "components/HorizontalLine";
import PanelHeader from "components/PanelComponents/PanelHeader";
import ReturnButton, {
ReturnButtonType,
} from "components/PanelComponents/ReturnButton";
import ContentPanel, {
ContentPanelWidthSizes,
} from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel";
import VideoPreview from "components/Videos/VideoPreview";
import { GetVideosPreviewQuery } from "graphql/generated";
import { getReadySdk } from "graphql/sdk";
import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
interface Props extends AppStaticProps {
videos: Exclude<GetVideosPreviewQuery["videos"], null | undefined>["data"];
}
export default function Videos(props: Props): JSX.Element {
const { langui, videos } = props;
const subPanel = (
<SubPanel>
<ReturnButton
href="/archives/"
title={"Archives"}
langui={langui}
displayOn={ReturnButtonType.desktop}
className="mb-10"
/>
<PanelHeader
icon="movie"
title="Videos"
description={langui.archives_description}
/>
</SubPanel>
);
const contentPanel = (
<ContentPanel width={ContentPanelWidthSizes.large}>
<div className="grid gap-8 items-end mobile:grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] pb-12 border-b-[3px] border-dotted last-of-type:border-0">
{videos.map((video) => (
<>{video.attributes && <VideoPreview video={video.attributes} />}</>
))}
</div>
</ContentPanel>
);
return (
<AppLayout
navTitle={langui.archives}
subPanel={subPanel}
contentPanel={contentPanel}
{...props}
/>
);
}
export async function getStaticProps(
context: GetStaticPropsContext
): Promise<{ notFound: boolean } | { props: Props }> {
const sdk = getReadySdk();
const videos = await sdk.getVideosPreview();
if (!videos.videos) return { notFound: true };
const props: Props = {
...(await getAppStaticProps(context)),
videos: videos.videos.data,
};
return {
props: props,
};
}

View File

@ -229,6 +229,37 @@ export function prettyItemSubType(
/* eslint-enable @typescript-eslint/no-explicit-any */ /* eslint-enable @typescript-eslint/no-explicit-any */
} }
export function prettyShortenNumber(number: number): string {
if (number > 1000000) {
return number.toLocaleString(undefined, {
maximumSignificantDigits: 3,
});
} else if (number > 1000) {
return (number / 1000).toLocaleString(undefined, {
maximumSignificantDigits: 2,
}) + "K";
}
return number.toLocaleString();
}
export function prettyDuration(seconds: number): string {
let hours = 0;
let minutes = 0;
while (seconds > 60) {
minutes += 1;
seconds -= 60;
}
while (minutes > 60) {
hours += 1;
minutes -= 60;
}
let result = "";
if (hours) result += hours.toString().padStart(2, "0") + ":";
result += minutes.toString().padStart(2, "0") + ":";
result += seconds.toString().padStart(2, "0");
return result;
}
export function prettyLanguage( export function prettyLanguage(
code: string, code: string,
languages: AppStaticProps["languages"] languages: AppStaticProps["languages"]
@ -417,3 +448,11 @@ export function getLocalesFromLanguages(
? languages.map((language) => language?.language?.data?.attributes?.code) ? languages.map((language) => language?.language?.data?.attributes?.code)
: []; : [];
} }
export function getVideoThumbnailURL(uid: string):string {
return `${process.env.NEXT_PUBLIC_URL_WATCH}/videos/${uid}.webp`;
}
export function getVideoFile(uid: string):string {
return `${process.env.NEXT_PUBLIC_URL_WATCH}/videos/${uid}.mp4`;
}