Removed static site building

main
Guus van Meerveld 3 years ago
parent 2b1c8582cf
commit fcf9237da0
Signed by: Guusvanmeerveld
GPG Key ID: 2BA7D7912771966E

@ -35,43 +35,8 @@ jobs:
- name: Lint - name: Lint
run: yarn run lint run: yarn run lint
- name: Build website - name: Build
run: yarn run export run: yarn run build
env:
NODE_ENV: production
- uses: actions/upload-artifact@v3
with:
name: output
path: out
pages:
runs-on: ubuntu-latest
needs: test
steps:
- name: Setup
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: "16.x"
cache: "yarn"
- name: Install npm dependencies
run: yarn install
- name: Build website
run: yarn run export
env:
NODE_ENV: production
- name: Deploy to pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./out
cname: materialtube.guusvanmeerveld.dev
docker: docker:
runs-on: ubuntu-latest runs-on: ubuntu-latest

@ -1,9 +1,7 @@
FROM node:16-alpine AS deps FROM node:16-alpine AS deps
WORKDIR /app WORKDIR /app
COPY package.json yarn.lock ./ COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile RUN yarn install --frozen-lockfile
@ -12,15 +10,32 @@ FROM node:16-alpine AS builder
WORKDIR /app WORKDIR /app
COPY . . COPY . .
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
ENV NEXT_TELEMETRY_DISABLED 1; ENV NEXT_TELEMETRY_DISABLED 1;
ENV NODE_ENV "production"
RUN yarn export RUN yarn build && yarn install --production --ignore-scripts --prefer-offline
FROM node:16-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/next-i18next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
FROM nginx:alpine AS runner USER nextjs
COPY --from=builder /app/out /usr/share/nginx/html EXPOSE 3000
EXPOSE 80 CMD ["yarn", "start"]

@ -5,4 +5,4 @@ services:
build: . build: .
container_name: material-tube container_name: material-tube
ports: ports:
- 3000:80 - 3000:3000

@ -6,14 +6,11 @@
"repository": { "repository": {
"url": "https://github.com/Guusvanmeerveld/MaterialTube" "url": "https://github.com/Guusvanmeerveld/MaterialTube"
}, },
"cacheDirectories": [ "cacheDirectories": [".next/cache"],
".next/cache"
],
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"export": "next build && next export",
"lint": "next lint", "lint": "next lint",
"prettify": "prettier src --write" "prettify": "prettier src --write"
}, },

@ -16,7 +16,10 @@ const Time: FC<{
const progress = useVideoState((state) => state.progress); const progress = useVideoState((state) => state.progress);
return ( return (
<Typography variant="subtitle1" color={theme.palette.text.secondary}> <Typography
variant="subtitle1"
color={theme.palette.text.secondary}
>
{formatTime(Math.round(progress))} {formatTime(Math.round(progress))}
<> / </> <> / </>
{formatTime(duration)} {formatTime(duration)}

@ -18,7 +18,7 @@ const Player: FC<{
captions: Caption[]; captions: Caption[];
length: number; length: number;
videoId: string; videoId: string;
sx: SxProps; sx?: SxProps;
}> = ({ formats, length: duration, sx }) => { }> = ({ formats, length: duration, sx }) => {
const [settings] = useSettings(); const [settings] = useSettings();
@ -40,7 +40,9 @@ const Player: FC<{
const pausedBy = useVideoState((state) => state.pausedBy); const pausedBy = useVideoState((state) => state.pausedBy);
const videoStream = formats.find( const videoStream = formats.find(
(format) => format.qualityLabel == "2160p" || format.qualityLabel == "1080p" (format) =>
format.qualityLabel == "2160p" ||
format.qualityLabel == "1080p"
)?.url; )?.url;
const audioStream = formats.find((format) => const audioStream = formats.find((format) =>
@ -78,7 +80,8 @@ const Player: FC<{
const handleFinishedWaiting = (e: Event) => { const handleFinishedWaiting = (e: Event) => {
setWaiting(false); setWaiting(false);
if (pausedBy == PausedBy.Player) setPlaying(VideoStatus.Playing); if (pausedBy == PausedBy.Player)
setPlaying(VideoStatus.Playing);
}; };
const onTimeUpdate = () => { const onTimeUpdate = () => {
@ -105,13 +108,19 @@ const Player: FC<{
audio.srcObject = null; audio.srcObject = null;
video.removeEventListener("waiting", handleWaiting); video.removeEventListener("waiting", handleWaiting);
video.removeEventListener("canplaythrough", handleFinishedWaiting); video.removeEventListener(
"canplaythrough",
handleFinishedWaiting
);
video.removeEventListener("error", handleError); video.removeEventListener("error", handleError);
video.removeEventListener("pause", handlePause); video.removeEventListener("pause", handlePause);
video.removeEventListener("timeupdate", onTimeUpdate); video.removeEventListener("timeupdate", onTimeUpdate);
audio.removeEventListener("waiting", handleWaiting); audio.removeEventListener("waiting", handleWaiting);
audio.removeEventListener("canplaythrough", handleFinishedWaiting); audio.removeEventListener(
"canplaythrough",
handleFinishedWaiting
);
audio.removeEventListener("pause", handlePause); audio.removeEventListener("pause", handlePause);
}; };
@ -119,18 +128,22 @@ const Player: FC<{
}, []); }, []);
useEffect(() => { useEffect(() => {
setPlaying(settings.autoPlay ? VideoStatus.Playing : VideoStatus.Paused); setPlaying(
settings.autoPlay
? VideoStatus.Playing
: VideoStatus.Paused
);
}, [setPlaying, settings.autoPlay]); }, [setPlaying, settings.autoPlay]);
useEffect(() => { useEffect(() => {
if (!videoRef.current || !audioRef.current) return; if (!videoRef.current || !audioRef.current) return;
if (playing == VideoStatus.Paused) { if (playing == VideoStatus.Playing && !error && !waiting) {
videoRef.current.pause();
audioRef.current.pause();
} else {
videoRef.current.play(); videoRef.current.play();
audioRef.current.play(); audioRef.current.play();
} else {
videoRef.current.pause();
audioRef.current.pause();
} }
}, [error, playing, waiting]); }, [error, playing, waiting]);
@ -172,7 +185,8 @@ const Player: FC<{
src={videoStream} src={videoStream}
ref={videoRef} ref={videoRef}
style={{ style={{
height: "100%" height: "100%",
width: "100%"
}} }}
autoPlay={playing == VideoStatus.Playing} autoPlay={playing == VideoStatus.Playing}
> >

@ -60,7 +60,12 @@ const Video: FC<{ video: VideoModel }> = ({ video }) => {
</Grid> </Grid>
<Grid item md={8} sx={{ padding: 3, width: "100%" }}> <Grid item md={8} sx={{ padding: 3, width: "100%" }}>
<Link href={{ pathname: "/watch", query: { v: video.id } }}> <Link
href={{
pathname: "/watch",
query: { v: video.id }
}}
>
<a> <a>
<Tooltip title={video.title}> <Tooltip title={video.title}>
<Typography gutterBottom noWrap variant="h5"> <Typography gutterBottom noWrap variant="h5">
@ -99,16 +104,29 @@ const Video: FC<{ video: VideoModel }> = ({ video }) => {
</Typography> </Typography>
<Link <Link
passHref passHref
href={{ pathname: "/channel", query: { c: video.author.id } }} href={{
pathname: "/channel",
query: {
c: video.author.id
}
}}
> >
<a> <a>
<Box ref={ref} sx={{ display: "flex", alignItems: "center" }}> <Box
ref={ref}
sx={{
display: "flex",
alignItems: "center"
}}
>
{isLoading && <CircularProgress />} {isLoading && <CircularProgress />}
{!isLoading && ( {!isLoading && (
<Avatar src={authorThumbnail} alt={video.author.name} /> <Avatar src={authorThumbnail} alt={video.author.name} />
)} )}
<Typography <Typography
sx={{ ml: 2 }} sx={{
ml: 2
}}
variant="subtitle1" variant="subtitle1"
color={theme.palette.text.secondary} color={theme.palette.text.secondary}
> >

@ -15,9 +15,12 @@ const NotFound: NextPage = () => {
/> />
<Layout> <Layout>
<Box sx={{ mt: 5, textAlign: "center" }}> <Box sx={{ mt: 5, textAlign: "center" }}>
<Typography variant="h3">Page not found</Typography> <Typography variant="h3">
Page not found
</Typography>
<Typography variant="h4"> <Typography variant="h4">
The page may have been moved or deleted. The page may have been moved or
deleted.
</Typography> </Typography>
</Box> </Box>
</Layout> </Layout>

@ -61,18 +61,35 @@ const InfoModal: FC<{
<Typography id="modal-modal-title" variant="h4"> <Typography id="modal-modal-title" variant="h4">
Stats for {settings.invidiousServer} Stats for {settings.invidiousServer}
</Typography> </Typography>
<Typography id="modal-modal-description" sx={{ mt: 2 }}> <Typography
id="modal-modal-description"
sx={{ mt: 2 }}
>
Version: {data.version} <br /> <br /> Version: {data.version} <br /> <br />
Software name: {data.software.name} <br /> Software name: {data.software.name}{" "}
Software version: {data.software.version} <br /> <br />
Software branch: {data.software.branch} <br /> <br /> Software version:{" "}
Is accepting registrations: {data.openRegistrations ? "Yes" : "No"} {data.software.version} <br />
Software branch: {
data.software.branch
}{" "}
<br /> <br /> <br /> <br />
Total users: {data.usage.users.total} <br /> Is accepting registrations:{" "}
Active in the past half year: {data.usage.users.activeHalfyear} {data.openRegistrations ? "Yes" : "No"}
<br /> <br />
Total users: {
data.usage.users.total
}{" "}
<br />
Active in the past half year:{" "}
{data.usage.users.activeHalfyear}
<br /> <br />
Active in the past month: {data.usage.users.activeMonth} <br /> <br /> Active in the past month:{" "}
Stats updated at: {lastUpdated.toLocaleDateString()} -{" "} {
data.usage.users.activeMonth
} <br /> <br />
Stats updated at:{" "}
{lastUpdated.toLocaleDateString()} -{" "}
{lastUpdated.toLocaleTimeString()} {lastUpdated.toLocaleTimeString()}
</Typography> </Typography>
</ModalBox> </ModalBox>
@ -92,7 +109,13 @@ const Setting: FC<{ title: string; description?: string }> = ({
<Box sx={{ flexGrow: 1 }}> <Box sx={{ flexGrow: 1 }}>
<Typography variant="h5">{title}</Typography> <Typography variant="h5">{title}</Typography>
{description && ( {description && (
<Typography variant="subtitle1" color={theme.palette.text.secondary}> <Typography
variant="subtitle1"
color={
theme.palette.text
.secondary
}
>
{description} {description}
</Typography> </Typography>
)} )}
@ -127,10 +150,13 @@ const Settings: NextPage = () => {
const invidiousServerResponse = useMutation<Stats, unknown, string>( const invidiousServerResponse = useMutation<Stats, unknown, string>(
"invidiousInstance", "invidiousInstance",
(server) => (server) =>
axios.get(`https://${server}/api/v1/stats`).then((res) => res.data) axios
.get(`https://${server}/api/v1/stats`)
.then((res) => res.data)
); );
const allowsRegistrations = invidiousServerResponse.data?.openRegistrations; const allowsRegistrations =
invidiousServerResponse.data?.openRegistrations;
return ( return (
<> <>
@ -146,7 +172,9 @@ const Settings: NextPage = () => {
<Divider sx={{ mb: 4 }} /> <Divider sx={{ mb: 4 }} />
<Typography variant="h4">Theme</Typography> <Typography variant="h4">
Theme
</Typography>
<Setting <Setting
title="General Theme" title="General Theme"
@ -157,58 +185,129 @@ const Settings: NextPage = () => {
color="primary" color="primary"
aria-label="outlined default button group" aria-label="outlined default button group"
> >
<Button onClick={() => setSetting("theme", "light")}> <Button
onClick={() =>
setSetting(
"theme",
"light"
)
}
>
Light Light
</Button> </Button>
<Button onClick={() => setSetting("theme")}>System</Button> <Button
<Button onClick={() => setSetting("theme", "dark")}>Dark</Button> onClick={() =>
setSetting(
"theme"
)
}
>
System
</Button>
<Button
onClick={() =>
setSetting(
"theme",
"dark"
)
}
>
Dark
</Button>
</ButtonGroup> </ButtonGroup>
</Setting> </Setting>
<Setting title="Primary Color"> <Setting title="Primary Color">
<ColorBox marginRight={1} color={settings.primaryColor} /> <ColorBox
marginRight={1}
color={
settings.primaryColor
}
/>
<Button <Button
onClick={() => setPrimaryColorModal(true)} onClick={() =>
setPrimaryColorModal(
true
)
}
variant="contained" variant="contained"
> >
Pick Color Pick Color
</Button> </Button>
<MaterialColorPicker <MaterialColorPicker
setState={setPrimaryColorModal} setState={
isOpen={primaryColorModalIsOpen} setPrimaryColorModal
setColor={(color) => setSetting("primaryColor", color)} }
selectedColor={settings.primaryColor} isOpen={
primaryColorModalIsOpen
}
setColor={(color) =>
setSetting(
"primaryColor",
color
)
}
selectedColor={
settings.primaryColor
}
/> />
</Setting> </Setting>
<Setting title="Accent Color"> <Setting title="Accent Color">
<ColorBox marginRight={1} color={settings.accentColor} /> <ColorBox
marginRight={1}
color={
settings.accentColor
}
/>
<Button <Button
onClick={() => setAccentColorModal(true)} onClick={() =>
setAccentColorModal(
true
)
}
variant="contained" variant="contained"
> >
Pick Color Pick Color
</Button> </Button>
<MaterialColorPicker <MaterialColorPicker
setState={setAccentColorModal} setState={
isOpen={accentColorModalIsOpen} setAccentColorModal
setColor={(color) => setSetting("accentColor", color)} }
selectedColor={settings.accentColor} isOpen={
accentColorModalIsOpen
}
setColor={(color) =>
setSetting(
"accentColor",
color
)
}
selectedColor={
settings.accentColor
}
/> />
</Setting> </Setting>
<Divider sx={{ my: 4 }} /> <Divider sx={{ my: 4 }} />
<Typography variant="h4">Player</Typography> <Typography variant="h4">
Player
</Typography>
<Divider sx={{ my: 4 }} /> <Divider sx={{ my: 4 }} />
<Typography variant="h4">Data</Typography> <Typography variant="h4">
Data
</Typography>
<Setting <Setting
title="Invidious Server" title="Invidious Server"
description={`Where to fetch data from ${ description={`Where to fetch data from ${
settings.storageType == "invidious" ? "and login into" : "" settings.storageType ==
"invidious"
? "and login into"
: ""
}`} }`}
> >
<Box sx={{ mr: 2 }}> <Box sx={{ mr: 2 }}>
@ -218,10 +317,15 @@ const Settings: NextPage = () => {
<Refresh <Refresh
sx={{ sx={{
cursor: "pointer", cursor: "pointer",
color: theme.palette.text.secondary color: theme
.palette
.text
.secondary
}} }}
onClick={() => onClick={() =>
invidiousServerResponse.mutate(settings.invidiousServer) invidiousServerResponse.mutate(
settings.invidiousServer
)
} }
/> />
)} )}
@ -229,50 +333,111 @@ const Settings: NextPage = () => {
!invidiousServerResponse.isLoading && ( !invidiousServerResponse.isLoading && (
<> <>
<Done <Done
onClick={() => setModalState(true)} onClick={() =>
sx={{ color: green[800], cursor: "pointer" }} setModalState(
true
)
}
sx={{
color: green[800],
cursor: "pointer"
}}
/> />
<InfoModal <InfoModal
modalIsOpen={modalIsOpen} modalIsOpen={
setModalState={setModalState} modalIsOpen
data={invidiousServerResponse.data!} }
setModalState={
setModalState
}
data={
invidiousServerResponse.data!
}
/> />
</> </>
)} )}
{invidiousServerResponse.error && ( {invidiousServerResponse.error && (
<Error sx={{ color: red[800] }} /> <Error
sx={{
color: red[800]
}}
/>
)}
{invidiousServerResponse.isLoading && (
<CircularProgress />
)} )}
{invidiousServerResponse.isLoading && <CircularProgress />}
</Box> </Box>
<FormControl sx={{ minWidth: 200 }}> <FormControl
<InputLabel id="server-select-label">Server</InputLabel> sx={{ minWidth: 200 }}
>
<InputLabel id="server-select-label">
Server
</InputLabel>
<Select <Select
labelId="server-select-label" labelId="server-select-label"
id="server-select" id="server-select"
value={settings.invidiousServer} value={
settings.invidiousServer
}
label="Server" label="Server"
onChange={(e) => { onChange={(
const server = e.target.value; e
) => {
invidiousServerResponse.mutate(server); const server =
e
.target
.value;
invidiousServerResponse.mutate(
server
);
setSetting("invidiousServer", server); setSetting(
"invidiousServer",
server
);
}}
MenuProps={{
sx: {
maxHeight: 300
}
}} }}
MenuProps={{ sx: { maxHeight: 300 } }}
> >
{instances.data && {instances.data &&
instances.data instances.data
.filter( .filter(
([, server]) => ([
server.type != ServerInstanceType.Onion && ,
server.api == true server
]) =>
server.type !=
ServerInstanceType.Onion &&
server.api ==
true
) )
.map(([uri, server]) => ( .map(
<MenuItem key={uri} value={uri}> ([
{server.flag} {uri} uri,
server
]) => (
<MenuItem
key={
uri
}
value={
uri
}
>
{
server.flag
}{" "}
{
uri
}
</MenuItem> </MenuItem>
))} )
)}
</Select> </Select>
</FormControl> </FormControl>
</Setting> </Setting>
@ -281,22 +446,43 @@ const Settings: NextPage = () => {
title="Data Storage Location" title="Data Storage Location"
description="Where your personal data will be stored" description="Where your personal data will be stored"
> >
<FormControl sx={{ minWidth: 200 }}> <FormControl
<InputLabel id="location-select-label">Location</InputLabel> sx={{ minWidth: 200 }}
>
<InputLabel id="location-select-label">
Location
</InputLabel>
<Select <Select
labelId="location-select-label" labelId="location-select-label"
id="location-select" id="location-select"
value={settings.storageType} value={
settings.storageType
}
label="Location" label="Location"
onChange={(e) => { onChange={(
const location = e.target.value; e
) => {
setSetting("storageType", location); const location =
e
.target
.value;
setSetting(
"storageType",
location
);
}}
MenuProps={{
sx: {
maxHeight: 300
}
}} }}
MenuProps={{ sx: { maxHeight: 300 } }}
> >
{[ {[
{ name: "Locally", value: StorageType.Local }, {
name: "Locally",
value: StorageType.Local
},
{ {
name: "Invidious server using auth", name: "Invidious server using auth",
value: StorageType.Invidious value: StorageType.Invidious
@ -305,43 +491,69 @@ const Settings: NextPage = () => {
name: `A custom ${process.env.NEXT_PUBLIC_APP_NAME} auth server`, name: `A custom ${process.env.NEXT_PUBLIC_APP_NAME} auth server`,
value: StorageType.RemoteServer value: StorageType.RemoteServer
} }
].map((location, i) => ( ].map(
<MenuItem key={i} value={location.value}> (
{location.name} location,
i
) => (
<MenuItem
key={
i
}
value={
location.value
}
>
{
location.name
}
</MenuItem> </MenuItem>
))} )
)}
</Select> </Select>
</FormControl> </FormControl>
</Setting> </Setting>
{settings.storageType != StorageType.Local && ( {settings.storageType !=
StorageType.Local && (
<> <>
{settings.storageType == "invidious" && allowsRegistrations && ( {settings.storageType ==
"invidious" &&
allowsRegistrations && (
<Setting <Setting
title="Username" title="Username"
description="The username for your Invidious account" description="The username for your Invidious account"
> >
<TextField label="Username" color="primary" /> <TextField
label="Username"
color="primary"
/>
</Setting> </Setting>
)} )}
{settings.storageType == StorageType.RemoteServer && ( {settings.storageType ==
StorageType.RemoteServer && (
<Setting <Setting
title="Server address" title="Server address"
description={`The address for your ${process.env.NEXT_PUBLIC_APP_NAME} auth server`} description={`The address for your ${process.env.NEXT_PUBLIC_APP_NAME} auth server`}
> >
<TextField label="Server adress" color="primary" /> <TextField
label="Server adress"
color="primary"
/>
</Setting> </Setting>
)} )}
<Setting <Setting
title={ title={
settings.storageType == StorageType.Invidious settings.storageType ==
StorageType.Invidious
? "Password" ? "Password"
: "Passphrase" : "Passphrase"
} }
description={ description={
settings.storageType == StorageType.Invidious settings.storageType ==
StorageType.Invidious
? "The password for your invidious account" ? "The password for your invidious account"
: "The passphrase for your account" : "The passphrase for your account"
} }
@ -349,7 +561,8 @@ const Settings: NextPage = () => {
<TextField <TextField
type="password" type="password"
label={ label={
settings.storageType == StorageType.Invidious settings.storageType ==
StorageType.Invidious
? "Password" ? "Password"
: "Passphrase" : "Passphrase"
} }

@ -0,0 +1,3 @@
.description a {
color: #566fff;
}

@ -1,29 +1,41 @@
import NotFound from "./404"; import NotFound from "./404";
import { NextPage } from "next"; import { GetStaticProps, NextPage } from "next";
import { NextSeo } from "next-seo"; import { NextSeo } from "next-seo";
import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useQuery } from "react-query"; import { useQuery } from "react-query";
import axios, { AxiosError } from "axios"; import axios, { AxiosError } from "axios";
import Avatar from "@mui/material/Avatar";
import Box from "@mui/material/Box";
import Container from "@mui/material/Container"; import Container from "@mui/material/Container";
import Divider from "@mui/material/Divider";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import Share from "@mui/icons-material/Share";
import { abbreviateNumber } from "@src/utils";
import { Error } from "@interfaces/api"; import { Error } from "@interfaces/api";
import VideoAPI from "@interfaces/api/video"; import VideoAPI from "@interfaces/api/video";
import { videoToVideo } from "@utils/conversions";
import useSettings from "@utils/hooks/useSettings"; import useSettings from "@utils/hooks/useSettings";
import Layout from "@components/Layout"; import Layout from "@components/Layout";
import Loading from "@components/Loading"; import Loading from "@components/Loading";
import Player from "@components/Player"; import Player from "@components/Player";
import styles from "./watch.module.css";
const Watch: NextPage = () => { const Watch: NextPage = () => {
const { query, isReady } = useRouter(); const { query, isReady } = useRouter();
const theme = useTheme();
const videoId = query["v"]; const videoId = query["v"];
const [settings] = useSettings(); const [settings] = useSettings();
@ -59,7 +71,6 @@ const Watch: NextPage = () => {
return ( return (
<> <>
<NextSeo title={data ? data.title : "Not Found"} /> <NextSeo title={data ? data.title : "Not Found"} />
<Layout> <Layout>
{data && ( {data && (
<> <>
@ -69,10 +80,79 @@ const Watch: NextPage = () => {
captions={data.captions} captions={data.captions}
length={data.lengthSeconds} length={data.lengthSeconds}
videoId={data.videoId} videoId={data.videoId}
sx={{ height: "75vh", margin: "auto", mt: 2 }} sx={{
height: "75vh",
margin: "auto",
mt: 2
}}
/> />
<Container sx={{ pt: 2 }}> <Container sx={{ mt: 2 }}>
<Box
sx={{
display: "flex",
alignItems: "center"
}}
>
<Box
sx={{
flex: 1
}}
>
<Typography variant="h4">{data.title}</Typography> <Typography variant="h4">{data.title}</Typography>
<Typography
variant="subtitle1"
color={theme.palette.text.secondary}
sx={{
mt: 1
}}
>
{abbreviateNumber(data.viewCount)} Views {" "}
{new Date(data.published * 1000).toLocaleDateString()}
</Typography>
</Box>
<Share />
</Box>
<Divider sx={{ my: 2 }} />
<Link
href={{
pathname: `/channel`,
query: {
c: data.authorId
}
}}
>
<a>
<Box
sx={{
display: "flex",
alignItems: "center",
mb: 2
}}
>
<Avatar
src={
data?.authorThumbnails.find(
(thumbnail) => thumbnail.width == 100
)?.url
}
alt={data.author}
sx={{
mr: 2
}}
/>
<Typography variant="h6">{data.author}</Typography>
</Box>
</a>
</Link>
<Typography
className={styles.description}
dangerouslySetInnerHTML={{
__html: data.descriptionHtml.replaceAll("\n", "<br>")
}}
></Typography>
</Container> </Container>
</> </>
)} )}

Loading…
Cancel
Save