jbilcke-hf HF staff commited on
Commit
df83860
1 Parent(s): 93f8352

working on the route system

Browse files
src/app/channel/page.tsx ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import { Main } from "../main"
2
+
3
+ export default async function ChannelPage() {
4
+ return (<Main />)
5
+ }
src/app/channels/page.tsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState, useTransition } from "react"
2
+ import Head from "next/head"
3
+ import Script from "next/script"
4
+ import { Metadata, ResolvingMetadata } from "next"
5
+
6
+
7
+ import { Main } from "../main"
8
+
9
+ export default async function ChannelsPage() {
10
+ return (<Main />)
11
+ }
src/app/globals.css CHANGED
@@ -3,16 +3,16 @@
3
  @tailwind utilities;
4
 
5
  :root {
6
- --foreground-rgb: 0, 0, 0;
7
- --background-start-rgb: 214, 219, 220;
8
- --background-end-rgb: 255, 255, 255;
9
  }
10
 
11
  @media (prefers-color-scheme: dark) {
12
  :root {
13
  --foreground-rgb: 255, 255, 255;
14
- --background-start-rgb: 0, 0, 0;
15
- --background-end-rgb: 0, 0, 0;
16
  }
17
  }
18
 
@@ -25,15 +25,3 @@ body {
25
  )
26
  rgb(var(--background-start-rgb));
27
  }
28
-
29
-
30
- /* this is the trick to bypass the style={{}} attribute when printing */
31
- @media print {
32
- .comic-page[style] { width: 100vw !important; }
33
- }
34
-
35
-
36
- .render-to-image .comic-panel {
37
- height: auto !important;
38
- /* max-width: fit-content !important; */
39
- }
 
3
  @tailwind utilities;
4
 
5
  :root {
6
+ --foreground-rgb: 255, 255, 255;
7
+ --background-start-rgb: 10, 10, 10;
8
+ --background-end-rgb: 10, 10, 10;
9
  }
10
 
11
  @media (prefers-color-scheme: dark) {
12
  :root {
13
  --foreground-rgb: 255, 255, 255;
14
+ --background-start-rgb: 10, 10, 10;
15
+ --background-end-rgb: 10, 10, 10;
16
  }
17
  }
18
 
 
25
  )
26
  rgb(var(--background-start-rgb));
27
  }
 
 
 
 
 
 
 
 
 
 
 
 
src/app/interface/left-menu/index.tsx CHANGED
@@ -8,6 +8,7 @@ import { useStore } from "@/app/state/useStore"
8
  import { cn } from "@/lib/utils"
9
  import { MenuItem } from "./menu-item"
10
  import { showBetaFeatures } from "@/app/config"
 
11
 
12
 
13
  export function LeftMenu() {
@@ -26,20 +27,24 @@ export function LeftMenu() {
26
  <div className={cn(
27
  `flex flex-col w-full`,
28
  )}>
29
- <MenuItem
30
- icon={<RiHome8Line className="h-6 w-6" />}
31
- selected={view === "home"}
32
- onClick={() => setView("home")}
33
- >
34
- Discover
35
- </MenuItem>
36
- <MenuItem
37
- icon={<GrChannel className="h-5 w-5" />}
38
- selected={view === "public_channels"}
39
- onClick={() => setView("public_channels")}
40
- >
41
- Channels
42
- </MenuItem>
 
 
 
 
43
  </div>
44
  <div className={cn(
45
  `flex flex-col w-full`,
 
8
  import { cn } from "@/lib/utils"
9
  import { MenuItem } from "./menu-item"
10
  import { showBetaFeatures } from "@/app/config"
11
+ import Link from "next/link"
12
 
13
 
14
  export function LeftMenu() {
 
27
  <div className={cn(
28
  `flex flex-col w-full`,
29
  )}>
30
+ <Link href="/">
31
+ <MenuItem
32
+ icon={<RiHome8Line className="h-6 w-6" />}
33
+ selected={view === "home"}
34
+ // </Link>onClick={() => setView("home")}
35
+ >
36
+ Discover
37
+ </MenuItem>
38
+ </Link>
39
+ <Link href="/channels">
40
+ <MenuItem
41
+ icon={<GrChannel className="h-5 w-5" />}
42
+ selected={view === "public_channels"}
43
+ // onClick={() => setView("public_channels")}
44
+ >
45
+ Channels
46
+ </MenuItem>
47
+ </Link>
48
  </div>
49
  <div className={cn(
50
  `flex flex-col w-full`,
src/app/interface/top-header/index.tsx CHANGED
@@ -1,3 +1,5 @@
 
 
1
  import { Pathway_Gothic_One } from 'next/font/google'
2
  import { PiPopcornBold } from "react-icons/pi"
3
 
@@ -11,9 +13,10 @@ const pathway = Pathway_Gothic_One({
11
  import { videoCategoriesWithLabels } from "@/app/state/categories"
12
  import { useStore } from "@/app/state/useStore"
13
  import { cn } from "@/lib/utils"
14
- import { useEffect } from 'react'
15
 
16
  export function TopHeader() {
 
17
  const view = useStore(s => s.view)
18
  const setView = useStore(s => s.setView)
19
  const displayMode = useStore(s => s.displayMode)
@@ -32,6 +35,9 @@ export function TopHeader() {
32
  const currentVideos = useStore(s => s.currentVideos)
33
  const currentVideo = useStore(s => s.currentVideo)
34
 
 
 
 
35
  const isNormalSize = headerMode === "normal"
36
 
37
 
@@ -44,6 +50,13 @@ export function TopHeader() {
44
  setMenuMode("normal_icon")
45
  }
46
  }, [view])
 
 
 
 
 
 
 
47
 
48
  return (
49
  <div className={cn(
@@ -109,27 +122,27 @@ export function TopHeader() {
109
  `text-[13px] font-semibold`,
110
  `mb-4`
111
  )}>
112
- {Object.entries(videoCategoriesWithLabels)
113
- .map(([ key, label ]) => (
114
  <div
115
- key={key}
116
  className={cn(
117
  `flex flex-col items-center justify-center`,
118
  `rounded-lg px-3 py-1 h-8`,
119
  `cursor-pointer`,
120
  `transition-all duration-200 ease-in-out`,
121
- currentTag === key
122
  ? `bg-neutral-100 text-neutral-800`
123
  : `bg-neutral-800 text-neutral-50/90 hover:bg-neutral-700 hover:text-neutral-50/90`,
124
  // `text-clip`
125
  )}
126
  onClick={() => {
127
- setCurrentTag(key)
128
  }}
129
  >
130
  <span className={cn(
131
- `text-center`
132
- )}>{label}</span>
 
133
  </div>
134
  ))}
135
  </div> : null}
 
1
+ import { useEffect, useTransition } from 'react'
2
+
3
  import { Pathway_Gothic_One } from 'next/font/google'
4
  import { PiPopcornBold } from "react-icons/pi"
5
 
 
13
  import { videoCategoriesWithLabels } from "@/app/state/categories"
14
  import { useStore } from "@/app/state/useStore"
15
  import { cn } from "@/lib/utils"
16
+ import { getTags } from '@/app/server/actions/ai-tube-hf/getTags'
17
 
18
  export function TopHeader() {
19
+ const [_pending, startTransition] = useTransition()
20
  const view = useStore(s => s.view)
21
  const setView = useStore(s => s.setView)
22
  const displayMode = useStore(s => s.displayMode)
 
35
  const currentVideos = useStore(s => s.currentVideos)
36
  const currentVideo = useStore(s => s.currentVideo)
37
 
38
+ const currentTags = useStore(s => s.currentTags)
39
+ const setCurrentTags = useStore(s => s.setCurrentTags)
40
+
41
  const isNormalSize = headerMode === "normal"
42
 
43
 
 
50
  setMenuMode("normal_icon")
51
  }
52
  }, [view])
53
+
54
+ useEffect(() => {
55
+ startTransition(async () => {
56
+ const tags = await getTags()
57
+ setCurrentTags(tags)
58
+ })
59
+ }, [])
60
 
61
  return (
62
  <div className={cn(
 
122
  `text-[13px] font-semibold`,
123
  `mb-4`
124
  )}>
125
+ {currentTags.slice(0, 9).map(tag => (
 
126
  <div
127
+ key={tag}
128
  className={cn(
129
  `flex flex-col items-center justify-center`,
130
  `rounded-lg px-3 py-1 h-8`,
131
  `cursor-pointer`,
132
  `transition-all duration-200 ease-in-out`,
133
+ currentTag === tag
134
  ? `bg-neutral-100 text-neutral-800`
135
  : `bg-neutral-800 text-neutral-50/90 hover:bg-neutral-700 hover:text-neutral-50/90`,
136
  // `text-clip`
137
  )}
138
  onClick={() => {
139
+ setCurrentTag(currentTag === tag ? undefined : tag)
140
  }}
141
  >
142
  <span className={cn(
143
+ `text-center`,
144
+ `capitalize`,
145
+ )}>{tag}</span>
146
  </div>
147
  ))}
148
  </div> : null}
src/app/interface/tube-layout/index.tsx ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+ import { ReactNode } from "react"
3
+
4
+ import { cn } from "@/lib/utils"
5
+ import { useStore } from "@/app/state/useStore"
6
+
7
+ import { LeftMenu } from "../left-menu"
8
+ import { TopHeader } from "../top-header"
9
+
10
+ export function TubeLayout({ children }: { children?: ReactNode }) {
11
+ const headerMode = useStore(s => s.headerMode)
12
+ const view = useStore(s => s.view)
13
+ return (
14
+ <div className={cn(
15
+ `dark flex flex-row h-screen w-screen inset-0 overflow-hidden`,
16
+ view === "public_video"
17
+ ? `bg-gradient-radial from-neutral-900 to-neutral-950`
18
+ : ''// bg-gradient-to-br from-neutral-950 via-neutral-950 to-neutral-950`
19
+
20
+ )}>
21
+ <LeftMenu />
22
+ <div className={cn(
23
+ `flex flex-col`,
24
+ `w-[calc(100vw-96px)]`,
25
+ `px-2`
26
+ )}>
27
+ <TopHeader />
28
+ <main className={cn(
29
+ `w-full overflow-x-hidden overflow-y-scroll`,
30
+ headerMode === "normal"
31
+ ? `h-[calc(100vh-112px)]`
32
+ : `h-[calc(100vh-48px)]`
33
+ )}>
34
+ {children}
35
+ </main>
36
+ </div>
37
+ </div>
38
+ )
39
+ }
src/app/interface/video-card/index.tsx CHANGED
@@ -5,6 +5,7 @@ import { cn } from "@/lib/utils"
5
  import { VideoInfo } from "@/types"
6
  import { formatDuration } from "@/lib/formatDuration"
7
  import { formatTimeAgo } from "@/lib/formatTimeAgo"
 
8
 
9
  export function VideoCard({
10
  video,
@@ -37,83 +38,85 @@ export function VideoCard({
37
  }
38
 
39
  return (
40
- <div
41
- className={cn(
42
- `w-full`,
43
- `flex flex-col`,
44
- `bg-line-900`,
45
- `space-y-3`,
46
- `cursor-pointer`,
47
- className,
48
- )}
49
- onPointerEnter={handlePointerEnter}
50
- onPointerLeave={handlePointerLeave}
51
- onClick={handleClick}
52
- >
53
- <div
54
- className={cn(
55
- `flex flex-col aspect-video items-center justify-center`,
56
- `rounded-xl overflow-hidden`,
57
- )}
58
  >
59
- <video
60
- ref={ref}
61
- src={video.assetUrl}
62
- className="w-full"
63
- onLoadedMetadata={handleLoad}
64
- muted
65
- />
 
 
 
 
 
 
66
 
67
- <div className={cn(
68
- ``,
69
- `w-full flex flex-row items-end justify-end`
70
- )}>
71
- <div className={cn(
72
- `-mt-8`,
73
- `mr-0`,
74
- )}
75
- >
76
  <div className={cn(
77
- `mb-[5px]`,
78
- `mr-[5px]`,
79
- `flex flex-col items-center justify-center text-center`,
80
- `bg-neutral-900 rounded`,
81
- `text-2xs font-semibold px-[3px] py-[1px]`,
82
  )}
83
- >{formatDuration(duration)}</div>
84
- </div>
85
- </div>
86
- </div>
87
- <div className={cn(
88
- `flex flex-row space-x-4`,
89
- )}>
90
- <div className="flex flex-col">
91
- <div className="flex w-9 rounded-full overflow-hidden">
92
- <img
93
- src="huggingface-avatar.jpeg"
94
- />
95
  </div>
96
  </div>
97
- <div className="flex flex-col flex-grow">
98
- <h3 className="text-zinc-100 text-base font-medium mb-0 line-clamp-2">{video.label}</h3>
99
- <div className={cn(
100
- `flex flex-row items-center`,
101
- `text-neutral-400 text-sm font-normal space-x-1`,
102
- )}>
103
- <div>{video.channel.label}</div>
104
- <div><RiCheckboxCircleFill className="" /></div>
 
105
  </div>
106
- <div className={cn(
107
- `flex flex-row`,
108
- `text-neutral-400 text-sm font-normal`,
109
- `space-x-1`
110
- )}>
111
- <div>0 views</div>
112
- <div className="font-semibold scale-125">·</div>
113
- <div>{formatTimeAgo(video.updatedAt)}</div>
 
 
 
 
 
 
 
 
 
 
114
  </div>
115
  </div>
116
  </div>
117
- </div>
118
  )
119
  }
 
5
  import { VideoInfo } from "@/types"
6
  import { formatDuration } from "@/lib/formatDuration"
7
  import { formatTimeAgo } from "@/lib/formatTimeAgo"
8
+ import Link from "next/link"
9
 
10
  export function VideoCard({
11
  video,
 
38
  }
39
 
40
  return (
41
+ <Link href={`/watch?v=${video.id}`}>
42
+ <div
43
+ className={cn(
44
+ `w-full`,
45
+ `flex flex-col`,
46
+ `bg-line-900`,
47
+ `space-y-3`,
48
+ `cursor-pointer`,
49
+ className,
50
+ )}
51
+ onPointerEnter={handlePointerEnter}
52
+ onPointerLeave={handlePointerLeave}
53
+ // onClick={handleClick}
 
 
 
 
 
54
  >
55
+ <div
56
+ className={cn(
57
+ `flex flex-col aspect-video items-center justify-center`,
58
+ `rounded-xl overflow-hidden`,
59
+ )}
60
+ >
61
+ <video
62
+ ref={ref}
63
+ src={video.assetUrl}
64
+ className="w-full"
65
+ onLoadedMetadata={handleLoad}
66
+ muted
67
+ />
68
 
69
+ <div className={cn(
70
+ ``,
71
+ `w-full flex flex-row items-end justify-end`
72
+ )}>
 
 
 
 
 
73
  <div className={cn(
74
+ `-mt-8`,
75
+ `mr-0`,
 
 
 
76
  )}
77
+ >
78
+ <div className={cn(
79
+ `mb-[5px]`,
80
+ `mr-[5px]`,
81
+ `flex flex-col items-center justify-center text-center`,
82
+ `bg-neutral-900 rounded`,
83
+ `text-2xs font-semibold px-[3px] py-[1px]`,
84
+ )}
85
+ >{formatDuration(duration)}</div>
86
+ </div>
 
 
87
  </div>
88
  </div>
89
+ <div className={cn(
90
+ `flex flex-row space-x-4`,
91
+ )}>
92
+ <div className="flex flex-col">
93
+ <div className="flex w-9 rounded-full overflow-hidden">
94
+ <img
95
+ src="huggingface-avatar.jpeg"
96
+ />
97
+ </div>
98
  </div>
99
+ <div className="flex flex-col flex-grow">
100
+ <h3 className="text-zinc-100 text-base font-medium mb-0 line-clamp-2">{video.label}</h3>
101
+ <div className={cn(
102
+ `flex flex-row items-center`,
103
+ `text-neutral-400 text-sm font-normal space-x-1`,
104
+ )}>
105
+ <div>{video.channel.label}</div>
106
+ <div><RiCheckboxCircleFill className="" /></div>
107
+ </div>
108
+ <div className={cn(
109
+ `flex flex-row`,
110
+ `text-neutral-400 text-sm font-normal`,
111
+ `space-x-1`
112
+ )}>
113
+ <div>0 views</div>
114
+ <div className="font-semibold scale-125">·</div>
115
+ <div>{formatTimeAgo(video.updatedAt)}</div>
116
+ </div>
117
  </div>
118
  </div>
119
  </div>
120
+ </Link>
121
  )
122
  }
src/app/layout.tsx CHANGED
@@ -4,6 +4,7 @@ import { Roboto } from 'next/font/google'
4
  import { cn } from '@/lib/utils'
5
 
6
  import './globals.css'
 
7
 
8
  const roboto = Roboto({
9
  weight: ['100', '300', '400', '500', '700', '900'],
@@ -24,12 +25,36 @@ export default function RootLayout({
24
  }) {
25
  return (
26
  <html lang="en">
 
 
 
 
 
27
  <body className={cn(
28
  `h-full w-full overflow-auto`,
 
29
  roboto.className
30
  )}>
31
  {children}
32
  </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  </html>
34
  )
35
  }
 
4
  import { cn } from '@/lib/utils'
5
 
6
  import './globals.css'
7
+ import Head from 'next/head'
8
 
9
  const roboto = Roboto({
10
  weight: ['100', '300', '400', '500', '700', '900'],
 
25
  }) {
26
  return (
27
  <html lang="en">
28
+ <Head>
29
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
30
+ <link rel="preconnect" href="https://fonts.googleapis.com" crossOrigin="anonymous" />
31
+ <meta name="viewport" content="width=device-width, initial-scale=0.86, maximum-scale=5.0, minimum-scale=0.86" />
32
+ </Head>
33
  <body className={cn(
34
  `h-full w-full overflow-auto`,
35
+ `dark text-neutral-100 bg-neutral-950`,
36
  roboto.className
37
  )}>
38
  {children}
39
  </body>
40
+
41
+ {/*
42
+ TODO: use a new tracker
43
+
44
+ import Script from "next/script"
45
+
46
+ This is the kind of project on which we want custom analytics!
47
+ <Script src="https://www.googletagmanager.com/gtag/js?id=GTM-NJ2ZZFBX" />
48
+ <Script id="google-analytics">
49
+ {`
50
+ window.dataLayer = window.dataLayer || [];
51
+ function gtag(){dataLayer.push(arguments);}
52
+ gtag('js', new Date());
53
+
54
+ gtag('config', 'GTM-NJ2ZZFBX');
55
+ `}
56
+ </Script>
57
+ */}
58
  </html>
59
  )
60
  }
src/app/main.tsx CHANGED
@@ -1,8 +1,5 @@
1
  "use client"
2
 
3
- import { cn } from "@/lib/utils"
4
- import { TopHeader } from "./interface/top-header"
5
- import { LeftMenu } from "./interface/left-menu"
6
  import { useStore } from "./state/useStore"
7
  import { HomeView } from "./views/home-view"
8
  import { PublicChannelsView } from "./views/public-channels-view"
@@ -11,39 +8,57 @@ import { PublicChannelView } from "./views/public-channel-view"
11
  import { UserChannelView } from "./views/user-channel-view"
12
  import { PublicVideoView } from "./views/public-video-view"
13
  import { UserAccountView } from "./views/user-account-view"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
- export function Main() {
16
  const view = useStore(s => s.view)
17
- const headerMode = useStore(s => s.headerMode)
18
  return (
19
- <div className={cn(
20
- `flex flex-row h-screen w-screen inset-0 overflow-hidden`,
21
- `dark`
22
- )}>
23
- <LeftMenu />
24
- <div className={cn(
25
- `flex flex-col`,
26
- `w-[calc(100vw-96px)]`,
27
- `px-2`
28
- )}>
29
- <TopHeader />
30
- <div className={cn(
31
- `w-full overflow-x-hidden overflow-y-scroll`,
32
- headerMode === "normal"
33
- ? `h-[calc(100vh-112px)]`
34
- : `h-[calc(100vh-48px)]`
35
- )}>
36
- {view === "home" && <HomeView />}
37
- {view === "public_video" && <PublicVideoView />}
38
- {view === "public_channels" && <PublicChannelsView />}
39
- {view === "public_channel" && <PublicChannelView />}
40
- {view === "user_channels" && <UserChannelsView />}
41
- {/*view === "user_videos" && <UserVideosView />*/}
42
- {view === "user_channel" && <UserChannelView />}
43
- {view === "user_account" && <UserAccountView />}
44
-
45
- </div>
46
- </div>
47
- </div>
48
  )
49
  }
 
1
  "use client"
2
 
 
 
 
3
  import { useStore } from "./state/useStore"
4
  import { HomeView } from "./views/home-view"
5
  import { PublicChannelsView } from "./views/public-channels-view"
 
8
  import { UserChannelView } from "./views/user-channel-view"
9
  import { PublicVideoView } from "./views/public-video-view"
10
  import { UserAccountView } from "./views/user-account-view"
11
+ import { NotFoundView } from "./views/not-found-view"
12
+ import { VideoInfo } from "@/types"
13
+ import { useEffect } from "react"
14
+ import { usePathname } from "next/navigation"
15
+ import { TubeLayout } from "./interface/tube-layout"
16
+
17
+ // this is where we transition from the server-side space
18
+ // and the client-side space
19
+ // basically, all the views are generated in client-side space
20
+ // so the role of Main is to map server-side provided params
21
+ // to the Zustand store (client-side)
22
+ //
23
+ // one benefit of doing this is that we will able to add some animations/transitions
24
+ // more easily
25
+ export function Main({
26
+ video
27
+ }: {
28
+ // server side params
29
+ video?: VideoInfo
30
+ }) {
31
+ const pathname = usePathname()
32
+
33
+ const setCurrentVideo = useStore(s => s.setCurrentVideo)
34
+ const setView = useStore(s => s.setView)
35
+ const setPathname = useStore(s => s.setPathname)
36
+
37
+ useEffect(() => {
38
+ if (video?.id) {
39
+ setCurrentVideo(video)
40
+ }
41
+ }, [video?.id])
42
+
43
+
44
+ useEffect(() => {
45
+ setPathname(pathname)
46
+ }, [pathname])
47
+
48
 
 
49
  const view = useStore(s => s.view)
50
+
51
  return (
52
+ <TubeLayout>
53
+ {view === "home" && <HomeView />}
54
+ {view === "public_video" && <PublicVideoView />}
55
+ {view === "public_channels" && <PublicChannelsView />}
56
+ {view === "public_channel" && <PublicChannelView />}
57
+ {view === "user_channels" && <UserChannelsView />}
58
+ {/*view === "user_videos" && <UserVideosView />*/}
59
+ {view === "user_channel" && <UserChannelView />}
60
+ {view === "user_account" && <UserAccountView />}
61
+ {view === "not_found" && <NotFoundView />}
62
+ </TubeLayout>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  )
64
  }
src/app/page.tsx CHANGED
@@ -1,50 +1,8 @@
1
- "use client"
2
-
3
- import { useEffect, useState } from "react"
4
- import Head from "next/head"
5
- import Script from "next/script"
6
-
7
- import { cn } from "@/lib/utils"
8
- import { useStore } from "@/app/state/useStore"
9
 
10
  import { Main } from "./main"
11
 
12
- // https://nextjs.org/docs/pages/building-your-application/optimizing/fonts
13
-
14
  export default function Page() {
15
- const view = useStore(s => s.view)
16
- const [isLoaded, setLoaded] = useState(false)
17
- useEffect(() => { setLoaded(true) }, [])
18
  return (
19
- <>
20
- <Head>
21
- <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
22
- <link rel="preconnect" href="https://fonts.googleapis.com" crossOrigin="anonymous" />
23
- <meta name="viewport" content="width=device-width, initial-scale=0.86, maximum-scale=5.0, minimum-scale=0.86" />
24
- </Head>
25
- <main className={cn(
26
- `light text-neutral-100`,
27
- // `bg-gradient-to-r from-green-500 to-yellow-400`,
28
- view === "public_video"
29
- ? `bg-gradient-radial from-neutral-900 to-neutral-950`
30
- : `bg-neutral-950` // bg-gradient-to-br from-neutral-950 via-neutral-950 to-neutral-950`
31
- )}>
32
- {isLoaded && <Main />}
33
- {/*
34
- TODO: use a new tracker
35
- This is the kind of project on which we want custom analytics!
36
- <Script src="https://www.googletagmanager.com/gtag/js?id=GTM-NJ2ZZFBX" />
37
- <Script id="google-analytics">
38
- {`
39
- window.dataLayer = window.dataLayer || [];
40
- function gtag(){dataLayer.push(arguments);}
41
- gtag('js', new Date());
42
-
43
- gtag('config', 'GTM-NJ2ZZFBX');
44
- `}
45
- </Script>
46
- */}
47
- </main>
48
- </>
49
  )
50
  }
 
 
 
 
 
 
 
 
 
1
 
2
  import { Main } from "./main"
3
 
 
 
4
  export default function Page() {
 
 
 
5
  return (
6
+ <Main />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  )
8
  }
src/app/server/actions/ai-tube-hf/getIndex.ts CHANGED
@@ -4,17 +4,19 @@ import { adminUsername } from "../config"
4
 
5
  export async function getIndex({
6
  status,
7
- renewCache,
 
8
  }: {
9
  status: VideoStatus
10
 
11
  renewCache?: boolean
 
12
  }): Promise<Record<string, VideoInfo>> {
13
  try {
14
  const response = await fetch(
15
  `https://huggingface.co/datasets/${adminUsername}/ai-tube-index/raw/main/${status}.json`
16
  , {
17
- cache: "no-store"
18
  })
19
 
20
  const jsonResponse = await response?.json()
@@ -31,7 +33,10 @@ export async function getIndex({
31
 
32
  return videos
33
  } catch (err) {
34
- console.error(`failed to get index ${status}:`, err)
35
- return {}
 
 
 
36
  }
37
  }
 
4
 
5
  export async function getIndex({
6
  status,
7
+ renewCache = true,
8
+ neverThrow = true,
9
  }: {
10
  status: VideoStatus
11
 
12
  renewCache?: boolean
13
+ neverThrow?: boolean
14
  }): Promise<Record<string, VideoInfo>> {
15
  try {
16
  const response = await fetch(
17
  `https://huggingface.co/datasets/${adminUsername}/ai-tube-index/raw/main/${status}.json`
18
  , {
19
+ cache: renewCache ? "no-store" : "default"
20
  })
21
 
22
  const jsonResponse = await response?.json()
 
33
 
34
  return videos
35
  } catch (err) {
36
+ if (neverThrow) {
37
+ console.error(`failed to get index ${status}:`, err)
38
+ return {}
39
+ }
40
+ throw err
41
  }
42
  }
src/app/server/actions/ai-tube-hf/getTags.ts ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use server"
2
+
3
+ import { getIndex } from "./getIndex"
4
+
5
+ export async function getTags({
6
+ renewCache = true,
7
+ neverThrow = true,
8
+ }: {
9
+ renewCache?: boolean
10
+ neverThrow?: boolean
11
+ } = {
12
+ renewCache: true,
13
+ neverThrow: true,
14
+ }): Promise<string[]> {
15
+ try {
16
+ const published = Object.values(await getIndex({
17
+ status: "published",
18
+ renewCache,
19
+ }))
20
+
21
+ const tags: Record<string, number> = {}
22
+ for (const video of published) {
23
+ for (const tag of video.tags) {
24
+ const key = tag.trim().toLowerCase()
25
+ tags[key] = 1 + (tags[key] || 0)
26
+ }
27
+ }
28
+
29
+ return Object.entries(tags).sort((a, b) => b[1] - a[1]).map(i => i[0])
30
+ } catch (err) {
31
+ if (neverThrow) {
32
+ return []
33
+ }
34
+ throw err
35
+ }
36
+ }
src/app/server/actions/ai-tube-hf/getVideo.ts ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use server"
2
+
3
+ import { VideoInfo } from "@/types"
4
+
5
+ import { getIndex } from "./getIndex"
6
+
7
+ export async function getVideo(videoId?: string | string[] | null): Promise<VideoInfo> {
8
+ const id = `${videoId || ""}`
9
+
10
+ if (!id) {
11
+ throw new Error(`cannot get the video, invalid id: "${id}"`)
12
+ }
13
+ const published = await getIndex({ status: "published" })
14
+
15
+ const video = published[id] || undefined
16
+
17
+ if (!video) {
18
+ throw new Error(`cannot get the video, nothing found for id "${id}"`)
19
+ }
20
+
21
+ return video
22
+ }
src/app/state/{locaStorageKeys.ts → localStorageKeys.ts} RENAMED
File without changes
src/app/state/useStore.ts CHANGED
@@ -17,6 +17,8 @@ export const useStore = create<{
17
  view: InterfaceView
18
  setView: (view?: InterfaceView) => void
19
 
 
 
20
  currentChannel?: ChannelInfo
21
  setCurrentChannel: (currentChannel?: ChannelInfo) => void
22
 
@@ -26,6 +28,9 @@ export const useStore = create<{
26
  currentTag?: string
27
  setCurrentTag: (currentTag?: string) => void
28
 
 
 
 
29
  currentVideos: VideoInfo[]
30
  setCurrentVideos: (currentVideos: VideoInfo[]) => void
31
 
@@ -46,6 +51,16 @@ export const useStore = create<{
46
  set({ view: view || "home" })
47
  },
48
 
 
 
 
 
 
 
 
 
 
 
49
  headerMode: "normal",
50
  setHeaderMode: (headerMode: InterfaceHeaderMode) => {
51
  set({ headerMode })
@@ -73,6 +88,11 @@ export const useStore = create<{
73
  set({ currentTag })
74
  },
75
 
 
 
 
 
 
76
  currentVideos: [],
77
  setCurrentVideos: (currentVideos: VideoInfo[] = []) => {
78
  set({
 
17
  view: InterfaceView
18
  setView: (view?: InterfaceView) => void
19
 
20
+ setPathname: (patname: string) => void
21
+
22
  currentChannel?: ChannelInfo
23
  setCurrentChannel: (currentChannel?: ChannelInfo) => void
24
 
 
28
  currentTag?: string
29
  setCurrentTag: (currentTag?: string) => void
30
 
31
+ currentTags: string[]
32
+ setCurrentTags: (currentTags?: string[]) => void
33
+
34
  currentVideos: VideoInfo[]
35
  setCurrentVideos: (currentVideos: VideoInfo[]) => void
36
 
 
51
  set({ view: view || "home" })
52
  },
53
 
54
+ setPathname: (pathname: string) => {
55
+ const routes: Record<string, InterfaceView> = {
56
+ "/": "home",
57
+ "/watch": "public_video",
58
+ "/channels": "public_channels"
59
+ }
60
+ console.log("setPathname: ", pathname)
61
+ set({ view: routes[pathname] || "not_found" })
62
+ },
63
+
64
  headerMode: "normal",
65
  setHeaderMode: (headerMode: InterfaceHeaderMode) => {
66
  set({ headerMode })
 
88
  set({ currentTag })
89
  },
90
 
91
+ currentTags: [],
92
+ setCurrentTags: (currentTags?: string[]) => {
93
+ set({ currentTags })
94
+ },
95
+
96
  currentVideos: [],
97
  setCurrentVideos: (currentVideos: VideoInfo[] = []) => {
98
  set({
src/app/views/home-view/index.tsx CHANGED
@@ -7,6 +7,7 @@ import { cn } from "@/lib/utils"
7
  import { VideoInfo } from "@/types"
8
  import { getVideos } from "@/app/server/actions/ai-tube-hf/getVideos"
9
  import { VideoList } from "@/app/interface/video-list"
 
10
 
11
  export function HomeView() {
12
  const [_isPending, startTransition] = useTransition()
@@ -18,6 +19,7 @@ export function HomeView() {
18
  const setCurrentChannel = useStore(s => s.setCurrentChannel)
19
  const currentTag = useStore(s => s.currentTag)
20
  const setCurrentTag = useStore(s => s.setCurrentTag)
 
21
  const currentVideos = useStore(s => s.currentVideos)
22
  const setCurrentVideos = useStore(s => s.setCurrentVideos)
23
  const currentVideo = useStore(s => s.currentVideo)
@@ -27,6 +29,7 @@ export function HomeView() {
27
  startTransition(async () => {
28
  const videos = await getVideos({
29
  sortBy: "date",
 
30
  maxVideos: 25
31
  })
32
 
 
7
  import { VideoInfo } from "@/types"
8
  import { getVideos } from "@/app/server/actions/ai-tube-hf/getVideos"
9
  import { VideoList } from "@/app/interface/video-list"
10
+ import { getTags } from "@/app/server/actions/ai-tube-hf/getTags"
11
 
12
  export function HomeView() {
13
  const [_isPending, startTransition] = useTransition()
 
19
  const setCurrentChannel = useStore(s => s.setCurrentChannel)
20
  const currentTag = useStore(s => s.currentTag)
21
  const setCurrentTag = useStore(s => s.setCurrentTag)
22
+ const setCurrentTags = useStore(s => s.setCurrentTags)
23
  const currentVideos = useStore(s => s.currentVideos)
24
  const setCurrentVideos = useStore(s => s.setCurrentVideos)
25
  const currentVideo = useStore(s => s.currentVideo)
 
29
  startTransition(async () => {
30
  const videos = await getVideos({
31
  sortBy: "date",
32
+ tag: currentTag,
33
  maxVideos: 25
34
  })
35
 
src/app/views/not-found-view/index.tsx ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ export function NotFoundView() {
6
+ return (
7
+ <div className={cn(
8
+ `w-full`,
9
+ `flex flex-row`,
10
+ `items-center justify-center`
11
+ )}>
12
+ <h1>Sorry, we couldn&apos;t find this content.</h1>
13
+ </div>
14
+ )
15
+ }
src/app/views/public-video-view/index.tsx CHANGED
@@ -6,13 +6,11 @@ import { RiCheckboxCircleFill } from "react-icons/ri"
6
  import { useStore } from "@/app/state/useStore"
7
  import { cn } from "@/lib/utils"
8
  import { VideoPlayer } from "@/app/interface/video-player"
 
9
 
10
 
11
  export function PublicVideoView() {
12
- const displayMode = useStore(s => s.displayMode)
13
  const video = useStore(s => s.currentVideo)
14
- const setMenuMode = useStore(s => s.setMenuMode)
15
- const setHeaderMode = useStore(s => s.setHeaderMode)
16
 
17
  if (!video) { return null }
18
 
@@ -36,7 +34,7 @@ export function PublicVideoView() {
36
  `text-xl text-zinc-100 font-medium mb-0 line-clamp-2`,
37
  `mb-2`
38
  )}>
39
- {video?.label}
40
  </div>
41
 
42
  {/** VIDEO TOOLBAR - HORIZONTAL */}
 
6
  import { useStore } from "@/app/state/useStore"
7
  import { cn } from "@/lib/utils"
8
  import { VideoPlayer } from "@/app/interface/video-player"
9
+ import { VideoInfo } from "@/types"
10
 
11
 
12
  export function PublicVideoView() {
 
13
  const video = useStore(s => s.currentVideo)
 
 
14
 
15
  if (!video) { return null }
16
 
 
34
  `text-xl text-zinc-100 font-medium mb-0 line-clamp-2`,
35
  `mb-2`
36
  )}>
37
+ {video.label}
38
  </div>
39
 
40
  {/** VIDEO TOOLBAR - HORIZONTAL */}
src/app/views/user-account-view/index.tsx CHANGED
@@ -5,7 +5,7 @@ import { useLocalStorage } from "usehooks-ts"
5
 
6
  import { cn } from "@/lib/utils"
7
  import { Input } from "@/components/ui/input"
8
- import { localStorageKeys } from "@/app/state/locaStorageKeys"
9
  import { defaultSettings } from "@/app/state/defaultSettings"
10
 
11
  export function UserAccountView() {
 
5
 
6
  import { cn } from "@/lib/utils"
7
  import { Input } from "@/components/ui/input"
8
+ import { localStorageKeys } from "@/app/state/localStorageKeys"
9
  import { defaultSettings } from "@/app/state/defaultSettings"
10
 
11
  export function UserAccountView() {
src/app/views/user-channel-view/index.tsx CHANGED
@@ -7,7 +7,7 @@ import { cn } from "@/lib/utils"
7
  import { VideoInfo } from "@/types"
8
 
9
  import { useLocalStorage } from "usehooks-ts"
10
- import { localStorageKeys } from "@/app/state/locaStorageKeys"
11
  import { defaultSettings } from "@/app/state/defaultSettings"
12
  import { Input } from "@/components/ui/input"
13
  import { Textarea } from "@/components/ui/textarea"
 
7
  import { VideoInfo } from "@/types"
8
 
9
  import { useLocalStorage } from "usehooks-ts"
10
+ import { localStorageKeys } from "@/app/state/localStorageKeys"
11
  import { defaultSettings } from "@/app/state/defaultSettings"
12
  import { Input } from "@/components/ui/input"
13
  import { Textarea } from "@/components/ui/textarea"
src/app/views/user-channels-view/index.tsx CHANGED
@@ -7,7 +7,7 @@ import { useStore } from "@/app/state/useStore"
7
  import { cn } from "@/lib/utils"
8
  import { getChannels } from "@/app/server/actions/ai-tube-hf/getChannels"
9
  import { ChannelList } from "@/app/interface/channel-list"
10
- import { localStorageKeys } from "@/app/state/locaStorageKeys"
11
  import { defaultSettings } from "@/app/state/defaultSettings"
12
  import { Input } from "@/components/ui/input"
13
 
 
7
  import { cn } from "@/lib/utils"
8
  import { getChannels } from "@/app/server/actions/ai-tube-hf/getChannels"
9
  import { ChannelList } from "@/app/interface/channel-list"
10
+ import { localStorageKeys } from "@/app/state/localStorageKeys"
11
  import { defaultSettings } from "@/app/state/defaultSettings"
12
  import { Input } from "@/components/ui/input"
13
 
src/app/watch/page.tsx ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState, useTransition } from "react"
2
+ import Head from "next/head"
3
+ import Script from "next/script"
4
+ import { Metadata, ResolvingMetadata } from "next"
5
+
6
+
7
+ import { Main } from "../main"
8
+
9
+ import { getVideo } from "../server/actions/ai-tube-hf/getVideo"
10
+
11
+ type Props = {
12
+ params: { id: string }
13
+ searchParams: {
14
+ v?: string | string[],
15
+ [key: string]: string | string[] | undefined
16
+ }
17
+ }
18
+
19
+ // https://nextjs.org/docs/pages/building-your-application/optimizing/fonts
20
+ export async function generateMetadata(
21
+ { params, searchParams: { v: videoId } }: Props,
22
+ parent: ResolvingMetadata
23
+ ): Promise<Metadata> {
24
+ // read route params
25
+
26
+ const metadataBase = new URL('https://huggingface.co/spaces/jbilcke-hf/ai-tube')
27
+
28
+ try {
29
+ const video = await getVideo(videoId)
30
+
31
+ if (!video) {
32
+ throw new Error("Video not found")
33
+ }
34
+
35
+ return {
36
+ title: `${video.label} - AI Tube`,
37
+ metadataBase: new URL('https://huggingface.co/spaces/jbilcke-hf/ai-tube'),
38
+ openGraph: {
39
+ type: "website",
40
+ // url: "https://example.com",
41
+ title: video.label || "", // put the video title here
42
+ description: video.description || "", // put the vide description here
43
+ siteName: "AI Tube",
44
+
45
+ videos: [
46
+ {
47
+ "url": video.assetUrl
48
+ }
49
+ ],
50
+ // images: ['/some-specific-page-image.jpg', ...previousImages],
51
+ },
52
+ }
53
+ } catch (err) {
54
+ return {
55
+ title: "AI Tube - 404 Video Not Found",
56
+ metadataBase,
57
+ openGraph: {
58
+ type: "website",
59
+ // url: "https://example.com",
60
+ title: "AI Tube - 404 Not Found", // put the video title here
61
+ description: "", // put the vide description here
62
+ siteName: "AI Tube",
63
+
64
+ videos: [],
65
+ images: [],
66
+ },
67
+ }
68
+ }
69
+ }
70
+
71
+
72
+ export default async function WatchPage({ searchParams: { v: videoId } }: Props) {
73
+ // const [_pending, startTransition] = useTransition()
74
+ // const setView = useStore(s => s.setView)
75
+ // const setCurrentVideo = useStore(s => s.setCurrentVideo)
76
+ const id = `${videoId || ""}`
77
+
78
+ const video = await getVideo(videoId)
79
+
80
+ // console.log("got video:", video.id)
81
+ return (
82
+ <Main video={video} />
83
+ )
84
+ }
src/types.ts CHANGED
@@ -366,6 +366,7 @@ export type InterfaceView =
366
  | "public_channels"
367
  | "public_channel" // public view of a channel
368
  | "public_video" // public view of a video
 
369
 
370
  export type Settings = {
371
  huggingfaceApiKey: string
 
366
  | "public_channels"
367
  | "public_channel" // public view of a channel
368
  | "public_video" // public view of a video
369
+ | "not_found"
370
 
371
  export type Settings = {
372
  huggingfaceApiKey: string