jbilcke-hf HF staff commited on
Commit
e4d3d8a
1 Parent(s): 0f35d4c

work in progress on the comment system

Browse files
src/app/interface/channel-card/index.tsx CHANGED
@@ -1,5 +1,4 @@
1
  import { useState } from "react"
2
- import dynamic from "next/dynamic"
3
 
4
  import { RiCheckboxCircleFill } from "react-icons/ri"
5
  import { IoAdd } from "react-icons/io5"
@@ -7,12 +6,8 @@ import { IoAdd } from "react-icons/io5"
7
  import { cn } from "@/lib/utils"
8
  import { ChannelInfo } from "@/types"
9
  import { isCertifiedUser } from "@/app/certification"
10
- import Link from "next/link"
11
 
12
- const DefaultAvatar = dynamic(() => import("../default-avatar"), {
13
- loading: () => null,
14
- })
15
-
16
  export function ChannelCard({
17
  channel,
18
  onClick,
 
1
  import { useState } from "react"
 
2
 
3
  import { RiCheckboxCircleFill } from "react-icons/ri"
4
  import { IoAdd } from "react-icons/io5"
 
6
  import { cn } from "@/lib/utils"
7
  import { ChannelInfo } from "@/types"
8
  import { isCertifiedUser } from "@/app/certification"
9
+ import { DefaultAvatar } from "../default-avatar"
10
 
 
 
 
 
11
  export function ChannelCard({
12
  channel,
13
  onClick,
src/app/interface/comment-card/index.tsx ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cn } from "@/lib/utils"
2
+ import { VideoComment } from "@/types"
3
+ import { useEffect, useState } from "react"
4
+ import { DefaultAvatar } from "../default-avatar"
5
+
6
+ export function CommentCard({
7
+ comment,
8
+ replies = []
9
+ }: {
10
+ comment?: VideoComment,
11
+ replies: VideoComment[]
12
+ }) {
13
+
14
+ const [userThumbnail, setUserThumbnail] = useState(comment?.user?.thumbnail || "")
15
+
16
+ useEffect(() => {
17
+ setUserThumbnail(comment?.user?.thumbnail || "")
18
+
19
+ }, [comment?.user?.thumbnail])
20
+
21
+ if (!comment) { return null }
22
+
23
+ const handleBadUserThumbnail = () => {
24
+ try {
25
+ if (userThumbnail) {
26
+ setUserThumbnail("")
27
+ }
28
+ } catch (err) {
29
+
30
+ }
31
+ }
32
+
33
+
34
+ return (
35
+ <div className={cn(
36
+ `flex flex-col`,
37
+
38
+ )}>
39
+ {/* THE COMMENT INFO - HORIZONTAL */}
40
+ <div className={cn(
41
+ `flex flex-col`,
42
+
43
+ )}>
44
+ <div
45
+ className={cn(
46
+ `flex flex-col items-center justify-center`,
47
+ `rounded-full overflow-hidden`,
48
+ `w-26 h-26`
49
+ )}
50
+ >
51
+ {comment.user.thumbnail
52
+ ? <img
53
+ src={comment.user.thumbnail}
54
+ onError={handleBadUserThumbnail}
55
+ />
56
+ : <DefaultAvatar
57
+ username={comment.user.userName}
58
+ bgColor="#fde047"
59
+ textColor="#1c1917"
60
+ width={104}
61
+ roundShape
62
+ />}
63
+ </div>
64
+ </div>
65
+
66
+ {/* THE REPLIES */}
67
+ {/* TODO */}
68
+ </div>
69
+ )
70
+ }
src/app/interface/comment-list/index.tsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { cn } from "@/lib/utils"
4
+ import { VideoComment } from "@/types"
5
+ import { CommentCard } from "../comment-card"
6
+
7
+ export function CommentList({
8
+ comments = []
9
+ }: {
10
+ comments: VideoComment[]
11
+ }) {
12
+
13
+ return (
14
+ <div className={cn(
15
+ `flex flex-col`,
16
+ `w-full space-y-4`
17
+ )}>
18
+ {comments.map(comment => (
19
+ <CommentCard
20
+ key={comment.id}
21
+ comment={comment}
22
+ replies={[]}
23
+ />
24
+ ))}
25
+ </div>
26
+ )
27
+ }
src/app/interface/default-avatar/impl.tsx ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import RSA from "react-string-avatar"
4
+
5
+ export type DefaultAvatarProps = {
6
+ username?: string
7
+ initials?: string
8
+ bgColor?: string
9
+ textColor?: string
10
+ roundShape?: boolean
11
+ cornerRadius?: number
12
+ pictureFormat?: string
13
+ pictureResolution?: number
14
+ width?: number
15
+ pixelated?: boolean
16
+ wrapper?: boolean
17
+ wrapperStyle?: Record<string, any>
18
+ }
19
+
20
+ export type DefaultAvatarComponent = (props: DefaultAvatarProps) => JSX.Element
21
+
22
+ const ReactStringAvatar = RSA as DefaultAvatarComponent
23
+
24
+
25
+ export default function DefaultAvatarImpl({
26
+ username,
27
+ initials: customInitials,
28
+ ...props
29
+ }: DefaultAvatarProps): JSX.Element {
30
+
31
+ const usernameInitials = `${username || ""}`
32
+ .trim()
33
+ .replaceAll("_", " ")
34
+ .replaceAll("-", " ")
35
+ .replace(/([a-z])([A-Z])/g, '$1 $2') // split the camel case
36
+ .split(" ") // split words
37
+ .map(u => u.trim()[0]) // take first char
38
+ .slice(0, 2) // keep first 2 chars
39
+ .join("")
40
+ .toUpperCase()
41
+
42
+ return (
43
+ <ReactStringAvatar
44
+ initials={customInitials || usernameInitials}
45
+ {...props}
46
+ />
47
+ )
48
+ }
src/app/interface/default-avatar/index.tsx CHANGED
@@ -1,48 +1,7 @@
1
  "use client"
2
 
3
- import RSA from "react-string-avatar"
4
 
5
- export type DefaultAvatarProps = {
6
- username?: string
7
- initials?: string
8
- bgColor?: string
9
- textColor?: string
10
- roundShape?: boolean
11
- cornerRadius?: number
12
- pictureFormat?: string
13
- pictureResolution?: number
14
- width?: number
15
- pixelated?: boolean
16
- wrapper?: boolean
17
- wrapperStyle?: Record<string, any>
18
- }
19
-
20
- export type DefaultAvatarComponent = (props: DefaultAvatarProps) => JSX.Element
21
-
22
- const ReactStringAvatar = RSA as DefaultAvatarComponent
23
-
24
-
25
- export default function DefaultAvatar({
26
- username,
27
- initials: customInitials,
28
- ...props
29
- }: DefaultAvatarProps): JSX.Element {
30
-
31
- const usernameInitials = `${username || ""}`
32
- .trim()
33
- .replaceAll("_", " ")
34
- .replaceAll("-", " ")
35
- .replace(/([a-z])([A-Z])/g, '$1 $2') // split the camel case
36
- .split(" ") // split words
37
- .map(u => u.trim()[0]) // take first char
38
- .slice(0, 2) // keep first 2 chars
39
- .join("")
40
- .toUpperCase()
41
-
42
- return (
43
- <ReactStringAvatar
44
- initials={customInitials || usernameInitials}
45
- {...props}
46
- />
47
- )
48
- }
 
1
  "use client"
2
 
3
+ import dynamic from "next/dynamic"
4
 
5
+ export const DefaultAvatar = dynamic(() => import("./impl"), {
6
+ loading: () => null,
7
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/interface/video-card/index.tsx CHANGED
@@ -11,11 +11,7 @@ import { formatDuration } from "@/lib/formatDuration"
11
  import { formatTimeAgo } from "@/lib/formatTimeAgo"
12
  import { isCertifiedUser } from "@/app/certification"
13
  import { transparentImage } from "@/lib/transparentImage"
14
-
15
- const DefaultAvatar = dynamic(() => import("../default-avatar"), {
16
- loading: () => null,
17
- })
18
-
19
 
20
  export function VideoCard({
21
  video,
@@ -184,12 +180,12 @@ export function VideoCard({
184
  </div>
185
  </div>
186
  : <DefaultAvatar
187
- username={video.channel.datasetUser}
188
- bgColor="#fde047"
189
- textColor="#1c1917"
190
- width={36}
191
- roundShape
192
- />}
193
  <div className={cn(
194
  `flex flex-col`,
195
  isCompact ? `` : `flex-grow`
 
11
  import { formatTimeAgo } from "@/lib/formatTimeAgo"
12
  import { isCertifiedUser } from "@/app/certification"
13
  import { transparentImage } from "@/lib/transparentImage"
14
+ import { DefaultAvatar } from "../default-avatar"
 
 
 
 
15
 
16
  export function VideoCard({
17
  video,
 
180
  </div>
181
  </div>
182
  : <DefaultAvatar
183
+ username={video.channel.datasetUser}
184
+ bgColor="#fde047"
185
+ textColor="#1c1917"
186
+ width={36}
187
+ roundShape
188
+ />}
189
  <div className={cn(
190
  `flex flex-col`,
191
  isCompact ? `` : `flex-grow`
src/app/views/public-channel-view/index.tsx CHANGED
@@ -1,16 +1,12 @@
1
  "use client"
2
 
3
  import { useEffect, useState, useTransition } from "react"
4
- import dynamic from "next/dynamic"
5
 
6
  import { useStore } from "@/app/state/useStore"
7
  import { cn } from "@/lib/utils"
8
  import { VideoList } from "@/app/interface/video-list"
9
  import { getChannelVideos } from "@/app/server/actions/ai-tube-hf/getChannelVideos"
10
-
11
- const DefaultAvatar = dynamic(() => import("../../interface/default-avatar"), {
12
- loading: () => null,
13
- })
14
 
15
  export function PublicChannelView() {
16
  const [_isPending, startTransition] = useTransition()
@@ -66,12 +62,12 @@ export function PublicChannelView() {
66
  className="w-full h-full overflow-hidden object-cover"
67
  />
68
  : <DefaultAvatar
69
- username={publicChannel.datasetUser}
70
- bgColor="#fde047"
71
- textColor="#1c1917"
72
- width={160}
73
- roundShape
74
- />}
75
  </div>
76
 
77
  {/* CHANNEL INFO - HORIZONTAL */}
 
1
  "use client"
2
 
3
  import { useEffect, useState, useTransition } from "react"
 
4
 
5
  import { useStore } from "@/app/state/useStore"
6
  import { cn } from "@/lib/utils"
7
  import { VideoList } from "@/app/interface/video-list"
8
  import { getChannelVideos } from "@/app/server/actions/ai-tube-hf/getChannelVideos"
9
+ import { DefaultAvatar } from "@/app/interface/default-avatar"
 
 
 
10
 
11
  export function PublicChannelView() {
12
  const [_isPending, startTransition] = useTransition()
 
62
  className="w-full h-full overflow-hidden object-cover"
63
  />
64
  : <DefaultAvatar
65
+ username={publicChannel.datasetUser}
66
+ bgColor="#fde047"
67
+ textColor="#1c1917"
68
+ width={160}
69
+ roundShape
70
+ />}
71
  </div>
72
 
73
  {/* CHANNEL INFO - HORIZONTAL */}
src/app/views/public-video-view/index.tsx CHANGED
@@ -1,7 +1,6 @@
1
  "use client"
2
 
3
  import { useEffect, useState, useTransition } from "react"
4
- import dynamic from "next/dynamic"
5
  import { RiCheckboxCircleFill } from "react-icons/ri"
6
  import { PiShareFatLight } from "react-icons/pi"
7
  import CopyToClipboard from "react-copy-to-clipboard"
@@ -18,10 +17,7 @@ import { RecommendedVideos } from "@/app/interface/recommended-videos"
18
  import { isCertifiedUser } from "@/app/certification"
19
  import { watchVideo } from "@/app/server/actions/stats"
20
  import { formatTimeAgo } from "@/lib/formatTimeAgo"
21
-
22
- const DefaultAvatar = dynamic(() => import("../../interface/default-avatar"), {
23
- loading: () => null,
24
- })
25
 
26
  export function PublicVideoView() {
27
  const [_pending, startTransition] = useTransition()
@@ -160,12 +156,12 @@ export function PublicVideoView() {
160
  </div>
161
  </div>
162
  : <DefaultAvatar
163
- username={video.channel.datasetUser}
164
- bgColor="#fde047"
165
- textColor="#1c1917"
166
- width={36}
167
- roundShape
168
- />}
169
  </div>
170
  </a>
171
 
 
1
  "use client"
2
 
3
  import { useEffect, useState, useTransition } from "react"
 
4
  import { RiCheckboxCircleFill } from "react-icons/ri"
5
  import { PiShareFatLight } from "react-icons/pi"
6
  import CopyToClipboard from "react-copy-to-clipboard"
 
17
  import { isCertifiedUser } from "@/app/certification"
18
  import { watchVideo } from "@/app/server/actions/stats"
19
  import { formatTimeAgo } from "@/lib/formatTimeAgo"
20
+ import { DefaultAvatar } from "@/app/interface/default-avatar"
 
 
 
21
 
22
  export function PublicVideoView() {
23
  const [_pending, startTransition] = useTransition()
 
156
  </div>
157
  </div>
158
  : <DefaultAvatar
159
+ username={video.channel.datasetUser}
160
+ bgColor="#fde047"
161
+ textColor="#1c1917"
162
+ width={36}
163
+ roundShape
164
+ />}
165
  </div>
166
  </a>
167
 
src/types.ts CHANGED
@@ -441,6 +441,48 @@ export type VideoInfo = {
441
  orientation: VideoOrientation
442
  }
443
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
  export type VideoGenerationModel =
445
  | "HotshotXL"
446
  | "SVD"
@@ -473,6 +515,7 @@ export type InterfaceView =
473
  | "public_music_videos" // public music videos - it's a special category, because music is *cool*
474
  | "not_found"
475
 
 
476
  export type Settings = {
477
  huggingfaceApiKey: string
478
  }
 
441
  orientation: VideoOrientation
442
  }
443
 
444
+ export type PublicUserInfo = {
445
+ id: string
446
+
447
+ type: "normal" | "admin"
448
+
449
+ userName: string
450
+
451
+ firstName: string
452
+
453
+ lastName: string
454
+
455
+ thumbnail: string
456
+
457
+ channels: ChannelInfo[]
458
+ }
459
+
460
+ export type PrivateUserInfo = PublicUserInfo & {
461
+
462
+ // the Hugging Face API token is confidential!
463
+ hfApiToken: string
464
+ }
465
+
466
+ export type VideoComment = {
467
+ id: string
468
+
469
+ user: PublicUserInfo
470
+
471
+ // if the video comment is in response to another comment,
472
+ // then "inReplyTo" will contain the other video comment id
473
+ inReplyTo?: string
474
+
475
+ createdAt: string
476
+ updatedAt: string
477
+ message: string
478
+
479
+ // how many likes did the comment receive
480
+ nbLikes: number
481
+
482
+ // if the comment was appreciated by the video owner
483
+ appreciated: number
484
+ }
485
+
486
  export type VideoGenerationModel =
487
  | "HotshotXL"
488
  | "SVD"
 
515
  | "public_music_videos" // public music videos - it's a special category, because music is *cool*
516
  | "not_found"
517
 
518
+
519
  export type Settings = {
520
  huggingfaceApiKey: string
521
  }