Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
Commit
•
e4d3d8a
1
Parent(s):
0f35d4c
work in progress on the comment system
Browse files- src/app/interface/channel-card/index.tsx +1 -6
- src/app/interface/comment-card/index.tsx +70 -0
- src/app/interface/comment-list/index.tsx +27 -0
- src/app/interface/default-avatar/impl.tsx +48 -0
- src/app/interface/default-avatar/index.tsx +4 -45
- src/app/interface/video-card/index.tsx +7 -11
- src/app/views/public-channel-view/index.tsx +7 -11
- src/app/views/public-video-view/index.tsx +7 -11
- src/types.ts +43 -0
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
|
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
|
4 |
|
5 |
-
export
|
6 |
-
|
7 |
-
|
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 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
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 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
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 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
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 |
}
|