diff --git a/package.json b/package.json index 413b3dc..8504c23 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "react-dom": "^18", "react-icons": "^5.0.1", "reactjs-visibility": "^0.1.4", + "sanitize-html": "^2.13.0", "use-debounce": "^10.0.0", "zod": "^3.22.4", "zustand": "^4.5.2" @@ -36,6 +37,7 @@ "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "@types/sanitize-html": "^2.11.0", "@typescript-eslint/eslint-plugin": "^7.3.1", "@typescript-eslint/parser": "^7.3.1", "autoprefixer": "^10.0.1", diff --git a/src/app/watch/Channel.tsx b/src/app/watch/Channel.tsx new file mode 100644 index 0000000..7b278fd --- /dev/null +++ b/src/app/watch/Channel.tsx @@ -0,0 +1,40 @@ +"use client"; + +import NextLink from "next/link"; + +import { Avatar } from "@nextui-org/avatar"; +import { Link } from "@nextui-org/link"; + +import { Author } from "@/client/typings/author"; +import formatBigNumber from "@/utils/formatBigNumber"; +import { channelUrl } from "@/utils/urls"; + +import { Component } from "@/typings/component"; + +export const Channel: Component<{ + data: Author; +}> = ({ data }) => { + const url = data?.id ? channelUrl(data.id) : undefined; + + return ( + +
+ +
+

{data.name}

+ {data.subscribers && ( +

+ {formatBigNumber(data.subscribers)} subscribers +

+ )} +
+
+ + ); +}; diff --git a/src/app/watch/Description.tsx b/src/app/watch/Description.tsx new file mode 100644 index 0000000..1d7192a --- /dev/null +++ b/src/app/watch/Description.tsx @@ -0,0 +1,80 @@ +import sanitizeHtml from "sanitize-html"; + +import { Fragment, useMemo, useState } from "react"; +import { + FiChevronUp as CollapseIcon, + FiChevronDown as ExpandIcon +} from "react-icons/fi"; + +import { Button } from "@nextui-org/button"; +import { Link } from "@nextui-org/link"; + +import formatDuration from "@/utils/formatDuration"; +import { highlight, ItemType } from "@/utils/highlight"; + +import { Component } from "@/typings/component"; + +export const Description: Component<{ data: string }> = ({ data }) => { + const [expandedDescription, setExpandedDescription] = useState(false); + + const sanitizedDescription = useMemo( + () => + sanitizeHtml(data, { + allowedTags: ["a", "br"], + allowedAttributes: { + a: ["href"] + } + }), + [data] + ); + + const descriptionCut = useMemo( + () => + expandedDescription + ? sanitizedDescription + : sanitizedDescription?.substring(0, 200) + "...", + [sanitizedDescription, expandedDescription] + ); + + const description = useMemo( + () => highlight(descriptionCut), + [descriptionCut] + ); + + return ( +
+

+ {description.map((item) => { + switch (item.type) { + case ItemType.Tokens: + return {item.content}; + + case ItemType.Link: + return ( + + {item.text ?? item.href} + + ); + + case ItemType.Timestamp: + return ( + + {formatDuration(item.duration * 1000)} + + ); + + case ItemType.Linebreak: + return
; + } + })} +

+ +
+ ); +}; diff --git a/src/app/watch/Player.tsx b/src/app/watch/Player.tsx new file mode 100644 index 0000000..392967f --- /dev/null +++ b/src/app/watch/Player.tsx @@ -0,0 +1,11 @@ +"use client"; + +import { Component } from "@/typings/component"; + +export const Player: Component = () => { + return ( + <> +