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.title}
-{data.author.name}
-- {formatUploadedTime(data.uploaded)} -
-+ {formatUploadedTime(data.uploaded)} +
+
Views: {formatBigNumber(data.views)}
diff --git a/src/utils/highlight.ts b/src/utils/highlight.ts
new file mode 100644
index 0000000..3d091a3
--- /dev/null
+++ b/src/utils/highlight.ts
@@ -0,0 +1,126 @@
+const itemPatterns: ItemPattern[] = [
+ {
+ regex: /(?:([0-9]{1,2}):)?([0-9]{1,2}):([0-9]{2})/g,
+ convert: (match): Timestamp => {
+ const hours = parseInt(match[1]) || 0;
+ const minutes = parseInt(match[2]) || 0;
+ const seconds = parseInt(match[3]) || 0;
+
+ const duration = hours * 3600 + minutes * 60 + seconds;
+
+ return {
+ type: ItemType.Timestamp,
+ duration,
+ id: Math.random().toString()
+ };
+ }
+ },
+ {
+ regex:
+ /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/g,
+ convert: (match): Link => {
+ console.log(match);
+
+ return {
+ type: ItemType.Link,
+ href: match[0],
+ id: Math.random().toString()
+ };
+ }
+ },
+ {
+ regex: /(\n)|(
)/g,
+ convert: () => ({ type: ItemType.Linebreak, id: Math.random().toString() })
+ }
+];
+
+interface BaseItem {
+ type: ItemType;
+ id: string;
+}
+
+interface Timestamp extends BaseItem {
+ type: ItemType.Timestamp;
+ duration: number;
+}
+
+interface Tokens extends BaseItem {
+ type: ItemType.Tokens;
+ content: string;
+}
+
+interface Link extends BaseItem {
+ type: ItemType.Link;
+ href: string;
+ text?: string;
+}
+
+interface Linebreak extends BaseItem {
+ type: ItemType.Linebreak;
+}
+
+export enum ItemType {
+ Linebreak,
+ Link,
+ Tokens,
+ Timestamp
+}
+
+type Item = Timestamp | Link | Tokens | Linebreak;
+
+export interface ItemPattern {
+ regex: RegExp;
+ convert: (match: RegExpMatchArray) => Item;
+}
+
+export const highlight = (
+ input: string,
+ patterns: ItemPattern[] = itemPatterns
+): Item[] => {
+ const items: Item[] = [];
+
+ const matches = patterns
+ .map((pattern) =>
+ Array.from(input.matchAll(pattern.regex)).map((match) => ({
+ match,
+ pattern
+ }))
+ )
+ .flat()
+ .map(({ match, pattern }) => ({
+ index: match.index ?? 0,
+ length: match[0].length,
+ item: pattern.convert(match)
+ }))
+ .sort((a, b) => a.index - b.index);
+
+ let lastIndex = 0;
+
+ for (const match of matches) {
+ if (match.index !== lastIndex) {
+ const content = input.substring(lastIndex, match.index);
+
+ items.push({
+ type: ItemType.Tokens,
+ content,
+ id: Math.random().toString()
+ });
+ }
+
+ items.push(match.item);
+
+ lastIndex = match.index + match.length;
+ }
+
+ if (lastIndex < input.length) {
+ const content = input.substring(lastIndex);
+
+ items.push({
+ type: ItemType.Tokens,
+ content,
+ id: Math.random().toString()
+ });
+ }
+
+ return items;
+};
diff --git a/yarn.lock b/yarn.lock
index 7c69ae9..7ec994b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3137,6 +3137,13 @@
dependencies:
"@types/node" "*"
+"@types/sanitize-html@^2.11.0":
+ version "2.11.0"
+ resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.11.0.tgz#582d8c72215c0228e3af2be136e40e0b531addf2"
+ integrity sha512-7oxPGNQHXLHE48r/r/qjn7q0hlrs3kL7oZnGj0Wf/h9tj/6ibFyRkNbsDxaBBZ4XUZ0Dx5LGCyDJ04ytSofacQ==
+ dependencies:
+ htmlparser2 "^8.0.0"
+
"@types/scheduler@*":
version "0.16.8"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff"
@@ -3968,6 +3975,36 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
+dom-serializer@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53"
+ integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==
+ dependencies:
+ domelementtype "^2.3.0"
+ domhandler "^5.0.2"
+ entities "^4.2.0"
+
+domelementtype@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
+ integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
+
+domhandler@^5.0.2, domhandler@^5.0.3:
+ version "5.0.3"
+ resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31"
+ integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==
+ dependencies:
+ domelementtype "^2.3.0"
+
+domutils@^3.0.1:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e"
+ integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==
+ dependencies:
+ dom-serializer "^2.0.0"
+ domelementtype "^2.3.0"
+ domhandler "^5.0.3"
+
eastasianwidth@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
@@ -4008,6 +4045,11 @@ enhanced-resolve@^5.12.0:
graceful-fs "^4.2.4"
tapable "^2.2.0"
+entities@^4.2.0, entities@^4.4.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
+ integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
+
es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.22.4:
version "1.22.5"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.5.tgz#1417df4e97cc55f09bf7e58d1e614bc61cb8df46"
@@ -4733,6 +4775,16 @@ hasown@^2.0.0, hasown@^2.0.1:
dependencies:
function-bind "^1.1.2"
+htmlparser2@^8.0.0:
+ version "8.0.2"
+ resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21"
+ integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==
+ dependencies:
+ domelementtype "^2.3.0"
+ domhandler "^5.0.3"
+ domutils "^3.0.1"
+ entities "^4.4.0"
+
idb@^7.0.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b"
@@ -4943,6 +4995,11 @@ is-path-inside@^3.0.3:
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
+is-plain-object@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
+ integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
+
is-regex@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
@@ -5623,6 +5680,11 @@ parent-module@^1.0.0:
dependencies:
callsites "^3.0.0"
+parse-srcset@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1"
+ integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==
+
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
@@ -5772,6 +5834,15 @@ postcss@^8, postcss@^8.4.23:
picocolors "^1.0.0"
source-map-js "^1.0.2"
+postcss@^8.3.11:
+ version "8.4.38"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
+ integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
+ dependencies:
+ nanoid "^3.3.7"
+ picocolors "^1.0.0"
+ source-map-js "^1.2.0"
+
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@@ -6067,6 +6138,18 @@ safe-regex-test@^1.0.3:
es-errors "^1.3.0"
is-regex "^1.1.4"
+sanitize-html@^2.13.0:
+ version "2.13.0"
+ resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.13.0.tgz#71aedcdb777897985a4ea1877bf4f895a1170dae"
+ integrity sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==
+ dependencies:
+ deepmerge "^4.2.2"
+ escape-string-regexp "^4.0.0"
+ htmlparser2 "^8.0.0"
+ is-plain-object "^5.0.0"
+ parse-srcset "^1.0.2"
+ postcss "^8.3.11"
+
scheduler@^0.23.0:
version "0.23.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
@@ -6196,6 +6279,11 @@ source-map-js@^1.0.2:
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
+source-map-js@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"
+ integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==
+
source-map-support@~0.5.20:
version "0.5.21"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"