Spaces:
Sleeping
Sleeping
Commit
·
1f122c3
0
Parent(s):
This view is limited to 50 files because it contains too many changes.
See raw diff
- .env +9 -0
- .eslintrc.json +3 -0
- .gitignore +37 -0
- .nvmrc +1 -0
- Dockerfile +65 -0
- README.md +13 -0
- TODO.md +6 -0
- components.json +16 -0
- next.config.js +6 -0
- package-lock.json +0 -0
- package.json +78 -0
- postcss.config.js +6 -0
- public/bubble.jpg +0 -0
- public/favicon.ico +0 -0
- public/favicon/favicon-114-precomposed.png +0 -0
- public/favicon/favicon-120-precomposed.png +0 -0
- public/favicon/favicon-144-precomposed.png +0 -0
- public/favicon/favicon-152-precomposed.png +0 -0
- public/favicon/favicon-180-precomposed.png +0 -0
- public/favicon/favicon-192.png +0 -0
- public/favicon/favicon-32.png +0 -0
- public/favicon/favicon-36.png +0 -0
- public/favicon/favicon-48.png +0 -0
- public/favicon/favicon-57.png +0 -0
- public/favicon/favicon-60.png +0 -0
- public/favicon/favicon-72-precomposed.png +0 -0
- public/favicon/favicon-72.png +0 -0
- public/favicon/favicon-76.png +0 -0
- public/favicon/favicon-96.png +0 -0
- public/favicon/favicon.ico +0 -0
- public/favicon/manifest.json +41 -0
- public/icon.png +0 -0
- public/mask.png +0 -0
- public/next.svg +1 -0
- public/vercel.svg +1 -0
- src/app/admin/index.tsx +0 -0
- src/app/favicon.ico +0 -0
- src/app/globals.css +39 -0
- src/app/icon.png +0 -0
- src/app/interface/channel-card/index.tsx +34 -0
- src/app/interface/channel-list/index.tsx +43 -0
- src/app/interface/fonts/index.tsx +7 -0
- src/app/interface/left-menu/index.tsx +45 -0
- src/app/interface/left-menu/menu-item/index.tsx +61 -0
- src/app/interface/top-menu/index.tsx +75 -0
- src/app/interface/video-card/index.tsx +34 -0
- src/app/interface/video-list/index.tsx +43 -0
- src/app/layout.tsx +30 -0
- src/app/main.tsx +36 -0
- src/app/page.tsx +46 -0
.env
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
ADMIN_HUGGING_FACE_API_TOKEN=""
|
3 |
+
ADMIN_HUGGING_FACE_USERNAME=""
|
4 |
+
|
5 |
+
# ----------- CENSORSHIP -------
|
6 |
+
ENABLE_CENSORSHIP=
|
7 |
+
FINGERPRINT_KEY=
|
8 |
+
MODERATION_KEY=
|
9 |
+
|
.eslintrc.json
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"extends": "next/core-web-vitals"
|
3 |
+
}
|
.gitignore
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
2 |
+
|
3 |
+
# dependencies
|
4 |
+
/node_modules
|
5 |
+
/.pnp
|
6 |
+
.pnp.js
|
7 |
+
|
8 |
+
# testing
|
9 |
+
/coverage
|
10 |
+
|
11 |
+
# next.js
|
12 |
+
/.next/
|
13 |
+
/out/
|
14 |
+
|
15 |
+
# production
|
16 |
+
/build
|
17 |
+
|
18 |
+
# misc
|
19 |
+
.DS_Store
|
20 |
+
*.pem
|
21 |
+
|
22 |
+
# debug
|
23 |
+
npm-debug.log*
|
24 |
+
yarn-debug.log*
|
25 |
+
yarn-error.log*
|
26 |
+
|
27 |
+
# local env files
|
28 |
+
.env*.local
|
29 |
+
|
30 |
+
# vercel
|
31 |
+
.vercel
|
32 |
+
|
33 |
+
# typescript
|
34 |
+
*.tsbuildinfo
|
35 |
+
next-env.d.ts
|
36 |
+
|
37 |
+
/sandbox/
|
.nvmrc
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
v20.9.0
|
Dockerfile
ADDED
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM node:20-alpine AS base
|
2 |
+
|
3 |
+
# Install dependencies only when needed
|
4 |
+
FROM base AS deps
|
5 |
+
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
6 |
+
RUN apk add --no-cache libc6-compat
|
7 |
+
WORKDIR /app
|
8 |
+
|
9 |
+
# Install dependencies based on the preferred package manager
|
10 |
+
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
|
11 |
+
RUN \
|
12 |
+
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
|
13 |
+
elif [ -f package-lock.json ]; then npm ci; \
|
14 |
+
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
|
15 |
+
else echo "Lockfile not found." && exit 1; \
|
16 |
+
fi
|
17 |
+
|
18 |
+
# Uncomment the following lines if you want to use a secret at buildtime,
|
19 |
+
# for example to access your private npm packages
|
20 |
+
# RUN --mount=type=secret,id=HF_EXAMPLE_SECRET,mode=0444,required=true \
|
21 |
+
# $(cat /run/secrets/HF_EXAMPLE_SECRET)
|
22 |
+
|
23 |
+
# Rebuild the source code only when needed
|
24 |
+
FROM base AS builder
|
25 |
+
WORKDIR /app
|
26 |
+
COPY --from=deps /app/node_modules ./node_modules
|
27 |
+
COPY . .
|
28 |
+
|
29 |
+
# Next.js collects completely anonymous telemetry data about general usage.
|
30 |
+
# Learn more here: https://nextjs.org/telemetry
|
31 |
+
# Uncomment the following line in case you want to disable telemetry during the build.
|
32 |
+
# ENV NEXT_TELEMETRY_DISABLED 1
|
33 |
+
|
34 |
+
# RUN yarn build
|
35 |
+
|
36 |
+
# If you use yarn, comment out this line and use the line above
|
37 |
+
RUN npm run build
|
38 |
+
|
39 |
+
# Production image, copy all the files and run next
|
40 |
+
FROM base AS runner
|
41 |
+
WORKDIR /app
|
42 |
+
|
43 |
+
ENV NODE_ENV production
|
44 |
+
# Uncomment the following line in case you want to disable telemetry during runtime.
|
45 |
+
# ENV NEXT_TELEMETRY_DISABLED 1
|
46 |
+
|
47 |
+
RUN addgroup --system --gid 1001 nodejs
|
48 |
+
RUN adduser --system --uid 1001 nextjs
|
49 |
+
|
50 |
+
COPY --from=builder /app/public ./public
|
51 |
+
|
52 |
+
# Automatically leverage output traces to reduce image size
|
53 |
+
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
54 |
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
55 |
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
56 |
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/cache ./.next/cache
|
57 |
+
# COPY --from=builder --chown=nextjs:nodejs /app/.next/cache/fetch-cache ./.next/cache/fetch-cache
|
58 |
+
|
59 |
+
USER nextjs
|
60 |
+
|
61 |
+
EXPOSE 3000
|
62 |
+
|
63 |
+
ENV PORT 3000
|
64 |
+
|
65 |
+
CMD ["node", "server.js"]
|
README.md
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: AI Tube
|
3 |
+
emoji: 🍿
|
4 |
+
colorFrom: red
|
5 |
+
colorTo: red
|
6 |
+
sdk: docker
|
7 |
+
pinned: true
|
8 |
+
app_port: 3000
|
9 |
+
---
|
10 |
+
|
11 |
+
# 🍿 AI Tube
|
12 |
+
|
13 |
+
(To be continued)
|
TODO.md
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
Allow browsing some loras
|
3 |
+
|
4 |
+
Funny use cases to try:
|
5 |
+
- Hugging Face
|
6 |
+
- Zelda 64 (will be a bit more tricky since it uses some custom Replicate stuff)
|
components.json
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"$schema": "https://ui.shadcn.com/schema.json",
|
3 |
+
"style": "default",
|
4 |
+
"rsc": true,
|
5 |
+
"tsx": true,
|
6 |
+
"tailwind": {
|
7 |
+
"config": "tailwind.config.js",
|
8 |
+
"css": "app/globals.css",
|
9 |
+
"baseColor": "stone",
|
10 |
+
"cssVariables": false
|
11 |
+
},
|
12 |
+
"aliases": {
|
13 |
+
"components": "@/components",
|
14 |
+
"utils": "@/lib/utils"
|
15 |
+
}
|
16 |
+
}
|
next.config.js
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/** @type {import('next').NextConfig} */
|
2 |
+
const nextConfig = {
|
3 |
+
output: 'standalone',
|
4 |
+
}
|
5 |
+
|
6 |
+
module.exports = nextConfig
|
package-lock.json
ADDED
The diff for this file is too large to render.
See raw diff
|
|
package.json
ADDED
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "ai-tube",
|
3 |
+
"version": "0.0.0",
|
4 |
+
"private": true,
|
5 |
+
"scripts": {
|
6 |
+
"dev": "next dev",
|
7 |
+
"build": "next build",
|
8 |
+
"start": "next start",
|
9 |
+
"lint": "next lint"
|
10 |
+
},
|
11 |
+
"dependencies": {
|
12 |
+
"@huggingface/inference": "^2.6.4",
|
13 |
+
"@radix-ui/react-accordion": "^1.1.2",
|
14 |
+
"@radix-ui/react-avatar": "^1.0.3",
|
15 |
+
"@radix-ui/react-checkbox": "^1.0.4",
|
16 |
+
"@radix-ui/react-collapsible": "^1.0.3",
|
17 |
+
"@radix-ui/react-dialog": "^1.0.4",
|
18 |
+
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
19 |
+
"@radix-ui/react-icons": "^1.3.0",
|
20 |
+
"@radix-ui/react-label": "^2.0.2",
|
21 |
+
"@radix-ui/react-menubar": "^1.0.3",
|
22 |
+
"@radix-ui/react-popover": "^1.0.6",
|
23 |
+
"@radix-ui/react-select": "^1.2.2",
|
24 |
+
"@radix-ui/react-separator": "^1.0.3",
|
25 |
+
"@radix-ui/react-slider": "^1.1.2",
|
26 |
+
"@radix-ui/react-slot": "^1.0.2",
|
27 |
+
"@radix-ui/react-switch": "^1.0.3",
|
28 |
+
"@radix-ui/react-toast": "^1.1.4",
|
29 |
+
"@radix-ui/react-tooltip": "^1.0.6",
|
30 |
+
"@react-spring/web": "^9.7.3",
|
31 |
+
"@types/node": "20.4.2",
|
32 |
+
"@types/react": "18.2.15",
|
33 |
+
"@types/react-dom": "18.2.7",
|
34 |
+
"@types/uuid": "^9.0.2",
|
35 |
+
"autoprefixer": "10.4.14",
|
36 |
+
"class-variance-authority": "^0.6.1",
|
37 |
+
"clsx": "^2.0.0",
|
38 |
+
"cmdk": "^0.2.0",
|
39 |
+
"cookies-next": "^2.1.2",
|
40 |
+
"eslint": "8.45.0",
|
41 |
+
"eslint-config-next": "13.4.10",
|
42 |
+
"hash-wasm": "^4.11.0",
|
43 |
+
"lucide-react": "^0.260.0",
|
44 |
+
"next": "^14.0.3",
|
45 |
+
"pick": "^0.0.1",
|
46 |
+
"postcss": "8.4.26",
|
47 |
+
"pythonia": "^1.0.4",
|
48 |
+
"qs": "^6.11.2",
|
49 |
+
"react": "18.2.0",
|
50 |
+
"react-circular-progressbar": "^2.1.0",
|
51 |
+
"react-dom": "18.2.0",
|
52 |
+
"react-icons": "^4.12.0",
|
53 |
+
"react-smooth-scroll-hook": "^1.3.4",
|
54 |
+
"react-virtualized-auto-sizer": "^1.0.20",
|
55 |
+
"replicate": "^0.17.0",
|
56 |
+
"sbd": "^1.0.19",
|
57 |
+
"sentence-splitter": "^4.3.0",
|
58 |
+
"sharp": "^0.32.5",
|
59 |
+
"styled-components": "^6.0.7",
|
60 |
+
"tailwind-merge": "^1.13.2",
|
61 |
+
"tailwindcss": "3.3.3",
|
62 |
+
"tailwindcss-animate": "^1.0.6",
|
63 |
+
"temp-dir": "^3.0.0",
|
64 |
+
"ts-node": "^10.9.1",
|
65 |
+
"type-fest": "^4.8.2",
|
66 |
+
"typescript": "5.1.6",
|
67 |
+
"usehooks-ts": "^2.9.1",
|
68 |
+
"uuid": "^9.0.0",
|
69 |
+
"zustand": "^4.4.1"
|
70 |
+
},
|
71 |
+
"devDependencies": {
|
72 |
+
"@types/proper-lockfile": "^4.1.2",
|
73 |
+
"@types/qs": "^6.9.7",
|
74 |
+
"@types/react-virtualized": "^9.21.22",
|
75 |
+
"@types/sbd": "^1.0.3",
|
76 |
+
"daisyui": "^3.7.4"
|
77 |
+
}
|
78 |
+
}
|
postcss.config.js
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
module.exports = {
|
2 |
+
plugins: {
|
3 |
+
tailwindcss: {},
|
4 |
+
autoprefixer: {},
|
5 |
+
},
|
6 |
+
}
|
public/bubble.jpg
ADDED
![]() |
public/favicon.ico
ADDED
|
public/favicon/favicon-114-precomposed.png
ADDED
![]() |
public/favicon/favicon-120-precomposed.png
ADDED
![]() |
public/favicon/favicon-144-precomposed.png
ADDED
![]() |
public/favicon/favicon-152-precomposed.png
ADDED
![]() |
public/favicon/favicon-180-precomposed.png
ADDED
![]() |
public/favicon/favicon-192.png
ADDED
![]() |
public/favicon/favicon-32.png
ADDED
![]() |
public/favicon/favicon-36.png
ADDED
![]() |
public/favicon/favicon-48.png
ADDED
![]() |
public/favicon/favicon-57.png
ADDED
![]() |
public/favicon/favicon-60.png
ADDED
![]() |
public/favicon/favicon-72-precomposed.png
ADDED
![]() |
public/favicon/favicon-72.png
ADDED
![]() |
public/favicon/favicon-76.png
ADDED
![]() |
public/favicon/favicon-96.png
ADDED
![]() |
public/favicon/favicon.ico
ADDED
|
public/favicon/manifest.json
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "pollo",
|
3 |
+
"icons": [
|
4 |
+
{
|
5 |
+
"src": "\/favicon-36.png",
|
6 |
+
"sizes": "36x36",
|
7 |
+
"type": "image\/png",
|
8 |
+
"density": 0.75
|
9 |
+
},
|
10 |
+
{
|
11 |
+
"src": "\/favicon-48.png",
|
12 |
+
"sizes": "48x48",
|
13 |
+
"type": "image\/png",
|
14 |
+
"density": 1
|
15 |
+
},
|
16 |
+
{
|
17 |
+
"src": "\/favicon-72.png",
|
18 |
+
"sizes": "72x72",
|
19 |
+
"type": "image\/png",
|
20 |
+
"density": 1.5
|
21 |
+
},
|
22 |
+
{
|
23 |
+
"src": "\/favicon-96.png",
|
24 |
+
"sizes": "96x96",
|
25 |
+
"type": "image\/png",
|
26 |
+
"density": 2
|
27 |
+
},
|
28 |
+
{
|
29 |
+
"src": "\/favicon-144.png",
|
30 |
+
"sizes": "144x144",
|
31 |
+
"type": "image\/png",
|
32 |
+
"density": 3
|
33 |
+
},
|
34 |
+
{
|
35 |
+
"src": "\/favicon-192.png",
|
36 |
+
"sizes": "192x192",
|
37 |
+
"type": "image\/png",
|
38 |
+
"density": 4
|
39 |
+
}
|
40 |
+
]
|
41 |
+
}
|
public/icon.png
ADDED
![]() |
public/mask.png
ADDED
![]() |
public/next.svg
ADDED
|
public/vercel.svg
ADDED
|
src/app/admin/index.tsx
ADDED
File without changes
|
src/app/favicon.ico
ADDED
|
src/app/globals.css
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@tailwind base;
|
2 |
+
@tailwind components;
|
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 |
+
|
19 |
+
body {
|
20 |
+
color: rgb(var(--foreground-rgb));
|
21 |
+
background: linear-gradient(
|
22 |
+
to bottom,
|
23 |
+
transparent,
|
24 |
+
rgb(var(--background-end-rgb))
|
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 |
+
}
|
src/app/icon.png
ADDED
![]() |
src/app/interface/channel-card/index.tsx
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { cn } from "@/lib/utils"
|
2 |
+
import { ChannelInfo } from "@/types"
|
3 |
+
|
4 |
+
export function ChannelCard({
|
5 |
+
channel,
|
6 |
+
className = "",
|
7 |
+
}: {
|
8 |
+
channel: ChannelInfo
|
9 |
+
className?: string
|
10 |
+
}) {
|
11 |
+
|
12 |
+
return (
|
13 |
+
<div
|
14 |
+
className={cn(
|
15 |
+
`flex flex-col`,
|
16 |
+
`w-[300px] h-[400px]`,
|
17 |
+
`bg-line-900`,
|
18 |
+
className,
|
19 |
+
)}>
|
20 |
+
<div
|
21 |
+
className={cn(
|
22 |
+
`rounded-lg overflow-hidden`
|
23 |
+
)}
|
24 |
+
>
|
25 |
+
<img src="" />
|
26 |
+
</div>
|
27 |
+
<div className={cn(
|
28 |
+
|
29 |
+
)}>
|
30 |
+
<h3>{channel.label}</h3>
|
31 |
+
</div>
|
32 |
+
</div>
|
33 |
+
)
|
34 |
+
}
|
src/app/interface/channel-list/index.tsx
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { cn } from "@/lib/utils"
|
2 |
+
import { ChannelInfo } from "@/types"
|
3 |
+
|
4 |
+
import { ChannelCard } from "../channel-card"
|
5 |
+
|
6 |
+
export function ChannelList({
|
7 |
+
channels,
|
8 |
+
layout = "flex",
|
9 |
+
className = "",
|
10 |
+
}: {
|
11 |
+
channels: ChannelInfo[]
|
12 |
+
|
13 |
+
/**
|
14 |
+
* Layout mode
|
15 |
+
*
|
16 |
+
* This isn't necessarily based on screen size, it can also be:
|
17 |
+
* - based on the device type (eg. a smart TV)
|
18 |
+
* - a design choice for a particular page
|
19 |
+
*/
|
20 |
+
layout?: "grid" | "flex"
|
21 |
+
|
22 |
+
className?: string
|
23 |
+
}) {
|
24 |
+
|
25 |
+
return (
|
26 |
+
<div
|
27 |
+
className={cn(
|
28 |
+
layout === "grid"
|
29 |
+
? `grid grid-cols-4 gap-4`
|
30 |
+
: `flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4`,
|
31 |
+
className,
|
32 |
+
)}
|
33 |
+
>
|
34 |
+
{channels.map((channel) => (
|
35 |
+
<ChannelCard
|
36 |
+
key={channel.id}
|
37 |
+
channel={channel}
|
38 |
+
className=""
|
39 |
+
/>
|
40 |
+
))}
|
41 |
+
</div>
|
42 |
+
)
|
43 |
+
}
|
src/app/interface/fonts/index.tsx
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { Grandstander, Klee_One } from 'next/font/google'
|
4 |
+
|
5 |
+
// If loading a variable font, you don't need to specify the font weight
|
6 |
+
export const headingFont = Grandstander({ subsets: ['latin'] })
|
7 |
+
export const paragraphFont = Klee_One({ subsets: ['latin'], weight: "600" })
|
src/app/interface/left-menu/index.tsx
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { GrChannel } from "react-icons/gr"
|
2 |
+
import { MdVideoLibrary } from "react-icons/md"
|
3 |
+
import { RiHome8Line } from "react-icons/ri"
|
4 |
+
|
5 |
+
import { useStore } from "@/app/state/useStore"
|
6 |
+
import { cn } from "@/lib/utils"
|
7 |
+
import { MenuItem } from "./menu-item"
|
8 |
+
|
9 |
+
export function LeftMenu() {
|
10 |
+
const view = useStore(s => s.view)
|
11 |
+
const setView = useStore(s => s.setView)
|
12 |
+
return (
|
13 |
+
<div className={cn(
|
14 |
+
`flex flex-col items-center`,
|
15 |
+
`justify-items-stretch`,
|
16 |
+
`w-24 px-1 pt-4`,
|
17 |
+
// `bg-orange-500`,
|
18 |
+
)}>
|
19 |
+
<MenuItem
|
20 |
+
icon={<RiHome8Line className="h-6 w-6" />}
|
21 |
+
selected={view === "home"}
|
22 |
+
onClick={() => setView("home")}
|
23 |
+
>
|
24 |
+
Home
|
25 |
+
</MenuItem>
|
26 |
+
<MenuItem
|
27 |
+
icon={<GrChannel className="h-5 w-5" />}
|
28 |
+
selected={view === "channels_public"}
|
29 |
+
onClick={() => setView("channels_public")}
|
30 |
+
>
|
31 |
+
Channels
|
32 |
+
</MenuItem>
|
33 |
+
<MenuItem
|
34 |
+
icon={<MdVideoLibrary className="h-6 w-6" />}
|
35 |
+
selected={
|
36 |
+
view === "channels_admin" ||
|
37 |
+
view === "channel_admin"
|
38 |
+
}
|
39 |
+
onClick={() => setView("channels_admin")}
|
40 |
+
>
|
41 |
+
My Content
|
42 |
+
</MenuItem>
|
43 |
+
</div>
|
44 |
+
)
|
45 |
+
}
|
src/app/interface/left-menu/menu-item/index.tsx
ADDED
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ReactNode } from "react"
|
2 |
+
|
3 |
+
import { cn } from "@/lib/utils"
|
4 |
+
|
5 |
+
export function MenuItem({
|
6 |
+
icon = null,
|
7 |
+
children = null,
|
8 |
+
selected = false,
|
9 |
+
onClick = undefined,
|
10 |
+
className = "",
|
11 |
+
}: {
|
12 |
+
icon?: ReactNode
|
13 |
+
children?: ReactNode
|
14 |
+
selected?: boolean
|
15 |
+
onClick?: () => void
|
16 |
+
className?: string
|
17 |
+
}) {
|
18 |
+
|
19 |
+
return (
|
20 |
+
<div className={cn(
|
21 |
+
`flex flex-col`,
|
22 |
+
`items-center justify-center justify-items-stretch`,
|
23 |
+
// `bg-green-500`,
|
24 |
+
`cursor-pointer`,
|
25 |
+
`w-full h-21`,
|
26 |
+
`p-1`,
|
27 |
+
`group`
|
28 |
+
)}
|
29 |
+
onClick={() => {
|
30 |
+
if (onClick && !selected) {
|
31 |
+
onClick()
|
32 |
+
}
|
33 |
+
}}
|
34 |
+
>
|
35 |
+
<div
|
36 |
+
className={cn(
|
37 |
+
`flex flex-col`,
|
38 |
+
`items-center justify-center`,
|
39 |
+
`w-full h-full`,
|
40 |
+
`space-y-1.5`,
|
41 |
+
`rounded-xl`,
|
42 |
+
`text-xs`,
|
43 |
+
`transition-all duration-300 ease-in-out`,
|
44 |
+
// `bg-yellow-500`,
|
45 |
+
|
46 |
+
selected
|
47 |
+
? `bg-neutral-100/10`
|
48 |
+
: `group-hover:bg-neutral-100/10 bg-neutral-100/0`,
|
49 |
+
|
50 |
+
|
51 |
+
className,
|
52 |
+
)}
|
53 |
+
>
|
54 |
+
{icon}
|
55 |
+
<div className="text-center">
|
56 |
+
{children}
|
57 |
+
</div>
|
58 |
+
</div>
|
59 |
+
</div>
|
60 |
+
)
|
61 |
+
}
|
src/app/interface/top-menu/index.tsx
ADDED
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { VideoCategory, videoCategoriesWithLabels } from "@/app/state/categories"
|
2 |
+
import { useStore } from "@/app/state/useStore"
|
3 |
+
import { cn } from "@/lib/utils"
|
4 |
+
|
5 |
+
export function TopMenu() {
|
6 |
+
const displayMode = useStore(s => s.displayMode)
|
7 |
+
const setDisplayMode = useStore(s => s.setDisplayMode)
|
8 |
+
const currentChannel = useStore(s => s.currentChannel)
|
9 |
+
const setCurrentChannel = useStore(s => s.setCurrentChannel)
|
10 |
+
const currentCategory = useStore(s => s.currentCategory)
|
11 |
+
const setCurrentCategory = useStore(s => s.setCurrentCategory)
|
12 |
+
const currentVideos = useStore(s => s.currentVideos)
|
13 |
+
const currentVideo = useStore(s => s.currentVideo)
|
14 |
+
const setCurrentVideo = useStore(s => s.setCurrentVideo)
|
15 |
+
|
16 |
+
return (
|
17 |
+
<div className={cn(
|
18 |
+
`flex flex-col`,
|
19 |
+
`overflow-hidden`,
|
20 |
+
`w-full h-28`,
|
21 |
+
)}>
|
22 |
+
<div className={cn(
|
23 |
+
`flex flex-row justify-between`,
|
24 |
+
`w-full`
|
25 |
+
)}>
|
26 |
+
<div className={cn(
|
27 |
+
`flex flex-col items-center justify-center`,
|
28 |
+
`px-4 py-2 w-32`,
|
29 |
+
)}>
|
30 |
+
<div className="flex flex-row items-center">
|
31 |
+
<span className="text-3xl mr-1">🍿 </span>
|
32 |
+
<span className="text-xl font-semibold">HugTube</span>
|
33 |
+
</div>
|
34 |
+
</div>
|
35 |
+
<div className={cn(
|
36 |
+
`flex flex-col items-center justify-center`,
|
37 |
+
`px-4 py-2 w-max-64`,
|
38 |
+
)}>
|
39 |
+
Search bar goes here
|
40 |
+
</div>
|
41 |
+
<div className={cn()}>
|
42 |
+
{/* unused for now */}
|
43 |
+
</div>
|
44 |
+
</div>
|
45 |
+
<div className={cn(
|
46 |
+
`flex flex-row space-x-3`,
|
47 |
+
`text-[13px] font-semibold`,
|
48 |
+
)}>
|
49 |
+
{Object.entries(videoCategoriesWithLabels)
|
50 |
+
.map(([ key, label ]) => (
|
51 |
+
<div
|
52 |
+
key={key}
|
53 |
+
className={cn(
|
54 |
+
`flex flex-col items-center justify-center`,
|
55 |
+
`rounded-lg px-3 py-1 h-8`,
|
56 |
+
`cursor-pointer`,
|
57 |
+
`transition-all duration-300 ease-in-out`,
|
58 |
+
currentCategory === key
|
59 |
+
? `bg-neutral-100 text-neutral-800`
|
60 |
+
: `bg-neutral-800 text-neutral-50/90 hover:bg-neutral-700 hover:text-neutral-50/90`,
|
61 |
+
// `text-clip`
|
62 |
+
)}
|
63 |
+
onClick={() => {
|
64 |
+
setCurrentCategory(key as VideoCategory)
|
65 |
+
}}
|
66 |
+
>
|
67 |
+
<span className={cn(
|
68 |
+
`text-center`
|
69 |
+
)}>{label}</span>
|
70 |
+
</div>
|
71 |
+
))}
|
72 |
+
</div>
|
73 |
+
</div>
|
74 |
+
)
|
75 |
+
}
|
src/app/interface/video-card/index.tsx
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { cn } from "@/lib/utils"
|
2 |
+
import { FullVideoInfo } from "@/types"
|
3 |
+
|
4 |
+
export function VideoCard({
|
5 |
+
video,
|
6 |
+
className = "",
|
7 |
+
}: {
|
8 |
+
video: FullVideoInfo
|
9 |
+
className?: string
|
10 |
+
}) {
|
11 |
+
|
12 |
+
return (
|
13 |
+
<div
|
14 |
+
className={cn(
|
15 |
+
`flex flex-col`,
|
16 |
+
`w-[300px] h-[400px]`,
|
17 |
+
`bg-line-900`,
|
18 |
+
className,
|
19 |
+
)}>
|
20 |
+
<div
|
21 |
+
className={cn(
|
22 |
+
`rounded-lg overflow-hidden`
|
23 |
+
)}
|
24 |
+
>
|
25 |
+
<img src="" />
|
26 |
+
</div>
|
27 |
+
<div className={cn(
|
28 |
+
|
29 |
+
)}>
|
30 |
+
<h3>{video.label}</h3>
|
31 |
+
</div>
|
32 |
+
</div>
|
33 |
+
)
|
34 |
+
}
|
src/app/interface/video-list/index.tsx
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { cn } from "@/lib/utils"
|
2 |
+
import { FullVideoInfo } from "@/types"
|
3 |
+
|
4 |
+
import { VideoCard } from "../video-card"
|
5 |
+
|
6 |
+
export function VideoList({
|
7 |
+
videos,
|
8 |
+
layout = "flex",
|
9 |
+
className = "",
|
10 |
+
}: {
|
11 |
+
videos: FullVideoInfo[]
|
12 |
+
|
13 |
+
/**
|
14 |
+
* Layout mode
|
15 |
+
*
|
16 |
+
* This isn't necessarily based on screen size, it can also be:
|
17 |
+
* - based on the device type (eg. a smart TV)
|
18 |
+
* - a design choice for a particular page
|
19 |
+
*/
|
20 |
+
layout?: "grid" | "flex"
|
21 |
+
|
22 |
+
className?: string
|
23 |
+
}) {
|
24 |
+
|
25 |
+
return (
|
26 |
+
<div
|
27 |
+
className={cn(
|
28 |
+
layout === "grid"
|
29 |
+
? `grid grid-cols-4 gap-4`
|
30 |
+
: `flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4`,
|
31 |
+
className,
|
32 |
+
)}
|
33 |
+
>
|
34 |
+
{videos.map((video) => (
|
35 |
+
<VideoCard
|
36 |
+
key={video.id}
|
37 |
+
video={video}
|
38 |
+
className=""
|
39 |
+
/>
|
40 |
+
))}
|
41 |
+
</div>
|
42 |
+
)
|
43 |
+
}
|
src/app/layout.tsx
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { Metadata } from 'next'
|
2 |
+
import { Inter } from 'next/font/google'
|
3 |
+
|
4 |
+
import { cn } from '@/lib/utils'
|
5 |
+
|
6 |
+
import './globals.css'
|
7 |
+
|
8 |
+
const inter = Inter({ subsets: ['latin'] })
|
9 |
+
|
10 |
+
export const metadata: Metadata = {
|
11 |
+
title: '🍿 AI Tube',
|
12 |
+
description: '🍿 AI Tube',
|
13 |
+
}
|
14 |
+
|
15 |
+
export default function RootLayout({
|
16 |
+
children,
|
17 |
+
}: {
|
18 |
+
children: React.ReactNode
|
19 |
+
}) {
|
20 |
+
return (
|
21 |
+
<html lang="en">
|
22 |
+
<body className={cn(
|
23 |
+
`h-full w-full overflow-auto`,
|
24 |
+
inter.className
|
25 |
+
)}>
|
26 |
+
{children}
|
27 |
+
</body>
|
28 |
+
</html>
|
29 |
+
)
|
30 |
+
}
|
src/app/main.tsx
ADDED
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { cn } from "@/lib/utils"
|
4 |
+
import { TopMenu } from "./interface/top-menu"
|
5 |
+
import { LeftMenu } from "./interface/left-menu"
|
6 |
+
import { useStore } from "./state/useStore"
|
7 |
+
import { HomeView } from "./views/home-view"
|
8 |
+
import { ChannelsPublicView } from "./views/channels-public-view"
|
9 |
+
import { ChannelsAdminView } from "./views/channels-admin-view"
|
10 |
+
import { ChannelPublicView } from "./views/channel-public-view"
|
11 |
+
import { ChannelAdminView } from "./views/channel-admin-view"
|
12 |
+
import { VideoPublicView } from "./views/video-public-view"
|
13 |
+
|
14 |
+
export function Main() {
|
15 |
+
const view = useStore(s => s.view)
|
16 |
+
|
17 |
+
return (
|
18 |
+
<div className={cn(
|
19 |
+
`flex flex-row h-screen w-screen inset-0 overflow-hidden`,
|
20 |
+
)}>
|
21 |
+
<LeftMenu />
|
22 |
+
<div className={cn(
|
23 |
+
`flex flex-col`,
|
24 |
+
`w-[calc(100vh-56px)]`
|
25 |
+
)}>
|
26 |
+
<TopMenu />
|
27 |
+
{view === "home" && <HomeView />}
|
28 |
+
{view === "channels_admin" && <ChannelsAdminView />}
|
29 |
+
{view === "channels_public" && <ChannelsPublicView />}
|
30 |
+
{view === "channel_public" && <ChannelPublicView />}
|
31 |
+
{view === "channel_admin" && <ChannelAdminView />}
|
32 |
+
{view === "video_public" && <VideoPublicView />}
|
33 |
+
</div>
|
34 |
+
</div>
|
35 |
+
)
|
36 |
+
}
|
src/app/page.tsx
ADDED
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
|
9 |
+
import { Main } from "./main"
|
10 |
+
|
11 |
+
// https://nextjs.org/docs/pages/building-your-application/optimizing/fonts
|
12 |
+
|
13 |
+
export default function Page() {
|
14 |
+
const [isLoaded, setLoaded] = useState(false)
|
15 |
+
useEffect(() => { setLoaded(true) }, [])
|
16 |
+
return (
|
17 |
+
<>
|
18 |
+
<Head>
|
19 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
20 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" crossOrigin="anonymous" />
|
21 |
+
<meta name="viewport" content="width=device-width, initial-scale=0.86, maximum-scale=5.0, minimum-scale=0.86" />
|
22 |
+
</Head>
|
23 |
+
<main className={cn(
|
24 |
+
`light text-neutral-100`,
|
25 |
+
// `bg-gradient-to-r from-green-500 to-yellow-400`,
|
26 |
+
`bg-gradient-to-r from-neutral-950 to-neutral-950`,
|
27 |
+
)}>
|
28 |
+
{isLoaded && <Main />}
|
29 |
+
{/*
|
30 |
+
TODO: use a new tracker
|
31 |
+
This is the kind of project on which we want custom analytics!
|
32 |
+
<Script src="https://www.googletagmanager.com/gtag/js?id=GTM-NJ2ZZFBX" />
|
33 |
+
<Script id="google-analytics">
|
34 |
+
{`
|
35 |
+
window.dataLayer = window.dataLayer || [];
|
36 |
+
function gtag(){dataLayer.push(arguments);}
|
37 |
+
gtag('js', new Date());
|
38 |
+
|
39 |
+
gtag('config', 'GTM-NJ2ZZFBX');
|
40 |
+
`}
|
41 |
+
</Script>
|
42 |
+
*/}
|
43 |
+
</main>
|
44 |
+
</>
|
45 |
+
)
|
46 |
+
}
|