Spaces:
Paused
Paused
Commit
·
e5c2fd4
1
Parent(s):
838a15b
starting to work on the ui
Browse files- next.config.js +5 -1
- scripts/test.js +2 -2
- src/api/base.ts +31 -0
- src/api/index.ts +16 -0
- src/app/data/data.ts +71 -0
- src/app/data/mock.json +16 -0
- src/app/data/schema.ts +13 -0
- src/app/forlater/page.tsx +18 -0
- src/app/main.tsx +66 -0
- src/app/page.tsx +2 -2
- src/app/pending/page.tsx +21 -0
- src/app/types.ts +207 -40
- src/components/business/tasks/columns.tsx +121 -0
- src/components/business/tasks/data-table-column-header.tsx +71 -0
- src/components/business/tasks/data-table-faceted-filter.tsx +147 -0
- src/components/business/tasks/data-table-pagination.tsx +97 -0
- src/components/business/tasks/data-table-row-actions.tsx +69 -0
- src/components/business/tasks/data-table-toolbar.tsx +61 -0
- src/components/business/tasks/data-table-view-options.tsx +59 -0
- src/components/business/tasks/data-table.tsx +126 -0
- src/components/business/tasks/user-nav.tsx +62 -0
- src/components/ui/avatar.tsx +50 -0
- src/components/ui/dropdown-menu.tsx +200 -0
- src/components/ui/input.tsx +25 -0
- src/components/ui/popover.tsx +31 -0
- src/components/ui/select.tsx +121 -0
- src/components/ui/separator.tsx +31 -0
- src/components/ui/table.tsx +114 -0
- src/core/hasBadWords.ts +1 -1
- src/core/interpolateVideo.ts +1 -1
next.config.js
CHANGED
@@ -1,4 +1,8 @@
|
|
1 |
/** @type {import('next').NextConfig} */
|
2 |
-
const nextConfig = {
|
|
|
|
|
|
|
|
|
3 |
|
4 |
module.exports = nextConfig
|
|
|
1 |
/** @type {import('next').NextConfig} */
|
2 |
+
const nextConfig = {
|
3 |
+
experimental: {
|
4 |
+
serverActions: true,
|
5 |
+
},
|
6 |
+
}
|
7 |
|
8 |
module.exports = nextConfig
|
scripts/test.js
CHANGED
@@ -2,14 +2,14 @@ const { promises: fs } = require("node:fs")
|
|
2 |
|
3 |
const main = async () => {
|
4 |
console.log('generating shot..')
|
5 |
-
const response = await fetch("http://localhost:3000/api/shot", {
|
6 |
method: "POST",
|
7 |
headers: {
|
8 |
"Accept": "application/json",
|
9 |
"Content-Type": "application/json"
|
10 |
},
|
11 |
body: JSON.stringify({
|
12 |
-
token: process.env.
|
13 |
shotPrompt: "video of a dancing cat"
|
14 |
})
|
15 |
});
|
|
|
2 |
|
3 |
const main = async () => {
|
4 |
console.log('generating shot..')
|
5 |
+
const response = await fetch(process.env."http://localhost:3000/api/shot", {
|
6 |
method: "POST",
|
7 |
headers: {
|
8 |
"Accept": "application/json",
|
9 |
"Content-Type": "application/json"
|
10 |
},
|
11 |
body: JSON.stringify({
|
12 |
+
token: process.env.VC_SECRET_ACCESS_TOKEN,
|
13 |
shotPrompt: "video of a dancing cat"
|
14 |
})
|
15 |
});
|
src/api/base.ts
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
// note: there is no / at the end in the variable
|
3 |
+
// so we have to add it ourselves if needed
|
4 |
+
const apiUrl = process.env.VC_VIDEOCHAIN_API_URL
|
5 |
+
|
6 |
+
export const get = async <T>(path: string = '', defaultValue: T): Promise<T> => {
|
7 |
+
try {
|
8 |
+
const res = await fetch(`${apiUrl}/${path}`, {
|
9 |
+
headers: {
|
10 |
+
method: "GET",
|
11 |
+
Accept: "application/json",
|
12 |
+
Authorization: `Bearer ${process.env.VC_SECRET_ACCESS_TOKEN}`,
|
13 |
+
}
|
14 |
+
})
|
15 |
+
// The return value is *not* serialized
|
16 |
+
// You can return Date, Map, Set, etc.
|
17 |
+
|
18 |
+
// Recommendation: handle errors
|
19 |
+
if (res.status !== 200) {
|
20 |
+
// This will activate the closest `error.js` Error Boundary
|
21 |
+
throw new Error('Failed to fetch data')
|
22 |
+
}
|
23 |
+
|
24 |
+
const data = await res.json()
|
25 |
+
|
26 |
+
return ((data as T) || defaultValue)
|
27 |
+
} catch (err) {
|
28 |
+
console.error(err)
|
29 |
+
return defaultValue
|
30 |
+
}
|
31 |
+
}
|
src/api/index.ts
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
import { VideoTask } from "@/app/types"
|
3 |
+
|
4 |
+
import { get } from "./base"
|
5 |
+
|
6 |
+
export const getPendingTasks = async () => {
|
7 |
+
const tasks = await get<VideoTask[]>("", [])
|
8 |
+
|
9 |
+
return tasks
|
10 |
+
}
|
11 |
+
|
12 |
+
export const getTask = async (id: string) => {
|
13 |
+
const task = await get<VideoTask>(id, null as unknown as VideoTask)
|
14 |
+
|
15 |
+
return task
|
16 |
+
}
|
src/app/data/data.ts
ADDED
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {
|
2 |
+
ArrowDownIcon,
|
3 |
+
ArrowRightIcon,
|
4 |
+
ArrowUpIcon,
|
5 |
+
CheckCircledIcon,
|
6 |
+
CircleIcon,
|
7 |
+
CrossCircledIcon,
|
8 |
+
QuestionMarkCircledIcon,
|
9 |
+
StopwatchIcon,
|
10 |
+
} from "@radix-ui/react-icons"
|
11 |
+
|
12 |
+
export const labels = [
|
13 |
+
{
|
14 |
+
value: "bug",
|
15 |
+
label: "Bug",
|
16 |
+
},
|
17 |
+
{
|
18 |
+
value: "feature",
|
19 |
+
label: "Feature",
|
20 |
+
},
|
21 |
+
{
|
22 |
+
value: "documentation",
|
23 |
+
label: "Documentation",
|
24 |
+
},
|
25 |
+
]
|
26 |
+
|
27 |
+
export const statuses = [
|
28 |
+
{
|
29 |
+
value: "backlog",
|
30 |
+
label: "Backlog",
|
31 |
+
icon: QuestionMarkCircledIcon,
|
32 |
+
},
|
33 |
+
{
|
34 |
+
value: "todo",
|
35 |
+
label: "Todo",
|
36 |
+
icon: CircleIcon,
|
37 |
+
},
|
38 |
+
{
|
39 |
+
value: "in progress",
|
40 |
+
label: "In Progress",
|
41 |
+
icon: StopwatchIcon,
|
42 |
+
},
|
43 |
+
{
|
44 |
+
value: "done",
|
45 |
+
label: "Done",
|
46 |
+
icon: CheckCircledIcon,
|
47 |
+
},
|
48 |
+
{
|
49 |
+
value: "canceled",
|
50 |
+
label: "Canceled",
|
51 |
+
icon: CrossCircledIcon,
|
52 |
+
},
|
53 |
+
]
|
54 |
+
|
55 |
+
export const priorities = [
|
56 |
+
{
|
57 |
+
label: "Low",
|
58 |
+
value: "low",
|
59 |
+
icon: ArrowDownIcon,
|
60 |
+
},
|
61 |
+
{
|
62 |
+
label: "Medium",
|
63 |
+
value: "medium",
|
64 |
+
icon: ArrowRightIcon,
|
65 |
+
},
|
66 |
+
{
|
67 |
+
label: "High",
|
68 |
+
value: "high",
|
69 |
+
icon: ArrowUpIcon,
|
70 |
+
},
|
71 |
+
]
|
src/app/data/mock.json
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[
|
2 |
+
{
|
3 |
+
"id": "TASK-8782",
|
4 |
+
"title": "You can't compress the program without quantifying the open-source SSD pixel!",
|
5 |
+
"status": "in progress",
|
6 |
+
"label": "documentation",
|
7 |
+
"priority": "medium"
|
8 |
+
},
|
9 |
+
{
|
10 |
+
"id": "TASK-7878",
|
11 |
+
"title": "Try to calculate the EXE feed, maybe it will index the multi-byte pixel!",
|
12 |
+
"status": "backlog",
|
13 |
+
"label": "documentation",
|
14 |
+
"priority": "medium"
|
15 |
+
}
|
16 |
+
]
|
src/app/data/schema.ts
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { z } from "zod"
|
2 |
+
|
3 |
+
// We're keeping a simple non-relational schema here.
|
4 |
+
// IRL, you will have a schema for your data models.
|
5 |
+
export const taskSchema = z.object({
|
6 |
+
id: z.string(),
|
7 |
+
title: z.string(),
|
8 |
+
status: z.string(),
|
9 |
+
label: z.string(),
|
10 |
+
priority: z.string(),
|
11 |
+
})
|
12 |
+
|
13 |
+
export type Task = z.infer<typeof taskSchema>
|
src/app/forlater/page.tsx
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import Head from "next/head"
|
2 |
+
|
3 |
+
import { Timeline } from "@/components/business/timeline"
|
4 |
+
|
5 |
+
export default function Index() {
|
6 |
+
return (
|
7 |
+
<div>
|
8 |
+
<Head>
|
9 |
+
<meta name="viewport" content="width=device-width, initial-scale=0.86, maximum-scale=5.0, minimum-scale=0.86" />
|
10 |
+
</Head>
|
11 |
+
<main className="h-screen w-full flex bg-gray-700 text-gray-200">
|
12 |
+
<div className="flex flex-col">
|
13 |
+
<Timeline />
|
14 |
+
</div>
|
15 |
+
</main>
|
16 |
+
</div>
|
17 |
+
)
|
18 |
+
}
|
src/app/main.tsx
ADDED
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { promises as fs } from "fs"
|
2 |
+
import path from "path"
|
3 |
+
import { Metadata } from "next"
|
4 |
+
import Image from "next/image"
|
5 |
+
import { z } from "zod"
|
6 |
+
|
7 |
+
import { taskSchema } from "./data/schema"
|
8 |
+
import { UserNav } from "@/components/business/tasks/user-nav"
|
9 |
+
import { DataTable } from "@/components/business/tasks/data-table"
|
10 |
+
import { columns } from "@/components/business/tasks/columns"
|
11 |
+
|
12 |
+
|
13 |
+
export const metadata: Metadata = {
|
14 |
+
title: "Tasks",
|
15 |
+
description: "A task and issue tracker build using Tanstack Table.",
|
16 |
+
}
|
17 |
+
|
18 |
+
// Simulate a database read for tasks.
|
19 |
+
async function getTasks() {
|
20 |
+
const data = await fs.readFile(
|
21 |
+
path.join(process.cwd(), "src/app/data/mock.json")
|
22 |
+
)
|
23 |
+
|
24 |
+
const tasks = JSON.parse(data.toString())
|
25 |
+
|
26 |
+
return z.array(taskSchema).parse(tasks)
|
27 |
+
}
|
28 |
+
|
29 |
+
export default async function Main() {
|
30 |
+
const tasks = await getTasks()
|
31 |
+
|
32 |
+
return (
|
33 |
+
<>
|
34 |
+
<div className="md:hidden">
|
35 |
+
<Image
|
36 |
+
src="/examples/tasks-light.png"
|
37 |
+
width={1280}
|
38 |
+
height={998}
|
39 |
+
alt="Playground"
|
40 |
+
className="block dark:hidden"
|
41 |
+
/>
|
42 |
+
<Image
|
43 |
+
src="/examples/tasks-dark.png"
|
44 |
+
width={1280}
|
45 |
+
height={998}
|
46 |
+
alt="Playground"
|
47 |
+
className="hidden dark:block"
|
48 |
+
/>
|
49 |
+
</div>
|
50 |
+
<div className="hidden h-full flex-1 flex-col space-y-8 p-8 md:flex">
|
51 |
+
<div className="flex items-center justify-between space-y-2">
|
52 |
+
<div>
|
53 |
+
<h2 className="text-2xl font-bold tracking-tight">Welcome back!</h2>
|
54 |
+
<p className="text-muted-foreground">
|
55 |
+
Here's a list of your tasks for this month!
|
56 |
+
</p>
|
57 |
+
</div>
|
58 |
+
<div className="flex items-center space-x-2">
|
59 |
+
<UserNav />
|
60 |
+
</div>
|
61 |
+
</div>
|
62 |
+
<DataTable data={tasks} columns={columns} />
|
63 |
+
</div>
|
64 |
+
</>
|
65 |
+
)
|
66 |
+
}
|
src/app/page.tsx
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
import Head from "next/head"
|
2 |
|
3 |
-
import
|
4 |
|
5 |
export default function Index() {
|
6 |
return (
|
@@ -10,7 +10,7 @@ export default function Index() {
|
|
10 |
</Head>
|
11 |
<main className="h-screen w-full flex bg-gray-700 text-gray-200">
|
12 |
<div className="flex flex-col">
|
13 |
-
<
|
14 |
</div>
|
15 |
</main>
|
16 |
</div>
|
|
|
1 |
import Head from "next/head"
|
2 |
|
3 |
+
import Main from "./main"
|
4 |
|
5 |
export default function Index() {
|
6 |
return (
|
|
|
10 |
</Head>
|
11 |
<main className="h-screen w-full flex bg-gray-700 text-gray-200">
|
12 |
<div className="flex flex-col">
|
13 |
+
<Main />
|
14 |
</div>
|
15 |
</main>
|
16 |
</div>
|
src/app/pending/page.tsx
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import Head from "next/head"
|
2 |
+
|
3 |
+
import { getPendingTasks } from "@/api"
|
4 |
+
|
5 |
+
|
6 |
+
export default async function Pending() {
|
7 |
+
const tasks = await getPendingTasks()
|
8 |
+
console.log(`tasks:`, tasks)
|
9 |
+
return (
|
10 |
+
<div>
|
11 |
+
<Head>
|
12 |
+
<meta name="viewport" content="width=device-width, initial-scale=0.86, maximum-scale=5.0, minimum-scale=0.86" />
|
13 |
+
</Head>
|
14 |
+
<main className="h-screen w-full flex bg-gray-700 text-gray-200">
|
15 |
+
<div className="flex flex-col">
|
16 |
+
Nb tasks: {tasks.length}
|
17 |
+
</div>
|
18 |
+
</main>
|
19 |
+
</div>
|
20 |
+
)
|
21 |
+
}
|
src/app/types.ts
CHANGED
@@ -1,63 +1,230 @@
|
|
1 |
-
export
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
}
|
21 |
|
22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
version: number
|
24 |
-
|
25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
}
|
27 |
|
|
|
28 |
|
29 |
-
export interface
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
|
34 |
// describe the background audio (crowd, birds, wind, sea etc..)
|
35 |
-
backgroundAudioPrompt
|
36 |
|
37 |
// describe the foreground audio (cars revving, footsteps, objects breaking, explosion etc)
|
38 |
-
foregroundAudioPrompt
|
39 |
|
40 |
// describe the main actor visible in the shot (optional)
|
41 |
-
actorPrompt
|
42 |
|
43 |
// describe the main actor voice (man, woman, old, young, amused, annoyed.. etc)
|
44 |
-
actorVoicePrompt
|
45 |
|
46 |
// describe the main actor dialogue line
|
47 |
-
actorDialoguePrompt
|
|
|
|
|
48 |
|
49 |
-
|
50 |
-
upscale?: boolean
|
51 |
|
52 |
-
|
53 |
-
steps?: number
|
54 |
|
55 |
-
fps
|
56 |
|
57 |
-
resolution
|
|
|
|
|
|
|
58 |
}
|
59 |
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export type VideoTransition =
|
2 |
+
| 'dissolve'
|
3 |
+
| 'bookflip'
|
4 |
+
| 'bounce'
|
5 |
+
| 'bowtiehorizontal'
|
6 |
+
| 'bowtievertical'
|
7 |
+
| 'bowtiewithparameter'
|
8 |
+
| 'butterflywavescrawler'
|
9 |
+
| 'circlecrop'
|
10 |
+
| 'colourdistance'
|
11 |
+
| 'crazyparametricfun'
|
12 |
+
| 'crosszoom'
|
13 |
+
| 'directional'
|
14 |
+
| 'directionalscaled'
|
15 |
+
| 'doomscreentransition'
|
16 |
+
| 'dreamy'
|
17 |
+
| 'dreamyzoom'
|
18 |
+
| 'edgetransition'
|
19 |
+
| 'filmburn'
|
20 |
+
| 'filmburnglitchdisplace'
|
21 |
+
| 'glitchmemories'
|
22 |
+
| 'gridflip'
|
23 |
+
| 'horizontalclose'
|
24 |
+
| 'horizontalopen'
|
25 |
+
| 'invertedpagecurl'
|
26 |
+
| 'leftright'
|
27 |
+
| 'linearblur'
|
28 |
+
| 'mosaic'
|
29 |
+
| 'overexposure'
|
30 |
+
| 'polkadotscurtain'
|
31 |
+
| 'radial'
|
32 |
+
| 'rectangle'
|
33 |
+
| 'rectanglecrop'
|
34 |
+
| 'rolls'
|
35 |
+
| 'rotatescalevanish'
|
36 |
+
| 'simplezoom'
|
37 |
+
| 'simplezoomout'
|
38 |
+
| 'slides'
|
39 |
+
| 'staticfade'
|
40 |
+
| 'stereoviewer'
|
41 |
+
| 'swirl'
|
42 |
+
| 'tvstatic'
|
43 |
+
| 'topbottom'
|
44 |
+
| 'verticalclose'
|
45 |
+
| 'verticalopen'
|
46 |
+
| 'waterdrop'
|
47 |
+
| 'waterdropzoomincircles'
|
48 |
+
| 'zoomleftwipe'
|
49 |
+
| 'zoomrigthwipe'
|
50 |
+
| 'angular'
|
51 |
+
| 'burn'
|
52 |
+
| 'cannabisleaf'
|
53 |
+
| 'circle'
|
54 |
+
| 'circleopen'
|
55 |
+
| 'colorphase'
|
56 |
+
| 'coordfromin'
|
57 |
+
| 'crosshatch'
|
58 |
+
| 'crosswarp'
|
59 |
+
| 'cube'
|
60 |
+
| 'directionaleasing'
|
61 |
+
| 'directionalwarp'
|
62 |
+
| 'directionalwipe'
|
63 |
+
| 'displacement'
|
64 |
+
| 'doorway'
|
65 |
+
| 'fade'
|
66 |
+
| 'fadecolor'
|
67 |
+
| 'fadegrayscale'
|
68 |
+
| 'flyeye'
|
69 |
+
| 'heart'
|
70 |
+
| 'hexagonalize'
|
71 |
+
| 'kaleidoscope'
|
72 |
+
| 'luma'
|
73 |
+
| 'luminance_melt'
|
74 |
+
| 'morph'
|
75 |
+
| 'mosaic_transition'
|
76 |
+
| 'multiply_blend'
|
77 |
+
| 'perlin'
|
78 |
+
| 'pinwheel'
|
79 |
+
| 'pixelize'
|
80 |
+
| 'polar_function'
|
81 |
+
| 'powerkaleido'
|
82 |
+
| 'randomnoisex'
|
83 |
+
| 'randomsquares'
|
84 |
+
| 'ripple'
|
85 |
+
| 'rotatetransition'
|
86 |
+
| 'rotate_scale_fade'
|
87 |
+
| 'scalein'
|
88 |
+
| 'squareswire'
|
89 |
+
| 'squeeze'
|
90 |
+
| 'static_wipe'
|
91 |
+
| 'swap'
|
92 |
+
| 'tangentmotionblur'
|
93 |
+
| 'undulatingburnout'
|
94 |
+
| 'wind'
|
95 |
+
| 'windowblinds'
|
96 |
+
| 'windowslice'
|
97 |
+
| 'wipedown'
|
98 |
+
| 'wipeleft'
|
99 |
+
| 'wiperight'
|
100 |
+
| 'wipeup'
|
101 |
+
| 'x_axistranslation'
|
102 |
|
103 |
+
|
104 |
+
export interface VideoShotMeta {
|
105 |
+
shotPrompt: string
|
106 |
+
// inputVideo?: string
|
107 |
+
|
108 |
+
// describe the background audio (crowd, birds, wind, sea etc..)
|
109 |
+
backgroundAudioPrompt: string
|
110 |
+
|
111 |
+
// describe the foreground audio (cars revving, footsteps, objects breaking, explosion etc)
|
112 |
+
foregroundAudioPrompt: string
|
113 |
+
|
114 |
+
// describe the main actor visible in the shot (optional)
|
115 |
+
actorPrompt: string
|
116 |
+
|
117 |
+
// describe the main actor voice (man, woman, old, young, amused, annoyed.. etc)
|
118 |
+
actorVoicePrompt: string
|
119 |
+
|
120 |
+
// describe the main actor dialogue line
|
121 |
+
actorDialoguePrompt: string
|
122 |
+
|
123 |
+
seed: number
|
124 |
+
noise: boolean // add movie noise
|
125 |
+
|
126 |
+
durationMs: number // in milliseconds
|
127 |
+
steps: number
|
128 |
+
|
129 |
+
fps: number // 8, 12, 24, 30, 60
|
130 |
+
|
131 |
+
resolution: string // {width}x{height} (256, 512, 576, 720, 1080)
|
132 |
+
|
133 |
+
introTransition: VideoTransition
|
134 |
+
introDurationMs: number // in milliseconds
|
135 |
}
|
136 |
|
137 |
+
|
138 |
+
export interface VideoShotData {
|
139 |
+
// must be unique
|
140 |
+
id: string
|
141 |
+
|
142 |
+
fileName: string
|
143 |
+
|
144 |
+
// used to check compatibility
|
145 |
version: number
|
146 |
+
|
147 |
+
// for internal use
|
148 |
+
hasGeneratedPreview: boolean
|
149 |
+
hasGeneratedVideo: boolean
|
150 |
+
hasUpscaledVideo: boolean
|
151 |
+
hasGeneratedBackgroundAudio: boolean
|
152 |
+
hasGeneratedForegroundAudio: boolean
|
153 |
+
hasGeneratedActor: boolean
|
154 |
+
hasInterpolatedVideo: boolean
|
155 |
+
hasAddedAudio: boolean
|
156 |
+
hasPostProcessedVideo: boolean
|
157 |
+
nbCompletedSteps: number
|
158 |
+
nbTotalSteps: number
|
159 |
+
progressPercent: number
|
160 |
+
completedAt: string
|
161 |
+
completed: boolean
|
162 |
+
error: string
|
163 |
}
|
164 |
|
165 |
+
export type VideoShot = VideoShotMeta & VideoShotData
|
166 |
|
167 |
+
export interface VideoSequenceMeta {
|
168 |
+
|
169 |
+
// describe the whole movie
|
170 |
+
videoPrompt: string
|
171 |
|
172 |
// describe the background audio (crowd, birds, wind, sea etc..)
|
173 |
+
backgroundAudioPrompt: string
|
174 |
|
175 |
// describe the foreground audio (cars revving, footsteps, objects breaking, explosion etc)
|
176 |
+
foregroundAudioPrompt: string
|
177 |
|
178 |
// describe the main actor visible in the shot (optional)
|
179 |
+
actorPrompt: string
|
180 |
|
181 |
// describe the main actor voice (man, woman, old, young, amused, annoyed.. etc)
|
182 |
+
actorVoicePrompt: string
|
183 |
|
184 |
// describe the main actor dialogue line
|
185 |
+
actorDialoguePrompt: string
|
186 |
+
|
187 |
+
seed: number
|
188 |
|
189 |
+
noise: boolean // add movie noise
|
|
|
190 |
|
191 |
+
steps: number // between 10 and 50
|
|
|
192 |
|
193 |
+
fps: number // 8, 12, 24, 30, 60
|
194 |
|
195 |
+
resolution: string // 256, 512, 576, 720, 1080
|
196 |
+
|
197 |
+
outroTransition: VideoTransition
|
198 |
+
outroDurationMs: number
|
199 |
}
|
200 |
|
201 |
+
|
202 |
+
export interface VideoSequenceData {
|
203 |
+
// must be unique
|
204 |
+
id: string
|
205 |
+
|
206 |
+
fileName: string
|
207 |
+
|
208 |
+
// used to check compatibility
|
209 |
+
version: number
|
210 |
+
|
211 |
+
hasAssembledVideo: boolean
|
212 |
+
nbCompletedShots: number
|
213 |
+
nbTotalShots: number
|
214 |
+
progressPercent: number
|
215 |
+
completedAt: string
|
216 |
+
completed: boolean
|
217 |
+
error: string
|
218 |
+
}
|
219 |
+
|
220 |
+
export type VideoSequence = VideoSequenceMeta & VideoSequenceData
|
221 |
+
|
222 |
+
export type VideoSequenceRequest = {
|
223 |
+
token: string
|
224 |
+
sequence: VideoSequenceMeta
|
225 |
+
shots: VideoShotMeta[]
|
226 |
+
}
|
227 |
+
|
228 |
+
export type VideoTask = VideoSequence & {
|
229 |
+
shots: VideoShot[]
|
230 |
+
}
|
src/components/business/tasks/columns.tsx
ADDED
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { ColumnDef } from "@tanstack/react-table"
|
4 |
+
|
5 |
+
import { Badge } from "@/components/ui/badge"
|
6 |
+
import { Checkbox } from "@/components/ui/checkbox"
|
7 |
+
|
8 |
+
import { labels, priorities, statuses } from "@/app/data/data"
|
9 |
+
|
10 |
+
import { DataTableColumnHeader } from "./data-table-column-header"
|
11 |
+
import { DataTableRowActions } from "./data-table-row-actions"
|
12 |
+
import { Task } from "@/app/data/schema"
|
13 |
+
|
14 |
+
export const columns: ColumnDef<Task>[] = [
|
15 |
+
{
|
16 |
+
id: "select",
|
17 |
+
header: ({ table }) => (
|
18 |
+
<Checkbox
|
19 |
+
checked={table.getIsAllPageRowsSelected()}
|
20 |
+
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
21 |
+
aria-label="Select all"
|
22 |
+
className="translate-y-[2px]"
|
23 |
+
/>
|
24 |
+
),
|
25 |
+
cell: ({ row }) => (
|
26 |
+
<Checkbox
|
27 |
+
checked={row.getIsSelected()}
|
28 |
+
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
29 |
+
aria-label="Select row"
|
30 |
+
className="translate-y-[2px]"
|
31 |
+
/>
|
32 |
+
),
|
33 |
+
enableSorting: false,
|
34 |
+
enableHiding: false,
|
35 |
+
},
|
36 |
+
{
|
37 |
+
accessorKey: "id",
|
38 |
+
header: ({ column }) => (
|
39 |
+
<DataTableColumnHeader column={column} title="Task" />
|
40 |
+
),
|
41 |
+
cell: ({ row }) => <div className="w-[80px]">{row.getValue("id")}</div>,
|
42 |
+
enableSorting: false,
|
43 |
+
enableHiding: false,
|
44 |
+
},
|
45 |
+
{
|
46 |
+
accessorKey: "title",
|
47 |
+
header: ({ column }) => (
|
48 |
+
<DataTableColumnHeader column={column} title="Title" />
|
49 |
+
),
|
50 |
+
cell: ({ row }) => {
|
51 |
+
const label = labels.find((label) => label.value === row.original.label)
|
52 |
+
|
53 |
+
return (
|
54 |
+
<div className="flex space-x-2">
|
55 |
+
{label && <Badge variant="outline">{label.label}</Badge>}
|
56 |
+
<span className="max-w-[500px] truncate font-medium">
|
57 |
+
{row.getValue("title")}
|
58 |
+
</span>
|
59 |
+
</div>
|
60 |
+
)
|
61 |
+
},
|
62 |
+
},
|
63 |
+
{
|
64 |
+
accessorKey: "status",
|
65 |
+
header: ({ column }) => (
|
66 |
+
<DataTableColumnHeader column={column} title="Status" />
|
67 |
+
),
|
68 |
+
cell: ({ row }) => {
|
69 |
+
const status = statuses.find(
|
70 |
+
(status) => status.value === row.getValue("status")
|
71 |
+
)
|
72 |
+
|
73 |
+
if (!status) {
|
74 |
+
return null
|
75 |
+
}
|
76 |
+
|
77 |
+
return (
|
78 |
+
<div className="flex w-[100px] items-center">
|
79 |
+
{status.icon && (
|
80 |
+
<status.icon className="mr-2 h-4 w-4 text-muted-foreground" />
|
81 |
+
)}
|
82 |
+
<span>{status.label}</span>
|
83 |
+
</div>
|
84 |
+
)
|
85 |
+
},
|
86 |
+
filterFn: (row, id, value) => {
|
87 |
+
return value.includes(row.getValue(id))
|
88 |
+
},
|
89 |
+
},
|
90 |
+
{
|
91 |
+
accessorKey: "priority",
|
92 |
+
header: ({ column }) => (
|
93 |
+
<DataTableColumnHeader column={column} title="Priority" />
|
94 |
+
),
|
95 |
+
cell: ({ row }) => {
|
96 |
+
const priority = priorities.find(
|
97 |
+
(priority) => priority.value === row.getValue("priority")
|
98 |
+
)
|
99 |
+
|
100 |
+
if (!priority) {
|
101 |
+
return null
|
102 |
+
}
|
103 |
+
|
104 |
+
return (
|
105 |
+
<div className="flex items-center">
|
106 |
+
{priority.icon && (
|
107 |
+
<priority.icon className="mr-2 h-4 w-4 text-muted-foreground" />
|
108 |
+
)}
|
109 |
+
<span>{priority.label}</span>
|
110 |
+
</div>
|
111 |
+
)
|
112 |
+
},
|
113 |
+
filterFn: (row, id, value) => {
|
114 |
+
return value.includes(row.getValue(id))
|
115 |
+
},
|
116 |
+
},
|
117 |
+
{
|
118 |
+
id: "actions",
|
119 |
+
cell: ({ row }) => <DataTableRowActions row={row} />,
|
120 |
+
},
|
121 |
+
]
|
src/components/business/tasks/data-table-column-header.tsx
ADDED
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {
|
2 |
+
ArrowDownIcon,
|
3 |
+
ArrowUpIcon,
|
4 |
+
CaretSortIcon,
|
5 |
+
EyeNoneIcon,
|
6 |
+
} from "@radix-ui/react-icons"
|
7 |
+
import { Column } from "@tanstack/react-table"
|
8 |
+
|
9 |
+
import { cn } from "@/lib/utils"
|
10 |
+
import { Button } from "@/components/ui/button"
|
11 |
+
import {
|
12 |
+
DropdownMenu,
|
13 |
+
DropdownMenuContent,
|
14 |
+
DropdownMenuItem,
|
15 |
+
DropdownMenuSeparator,
|
16 |
+
DropdownMenuTrigger,
|
17 |
+
} from "@/components/ui/dropdown-menu"
|
18 |
+
|
19 |
+
interface DataTableColumnHeaderProps<TData, TValue>
|
20 |
+
extends React.HTMLAttributes<HTMLDivElement> {
|
21 |
+
column: Column<TData, TValue>
|
22 |
+
title: string
|
23 |
+
}
|
24 |
+
|
25 |
+
export function DataTableColumnHeader<TData, TValue>({
|
26 |
+
column,
|
27 |
+
title,
|
28 |
+
className,
|
29 |
+
}: DataTableColumnHeaderProps<TData, TValue>) {
|
30 |
+
if (!column.getCanSort()) {
|
31 |
+
return <div className={cn(className)}>{title}</div>
|
32 |
+
}
|
33 |
+
|
34 |
+
return (
|
35 |
+
<div className={cn("flex items-center space-x-2", className)}>
|
36 |
+
<DropdownMenu>
|
37 |
+
<DropdownMenuTrigger asChild>
|
38 |
+
<Button
|
39 |
+
variant="ghost"
|
40 |
+
size="sm"
|
41 |
+
className="-ml-3 h-8 data-[state=open]:bg-accent"
|
42 |
+
>
|
43 |
+
<span>{title}</span>
|
44 |
+
{column.getIsSorted() === "desc" ? (
|
45 |
+
<ArrowDownIcon className="ml-2 h-4 w-4" />
|
46 |
+
) : column.getIsSorted() === "asc" ? (
|
47 |
+
<ArrowUpIcon className="ml-2 h-4 w-4" />
|
48 |
+
) : (
|
49 |
+
<CaretSortIcon className="ml-2 h-4 w-4" />
|
50 |
+
)}
|
51 |
+
</Button>
|
52 |
+
</DropdownMenuTrigger>
|
53 |
+
<DropdownMenuContent align="start">
|
54 |
+
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
|
55 |
+
<ArrowUpIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
|
56 |
+
Asc
|
57 |
+
</DropdownMenuItem>
|
58 |
+
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
|
59 |
+
<ArrowDownIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
|
60 |
+
Desc
|
61 |
+
</DropdownMenuItem>
|
62 |
+
<DropdownMenuSeparator />
|
63 |
+
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
|
64 |
+
<EyeNoneIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
|
65 |
+
Hide
|
66 |
+
</DropdownMenuItem>
|
67 |
+
</DropdownMenuContent>
|
68 |
+
</DropdownMenu>
|
69 |
+
</div>
|
70 |
+
)
|
71 |
+
}
|
src/components/business/tasks/data-table-faceted-filter.tsx
ADDED
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from "react"
|
2 |
+
import { CheckIcon, PlusCircledIcon } from "@radix-ui/react-icons"
|
3 |
+
import { Column } from "@tanstack/react-table"
|
4 |
+
|
5 |
+
import { cn } from "@/lib/utils"
|
6 |
+
import { Badge } from "@/components/ui/badge"
|
7 |
+
import { Button } from "@/components/ui/button"
|
8 |
+
import {
|
9 |
+
Command,
|
10 |
+
CommandEmpty,
|
11 |
+
CommandGroup,
|
12 |
+
CommandInput,
|
13 |
+
CommandItem,
|
14 |
+
CommandList,
|
15 |
+
CommandSeparator,
|
16 |
+
} from "@/components/ui/command"
|
17 |
+
import {
|
18 |
+
Popover,
|
19 |
+
PopoverContent,
|
20 |
+
PopoverTrigger,
|
21 |
+
} from "@/components/ui/popover"
|
22 |
+
import { Separator } from "@/components/ui/separator"
|
23 |
+
|
24 |
+
interface DataTableFacetedFilter<TData, TValue> {
|
25 |
+
column?: Column<TData, TValue>
|
26 |
+
title?: string
|
27 |
+
options: {
|
28 |
+
label: string
|
29 |
+
value: string
|
30 |
+
icon?: React.ComponentType<{ className?: string }>
|
31 |
+
}[]
|
32 |
+
}
|
33 |
+
|
34 |
+
export function DataTableFacetedFilter<TData, TValue>({
|
35 |
+
column,
|
36 |
+
title,
|
37 |
+
options,
|
38 |
+
}: DataTableFacetedFilter<TData, TValue>) {
|
39 |
+
const facets = column?.getFacetedUniqueValues()
|
40 |
+
const selectedValues = new Set(column?.getFilterValue() as string[])
|
41 |
+
|
42 |
+
return (
|
43 |
+
<Popover>
|
44 |
+
<PopoverTrigger asChild>
|
45 |
+
<Button variant="outline" size="sm" className="h-8 border-dashed">
|
46 |
+
<PlusCircledIcon className="mr-2 h-4 w-4" />
|
47 |
+
{title}
|
48 |
+
{selectedValues?.size > 0 && (
|
49 |
+
<>
|
50 |
+
<Separator orientation="vertical" className="mx-2 h-4" />
|
51 |
+
<Badge
|
52 |
+
variant="secondary"
|
53 |
+
className="rounded-sm px-1 font-normal lg:hidden"
|
54 |
+
>
|
55 |
+
{selectedValues.size}
|
56 |
+
</Badge>
|
57 |
+
<div className="hidden space-x-1 lg:flex">
|
58 |
+
{selectedValues.size > 2 ? (
|
59 |
+
<Badge
|
60 |
+
variant="secondary"
|
61 |
+
className="rounded-sm px-1 font-normal"
|
62 |
+
>
|
63 |
+
{selectedValues.size} selected
|
64 |
+
</Badge>
|
65 |
+
) : (
|
66 |
+
options
|
67 |
+
.filter((option) => selectedValues.has(option.value))
|
68 |
+
.map((option) => (
|
69 |
+
<Badge
|
70 |
+
variant="secondary"
|
71 |
+
key={option.value}
|
72 |
+
className="rounded-sm px-1 font-normal"
|
73 |
+
>
|
74 |
+
{option.label}
|
75 |
+
</Badge>
|
76 |
+
))
|
77 |
+
)}
|
78 |
+
</div>
|
79 |
+
</>
|
80 |
+
)}
|
81 |
+
</Button>
|
82 |
+
</PopoverTrigger>
|
83 |
+
<PopoverContent className="w-[200px] p-0" align="start">
|
84 |
+
<Command>
|
85 |
+
<CommandInput placeholder={title} />
|
86 |
+
<CommandList>
|
87 |
+
<CommandEmpty>No results found.</CommandEmpty>
|
88 |
+
<CommandGroup>
|
89 |
+
{options.map((option) => {
|
90 |
+
const isSelected = selectedValues.has(option.value)
|
91 |
+
return (
|
92 |
+
<CommandItem
|
93 |
+
key={option.value}
|
94 |
+
onSelect={() => {
|
95 |
+
if (isSelected) {
|
96 |
+
selectedValues.delete(option.value)
|
97 |
+
} else {
|
98 |
+
selectedValues.add(option.value)
|
99 |
+
}
|
100 |
+
const filterValues = Array.from(selectedValues)
|
101 |
+
column?.setFilterValue(
|
102 |
+
filterValues.length ? filterValues : undefined
|
103 |
+
)
|
104 |
+
}}
|
105 |
+
>
|
106 |
+
<div
|
107 |
+
className={cn(
|
108 |
+
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
109 |
+
isSelected
|
110 |
+
? "bg-primary text-primary-foreground"
|
111 |
+
: "opacity-50 [&_svg]:invisible"
|
112 |
+
)}
|
113 |
+
>
|
114 |
+
<CheckIcon className={cn("h-4 w-4")} />
|
115 |
+
</div>
|
116 |
+
{option.icon && (
|
117 |
+
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
|
118 |
+
)}
|
119 |
+
<span>{option.label}</span>
|
120 |
+
{facets?.get(option.value) && (
|
121 |
+
<span className="ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs">
|
122 |
+
{facets.get(option.value)}
|
123 |
+
</span>
|
124 |
+
)}
|
125 |
+
</CommandItem>
|
126 |
+
)
|
127 |
+
})}
|
128 |
+
</CommandGroup>
|
129 |
+
{selectedValues.size > 0 && (
|
130 |
+
<>
|
131 |
+
<CommandSeparator />
|
132 |
+
<CommandGroup>
|
133 |
+
<CommandItem
|
134 |
+
onSelect={() => column?.setFilterValue(undefined)}
|
135 |
+
className="justify-center text-center"
|
136 |
+
>
|
137 |
+
Clear filters
|
138 |
+
</CommandItem>
|
139 |
+
</CommandGroup>
|
140 |
+
</>
|
141 |
+
)}
|
142 |
+
</CommandList>
|
143 |
+
</Command>
|
144 |
+
</PopoverContent>
|
145 |
+
</Popover>
|
146 |
+
)
|
147 |
+
}
|
src/components/business/tasks/data-table-pagination.tsx
ADDED
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {
|
2 |
+
ChevronLeftIcon,
|
3 |
+
ChevronRightIcon,
|
4 |
+
DoubleArrowLeftIcon,
|
5 |
+
DoubleArrowRightIcon,
|
6 |
+
} from "@radix-ui/react-icons"
|
7 |
+
import { Table } from "@tanstack/react-table"
|
8 |
+
|
9 |
+
import { Button } from "@/components/ui/button"
|
10 |
+
import {
|
11 |
+
Select,
|
12 |
+
SelectContent,
|
13 |
+
SelectItem,
|
14 |
+
SelectTrigger,
|
15 |
+
SelectValue,
|
16 |
+
} from "@/components/ui/select"
|
17 |
+
|
18 |
+
interface DataTablePaginationProps<TData> {
|
19 |
+
table: Table<TData>
|
20 |
+
}
|
21 |
+
|
22 |
+
export function DataTablePagination<TData>({
|
23 |
+
table,
|
24 |
+
}: DataTablePaginationProps<TData>) {
|
25 |
+
return (
|
26 |
+
<div className="flex items-center justify-between px-2">
|
27 |
+
<div className="flex-1 text-sm text-muted-foreground">
|
28 |
+
{table.getFilteredSelectedRowModel().rows.length} of{" "}
|
29 |
+
{table.getFilteredRowModel().rows.length} row(s) selected.
|
30 |
+
</div>
|
31 |
+
<div className="flex items-center space-x-6 lg:space-x-8">
|
32 |
+
<div className="flex items-center space-x-2">
|
33 |
+
<p className="text-sm font-medium">Rows per page</p>
|
34 |
+
<Select
|
35 |
+
value={`${table.getState().pagination.pageSize}`}
|
36 |
+
onValueChange={(value) => {
|
37 |
+
table.setPageSize(Number(value))
|
38 |
+
}}
|
39 |
+
>
|
40 |
+
<SelectTrigger className="h-8 w-[70px]">
|
41 |
+
<SelectValue placeholder={table.getState().pagination.pageSize} />
|
42 |
+
</SelectTrigger>
|
43 |
+
<SelectContent side="top">
|
44 |
+
{[10, 20, 30, 40, 50].map((pageSize) => (
|
45 |
+
<SelectItem key={pageSize} value={`${pageSize}`}>
|
46 |
+
{pageSize}
|
47 |
+
</SelectItem>
|
48 |
+
))}
|
49 |
+
</SelectContent>
|
50 |
+
</Select>
|
51 |
+
</div>
|
52 |
+
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
|
53 |
+
Page {table.getState().pagination.pageIndex + 1} of{" "}
|
54 |
+
{table.getPageCount()}
|
55 |
+
</div>
|
56 |
+
<div className="flex items-center space-x-2">
|
57 |
+
<Button
|
58 |
+
variant="outline"
|
59 |
+
className="hidden h-8 w-8 p-0 lg:flex"
|
60 |
+
onClick={() => table.setPageIndex(0)}
|
61 |
+
disabled={!table.getCanPreviousPage()}
|
62 |
+
>
|
63 |
+
<span className="sr-only">Go to first page</span>
|
64 |
+
<DoubleArrowLeftIcon className="h-4 w-4" />
|
65 |
+
</Button>
|
66 |
+
<Button
|
67 |
+
variant="outline"
|
68 |
+
className="h-8 w-8 p-0"
|
69 |
+
onClick={() => table.previousPage()}
|
70 |
+
disabled={!table.getCanPreviousPage()}
|
71 |
+
>
|
72 |
+
<span className="sr-only">Go to previous page</span>
|
73 |
+
<ChevronLeftIcon className="h-4 w-4" />
|
74 |
+
</Button>
|
75 |
+
<Button
|
76 |
+
variant="outline"
|
77 |
+
className="h-8 w-8 p-0"
|
78 |
+
onClick={() => table.nextPage()}
|
79 |
+
disabled={!table.getCanNextPage()}
|
80 |
+
>
|
81 |
+
<span className="sr-only">Go to next page</span>
|
82 |
+
<ChevronRightIcon className="h-4 w-4" />
|
83 |
+
</Button>
|
84 |
+
<Button
|
85 |
+
variant="outline"
|
86 |
+
className="hidden h-8 w-8 p-0 lg:flex"
|
87 |
+
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
88 |
+
disabled={!table.getCanNextPage()}
|
89 |
+
>
|
90 |
+
<span className="sr-only">Go to last page</span>
|
91 |
+
<DoubleArrowRightIcon className="h-4 w-4" />
|
92 |
+
</Button>
|
93 |
+
</div>
|
94 |
+
</div>
|
95 |
+
</div>
|
96 |
+
)
|
97 |
+
}
|
src/components/business/tasks/data-table-row-actions.tsx
ADDED
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { DotsHorizontalIcon } from "@radix-ui/react-icons"
|
4 |
+
import { Row } from "@tanstack/react-table"
|
5 |
+
|
6 |
+
import { Button } from "@/components/ui/button"
|
7 |
+
import {
|
8 |
+
DropdownMenu,
|
9 |
+
DropdownMenuContent,
|
10 |
+
DropdownMenuItem,
|
11 |
+
DropdownMenuRadioGroup,
|
12 |
+
DropdownMenuRadioItem,
|
13 |
+
DropdownMenuSeparator,
|
14 |
+
DropdownMenuShortcut,
|
15 |
+
DropdownMenuSub,
|
16 |
+
DropdownMenuSubContent,
|
17 |
+
DropdownMenuSubTrigger,
|
18 |
+
DropdownMenuTrigger,
|
19 |
+
} from "@/components/ui/dropdown-menu"
|
20 |
+
|
21 |
+
import { labels } from "@/app/data/data"
|
22 |
+
import { taskSchema } from "@/app/data/schema"
|
23 |
+
|
24 |
+
interface DataTableRowActionsProps<TData> {
|
25 |
+
row: Row<TData>
|
26 |
+
}
|
27 |
+
|
28 |
+
export function DataTableRowActions<TData>({
|
29 |
+
row,
|
30 |
+
}: DataTableRowActionsProps<TData>) {
|
31 |
+
const task = taskSchema.parse(row.original)
|
32 |
+
|
33 |
+
return (
|
34 |
+
<DropdownMenu>
|
35 |
+
<DropdownMenuTrigger asChild>
|
36 |
+
<Button
|
37 |
+
variant="ghost"
|
38 |
+
className="flex h-8 w-8 p-0 data-[state=open]:bg-muted"
|
39 |
+
>
|
40 |
+
<DotsHorizontalIcon className="h-4 w-4" />
|
41 |
+
<span className="sr-only">Open menu</span>
|
42 |
+
</Button>
|
43 |
+
</DropdownMenuTrigger>
|
44 |
+
<DropdownMenuContent align="end" className="w-[160px]">
|
45 |
+
<DropdownMenuItem>Edit</DropdownMenuItem>
|
46 |
+
<DropdownMenuItem>Make a copy</DropdownMenuItem>
|
47 |
+
<DropdownMenuItem>Favorite</DropdownMenuItem>
|
48 |
+
<DropdownMenuSeparator />
|
49 |
+
<DropdownMenuSub>
|
50 |
+
<DropdownMenuSubTrigger>Labels</DropdownMenuSubTrigger>
|
51 |
+
<DropdownMenuSubContent>
|
52 |
+
<DropdownMenuRadioGroup value={task.label}>
|
53 |
+
{labels.map((label) => (
|
54 |
+
<DropdownMenuRadioItem key={label.value} value={label.value}>
|
55 |
+
{label.label}
|
56 |
+
</DropdownMenuRadioItem>
|
57 |
+
))}
|
58 |
+
</DropdownMenuRadioGroup>
|
59 |
+
</DropdownMenuSubContent>
|
60 |
+
</DropdownMenuSub>
|
61 |
+
<DropdownMenuSeparator />
|
62 |
+
<DropdownMenuItem>
|
63 |
+
Delete
|
64 |
+
<DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut>
|
65 |
+
</DropdownMenuItem>
|
66 |
+
</DropdownMenuContent>
|
67 |
+
</DropdownMenu>
|
68 |
+
)
|
69 |
+
}
|
src/components/business/tasks/data-table-toolbar.tsx
ADDED
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { Cross2Icon } from "@radix-ui/react-icons"
|
4 |
+
import { Table } from "@tanstack/react-table"
|
5 |
+
|
6 |
+
import { Button } from "@/components/ui/button"
|
7 |
+
import { Input } from "@/components/ui/input"
|
8 |
+
import { DataTableViewOptions } from "@/components/business/tasks/data-table-view-options"
|
9 |
+
|
10 |
+
import { DataTableFacetedFilter } from "./data-table-faceted-filter"
|
11 |
+
import { priorities, statuses } from "@/app/data/data"
|
12 |
+
|
13 |
+
interface DataTableToolbarProps<TData> {
|
14 |
+
table: Table<TData>
|
15 |
+
}
|
16 |
+
|
17 |
+
export function DataTableToolbar<TData>({
|
18 |
+
table,
|
19 |
+
}: DataTableToolbarProps<TData>) {
|
20 |
+
const isFiltered = table.getState().columnFilters.length > 0
|
21 |
+
|
22 |
+
return (
|
23 |
+
<div className="flex items-center justify-between">
|
24 |
+
<div className="flex flex-1 items-center space-x-2">
|
25 |
+
<Input
|
26 |
+
placeholder="Filter tasks..."
|
27 |
+
value={(table.getColumn("title")?.getFilterValue() as string) ?? ""}
|
28 |
+
onChange={(event) =>
|
29 |
+
table.getColumn("title")?.setFilterValue(event.target.value)
|
30 |
+
}
|
31 |
+
className="h-8 w-[150px] lg:w-[250px]"
|
32 |
+
/>
|
33 |
+
{table.getColumn("status") && (
|
34 |
+
<DataTableFacetedFilter
|
35 |
+
column={table.getColumn("status")}
|
36 |
+
title="Status"
|
37 |
+
options={statuses}
|
38 |
+
/>
|
39 |
+
)}
|
40 |
+
{table.getColumn("priority") && (
|
41 |
+
<DataTableFacetedFilter
|
42 |
+
column={table.getColumn("priority")}
|
43 |
+
title="Priority"
|
44 |
+
options={priorities}
|
45 |
+
/>
|
46 |
+
)}
|
47 |
+
{isFiltered && (
|
48 |
+
<Button
|
49 |
+
variant="ghost"
|
50 |
+
onClick={() => table.resetColumnFilters()}
|
51 |
+
className="h-8 px-2 lg:px-3"
|
52 |
+
>
|
53 |
+
Reset
|
54 |
+
<Cross2Icon className="ml-2 h-4 w-4" />
|
55 |
+
</Button>
|
56 |
+
)}
|
57 |
+
</div>
|
58 |
+
<DataTableViewOptions table={table} />
|
59 |
+
</div>
|
60 |
+
)
|
61 |
+
}
|
src/components/business/tasks/data-table-view-options.tsx
ADDED
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"
|
4 |
+
import { MixerHorizontalIcon } from "@radix-ui/react-icons"
|
5 |
+
import { Table } from "@tanstack/react-table"
|
6 |
+
|
7 |
+
import { Button } from "@/components/ui/button"
|
8 |
+
import {
|
9 |
+
DropdownMenu,
|
10 |
+
DropdownMenuCheckboxItem,
|
11 |
+
DropdownMenuContent,
|
12 |
+
DropdownMenuLabel,
|
13 |
+
DropdownMenuSeparator,
|
14 |
+
} from "@/components/ui/dropdown-menu"
|
15 |
+
|
16 |
+
interface DataTableViewOptionsProps<TData> {
|
17 |
+
table: Table<TData>
|
18 |
+
}
|
19 |
+
|
20 |
+
export function DataTableViewOptions<TData>({
|
21 |
+
table,
|
22 |
+
}: DataTableViewOptionsProps<TData>) {
|
23 |
+
return (
|
24 |
+
<DropdownMenu>
|
25 |
+
<DropdownMenuTrigger asChild>
|
26 |
+
<Button
|
27 |
+
variant="outline"
|
28 |
+
size="sm"
|
29 |
+
className="ml-auto hidden h-8 lg:flex"
|
30 |
+
>
|
31 |
+
<MixerHorizontalIcon className="mr-2 h-4 w-4" />
|
32 |
+
View
|
33 |
+
</Button>
|
34 |
+
</DropdownMenuTrigger>
|
35 |
+
<DropdownMenuContent align="end" className="w-[150px]">
|
36 |
+
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
|
37 |
+
<DropdownMenuSeparator />
|
38 |
+
{table
|
39 |
+
.getAllColumns()
|
40 |
+
.filter(
|
41 |
+
(column) =>
|
42 |
+
typeof column.accessorFn !== "undefined" && column.getCanHide()
|
43 |
+
)
|
44 |
+
.map((column) => {
|
45 |
+
return (
|
46 |
+
<DropdownMenuCheckboxItem
|
47 |
+
key={column.id}
|
48 |
+
className="capitalize"
|
49 |
+
checked={column.getIsVisible()}
|
50 |
+
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
51 |
+
>
|
52 |
+
{column.id}
|
53 |
+
</DropdownMenuCheckboxItem>
|
54 |
+
)
|
55 |
+
})}
|
56 |
+
</DropdownMenuContent>
|
57 |
+
</DropdownMenu>
|
58 |
+
)
|
59 |
+
}
|
src/components/business/tasks/data-table.tsx
ADDED
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import {
|
5 |
+
ColumnDef,
|
6 |
+
ColumnFiltersState,
|
7 |
+
SortingState,
|
8 |
+
VisibilityState,
|
9 |
+
flexRender,
|
10 |
+
getCoreRowModel,
|
11 |
+
getFacetedRowModel,
|
12 |
+
getFacetedUniqueValues,
|
13 |
+
getFilteredRowModel,
|
14 |
+
getPaginationRowModel,
|
15 |
+
getSortedRowModel,
|
16 |
+
useReactTable,
|
17 |
+
} from "@tanstack/react-table"
|
18 |
+
|
19 |
+
import {
|
20 |
+
Table,
|
21 |
+
TableBody,
|
22 |
+
TableCell,
|
23 |
+
TableHead,
|
24 |
+
TableHeader,
|
25 |
+
TableRow,
|
26 |
+
} from "@/components/ui/table"
|
27 |
+
|
28 |
+
import { DataTablePagination } from "@/components/business/tasks/data-table-pagination"
|
29 |
+
import { DataTableToolbar } from "@/components/business/tasks/data-table-toolbar"
|
30 |
+
|
31 |
+
interface DataTableProps<TData, TValue> {
|
32 |
+
columns: ColumnDef<TData, TValue>[]
|
33 |
+
data: TData[]
|
34 |
+
}
|
35 |
+
|
36 |
+
export function DataTable<TData, TValue>({
|
37 |
+
columns,
|
38 |
+
data,
|
39 |
+
}: DataTableProps<TData, TValue>) {
|
40 |
+
const [rowSelection, setRowSelection] = React.useState({})
|
41 |
+
const [columnVisibility, setColumnVisibility] =
|
42 |
+
React.useState<VisibilityState>({})
|
43 |
+
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
44 |
+
[]
|
45 |
+
)
|
46 |
+
const [sorting, setSorting] = React.useState<SortingState>([])
|
47 |
+
|
48 |
+
const table = useReactTable({
|
49 |
+
data,
|
50 |
+
columns,
|
51 |
+
state: {
|
52 |
+
sorting,
|
53 |
+
columnVisibility,
|
54 |
+
rowSelection,
|
55 |
+
columnFilters,
|
56 |
+
},
|
57 |
+
enableRowSelection: true,
|
58 |
+
onRowSelectionChange: setRowSelection,
|
59 |
+
onSortingChange: setSorting,
|
60 |
+
onColumnFiltersChange: setColumnFilters,
|
61 |
+
onColumnVisibilityChange: setColumnVisibility,
|
62 |
+
getCoreRowModel: getCoreRowModel(),
|
63 |
+
getFilteredRowModel: getFilteredRowModel(),
|
64 |
+
getPaginationRowModel: getPaginationRowModel(),
|
65 |
+
getSortedRowModel: getSortedRowModel(),
|
66 |
+
getFacetedRowModel: getFacetedRowModel(),
|
67 |
+
getFacetedUniqueValues: getFacetedUniqueValues(),
|
68 |
+
})
|
69 |
+
|
70 |
+
return (
|
71 |
+
<div className="space-y-4">
|
72 |
+
<DataTableToolbar table={table} />
|
73 |
+
<div className="rounded-md border">
|
74 |
+
<Table>
|
75 |
+
<TableHeader>
|
76 |
+
{table.getHeaderGroups().map((headerGroup) => (
|
77 |
+
<TableRow key={headerGroup.id}>
|
78 |
+
{headerGroup.headers.map((header) => {
|
79 |
+
return (
|
80 |
+
<TableHead key={header.id}>
|
81 |
+
{header.isPlaceholder
|
82 |
+
? null
|
83 |
+
: flexRender(
|
84 |
+
header.column.columnDef.header,
|
85 |
+
header.getContext()
|
86 |
+
)}
|
87 |
+
</TableHead>
|
88 |
+
)
|
89 |
+
})}
|
90 |
+
</TableRow>
|
91 |
+
))}
|
92 |
+
</TableHeader>
|
93 |
+
<TableBody>
|
94 |
+
{table.getRowModel().rows?.length ? (
|
95 |
+
table.getRowModel().rows.map((row) => (
|
96 |
+
<TableRow
|
97 |
+
key={row.id}
|
98 |
+
data-state={row.getIsSelected() && "selected"}
|
99 |
+
>
|
100 |
+
{row.getVisibleCells().map((cell) => (
|
101 |
+
<TableCell key={cell.id}>
|
102 |
+
{flexRender(
|
103 |
+
cell.column.columnDef.cell,
|
104 |
+
cell.getContext()
|
105 |
+
)}
|
106 |
+
</TableCell>
|
107 |
+
))}
|
108 |
+
</TableRow>
|
109 |
+
))
|
110 |
+
) : (
|
111 |
+
<TableRow>
|
112 |
+
<TableCell
|
113 |
+
colSpan={columns.length}
|
114 |
+
className="h-24 text-center"
|
115 |
+
>
|
116 |
+
No results.
|
117 |
+
</TableCell>
|
118 |
+
</TableRow>
|
119 |
+
)}
|
120 |
+
</TableBody>
|
121 |
+
</Table>
|
122 |
+
</div>
|
123 |
+
<DataTablePagination table={table} />
|
124 |
+
</div>
|
125 |
+
)
|
126 |
+
}
|
src/components/business/tasks/user-nav.tsx
ADDED
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {
|
2 |
+
Avatar,
|
3 |
+
AvatarFallback,
|
4 |
+
AvatarImage,
|
5 |
+
} from "@/components/ui/avatar"
|
6 |
+
import { Button } from "@/components/ui/button"
|
7 |
+
import {
|
8 |
+
DropdownMenu,
|
9 |
+
DropdownMenuContent,
|
10 |
+
DropdownMenuGroup,
|
11 |
+
DropdownMenuItem,
|
12 |
+
DropdownMenuLabel,
|
13 |
+
DropdownMenuSeparator,
|
14 |
+
DropdownMenuShortcut,
|
15 |
+
DropdownMenuTrigger,
|
16 |
+
} from "@/components/ui/dropdown-menu"
|
17 |
+
|
18 |
+
export function UserNav() {
|
19 |
+
return (
|
20 |
+
<DropdownMenu>
|
21 |
+
<DropdownMenuTrigger asChild>
|
22 |
+
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
|
23 |
+
<Avatar className="h-9 w-9">
|
24 |
+
<AvatarImage src="/avatars/03.png" alt="@shadcn" />
|
25 |
+
<AvatarFallback>SC</AvatarFallback>
|
26 |
+
</Avatar>
|
27 |
+
</Button>
|
28 |
+
</DropdownMenuTrigger>
|
29 |
+
<DropdownMenuContent className="w-56" align="end" forceMount>
|
30 |
+
<DropdownMenuLabel className="font-normal">
|
31 |
+
<div className="flex flex-col space-y-1">
|
32 |
+
<p className="text-sm font-medium leading-none">shadcn</p>
|
33 |
+
<p className="text-xs leading-none text-muted-foreground">
|
34 |
+
m@example.com
|
35 |
+
</p>
|
36 |
+
</div>
|
37 |
+
</DropdownMenuLabel>
|
38 |
+
<DropdownMenuSeparator />
|
39 |
+
<DropdownMenuGroup>
|
40 |
+
<DropdownMenuItem>
|
41 |
+
Profile
|
42 |
+
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
43 |
+
</DropdownMenuItem>
|
44 |
+
<DropdownMenuItem>
|
45 |
+
Billing
|
46 |
+
<DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
|
47 |
+
</DropdownMenuItem>
|
48 |
+
<DropdownMenuItem>
|
49 |
+
Settings
|
50 |
+
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
|
51 |
+
</DropdownMenuItem>
|
52 |
+
<DropdownMenuItem>New Team</DropdownMenuItem>
|
53 |
+
</DropdownMenuGroup>
|
54 |
+
<DropdownMenuSeparator />
|
55 |
+
<DropdownMenuItem>
|
56 |
+
Log out
|
57 |
+
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
|
58 |
+
</DropdownMenuItem>
|
59 |
+
</DropdownMenuContent>
|
60 |
+
</DropdownMenu>
|
61 |
+
)
|
62 |
+
}
|
src/components/ui/avatar.tsx
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
5 |
+
|
6 |
+
import { cn } from "@/lib/utils"
|
7 |
+
|
8 |
+
const Avatar = React.forwardRef<
|
9 |
+
React.ElementRef<typeof AvatarPrimitive.Root>,
|
10 |
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
11 |
+
>(({ className, ...props }, ref) => (
|
12 |
+
<AvatarPrimitive.Root
|
13 |
+
ref={ref}
|
14 |
+
className={cn(
|
15 |
+
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
16 |
+
className
|
17 |
+
)}
|
18 |
+
{...props}
|
19 |
+
/>
|
20 |
+
))
|
21 |
+
Avatar.displayName = AvatarPrimitive.Root.displayName
|
22 |
+
|
23 |
+
const AvatarImage = React.forwardRef<
|
24 |
+
React.ElementRef<typeof AvatarPrimitive.Image>,
|
25 |
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
26 |
+
>(({ className, ...props }, ref) => (
|
27 |
+
<AvatarPrimitive.Image
|
28 |
+
ref={ref}
|
29 |
+
className={cn("aspect-square h-full w-full", className)}
|
30 |
+
{...props}
|
31 |
+
/>
|
32 |
+
))
|
33 |
+
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
34 |
+
|
35 |
+
const AvatarFallback = React.forwardRef<
|
36 |
+
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
37 |
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
38 |
+
>(({ className, ...props }, ref) => (
|
39 |
+
<AvatarPrimitive.Fallback
|
40 |
+
ref={ref}
|
41 |
+
className={cn(
|
42 |
+
"flex h-full w-full items-center justify-center rounded-full bg-zinc-100 dark:bg-zinc-800",
|
43 |
+
className
|
44 |
+
)}
|
45 |
+
{...props}
|
46 |
+
/>
|
47 |
+
))
|
48 |
+
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
49 |
+
|
50 |
+
export { Avatar, AvatarImage, AvatarFallback }
|
src/components/ui/dropdown-menu.tsx
ADDED
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
5 |
+
import { Check, ChevronRight, Circle } from "lucide-react"
|
6 |
+
|
7 |
+
import { cn } from "@/lib/utils"
|
8 |
+
|
9 |
+
const DropdownMenu = DropdownMenuPrimitive.Root
|
10 |
+
|
11 |
+
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
12 |
+
|
13 |
+
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
14 |
+
|
15 |
+
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
16 |
+
|
17 |
+
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
18 |
+
|
19 |
+
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
20 |
+
|
21 |
+
const DropdownMenuSubTrigger = React.forwardRef<
|
22 |
+
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
23 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
24 |
+
inset?: boolean
|
25 |
+
}
|
26 |
+
>(({ className, inset, children, ...props }, ref) => (
|
27 |
+
<DropdownMenuPrimitive.SubTrigger
|
28 |
+
ref={ref}
|
29 |
+
className={cn(
|
30 |
+
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-zinc-100 data-[state=open]:bg-zinc-100 dark:focus:bg-zinc-800 dark:data-[state=open]:bg-zinc-800",
|
31 |
+
inset && "pl-8",
|
32 |
+
className
|
33 |
+
)}
|
34 |
+
{...props}
|
35 |
+
>
|
36 |
+
{children}
|
37 |
+
<ChevronRight className="ml-auto h-4 w-4" />
|
38 |
+
</DropdownMenuPrimitive.SubTrigger>
|
39 |
+
))
|
40 |
+
DropdownMenuSubTrigger.displayName =
|
41 |
+
DropdownMenuPrimitive.SubTrigger.displayName
|
42 |
+
|
43 |
+
const DropdownMenuSubContent = React.forwardRef<
|
44 |
+
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
45 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
46 |
+
>(({ className, ...props }, ref) => (
|
47 |
+
<DropdownMenuPrimitive.SubContent
|
48 |
+
ref={ref}
|
49 |
+
className={cn(
|
50 |
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-zinc-200 bg-white p-1 text-zinc-950 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50",
|
51 |
+
className
|
52 |
+
)}
|
53 |
+
{...props}
|
54 |
+
/>
|
55 |
+
))
|
56 |
+
DropdownMenuSubContent.displayName =
|
57 |
+
DropdownMenuPrimitive.SubContent.displayName
|
58 |
+
|
59 |
+
const DropdownMenuContent = React.forwardRef<
|
60 |
+
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
61 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
62 |
+
>(({ className, sideOffset = 4, ...props }, ref) => (
|
63 |
+
<DropdownMenuPrimitive.Portal>
|
64 |
+
<DropdownMenuPrimitive.Content
|
65 |
+
ref={ref}
|
66 |
+
sideOffset={sideOffset}
|
67 |
+
className={cn(
|
68 |
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-zinc-200 bg-white p-1 text-zinc-950 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50",
|
69 |
+
className
|
70 |
+
)}
|
71 |
+
{...props}
|
72 |
+
/>
|
73 |
+
</DropdownMenuPrimitive.Portal>
|
74 |
+
))
|
75 |
+
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
76 |
+
|
77 |
+
const DropdownMenuItem = React.forwardRef<
|
78 |
+
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
79 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
80 |
+
inset?: boolean
|
81 |
+
}
|
82 |
+
>(({ className, inset, ...props }, ref) => (
|
83 |
+
<DropdownMenuPrimitive.Item
|
84 |
+
ref={ref}
|
85 |
+
className={cn(
|
86 |
+
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-zinc-100 focus:text-zinc-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-zinc-800 dark:focus:text-zinc-50",
|
87 |
+
inset && "pl-8",
|
88 |
+
className
|
89 |
+
)}
|
90 |
+
{...props}
|
91 |
+
/>
|
92 |
+
))
|
93 |
+
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
94 |
+
|
95 |
+
const DropdownMenuCheckboxItem = React.forwardRef<
|
96 |
+
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
97 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
98 |
+
>(({ className, children, checked, ...props }, ref) => (
|
99 |
+
<DropdownMenuPrimitive.CheckboxItem
|
100 |
+
ref={ref}
|
101 |
+
className={cn(
|
102 |
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-zinc-100 focus:text-zinc-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-zinc-800 dark:focus:text-zinc-50",
|
103 |
+
className
|
104 |
+
)}
|
105 |
+
checked={checked}
|
106 |
+
{...props}
|
107 |
+
>
|
108 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
109 |
+
<DropdownMenuPrimitive.ItemIndicator>
|
110 |
+
<Check className="h-4 w-4" />
|
111 |
+
</DropdownMenuPrimitive.ItemIndicator>
|
112 |
+
</span>
|
113 |
+
{children}
|
114 |
+
</DropdownMenuPrimitive.CheckboxItem>
|
115 |
+
))
|
116 |
+
DropdownMenuCheckboxItem.displayName =
|
117 |
+
DropdownMenuPrimitive.CheckboxItem.displayName
|
118 |
+
|
119 |
+
const DropdownMenuRadioItem = React.forwardRef<
|
120 |
+
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
121 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
122 |
+
>(({ className, children, ...props }, ref) => (
|
123 |
+
<DropdownMenuPrimitive.RadioItem
|
124 |
+
ref={ref}
|
125 |
+
className={cn(
|
126 |
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-zinc-100 focus:text-zinc-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-zinc-800 dark:focus:text-zinc-50",
|
127 |
+
className
|
128 |
+
)}
|
129 |
+
{...props}
|
130 |
+
>
|
131 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
132 |
+
<DropdownMenuPrimitive.ItemIndicator>
|
133 |
+
<Circle className="h-2 w-2 fill-current" />
|
134 |
+
</DropdownMenuPrimitive.ItemIndicator>
|
135 |
+
</span>
|
136 |
+
{children}
|
137 |
+
</DropdownMenuPrimitive.RadioItem>
|
138 |
+
))
|
139 |
+
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
140 |
+
|
141 |
+
const DropdownMenuLabel = React.forwardRef<
|
142 |
+
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
143 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
144 |
+
inset?: boolean
|
145 |
+
}
|
146 |
+
>(({ className, inset, ...props }, ref) => (
|
147 |
+
<DropdownMenuPrimitive.Label
|
148 |
+
ref={ref}
|
149 |
+
className={cn(
|
150 |
+
"px-2 py-1.5 text-sm font-semibold",
|
151 |
+
inset && "pl-8",
|
152 |
+
className
|
153 |
+
)}
|
154 |
+
{...props}
|
155 |
+
/>
|
156 |
+
))
|
157 |
+
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
158 |
+
|
159 |
+
const DropdownMenuSeparator = React.forwardRef<
|
160 |
+
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
161 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
162 |
+
>(({ className, ...props }, ref) => (
|
163 |
+
<DropdownMenuPrimitive.Separator
|
164 |
+
ref={ref}
|
165 |
+
className={cn("-mx-1 my-1 h-px bg-zinc-100 dark:bg-zinc-800", className)}
|
166 |
+
{...props}
|
167 |
+
/>
|
168 |
+
))
|
169 |
+
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
170 |
+
|
171 |
+
const DropdownMenuShortcut = ({
|
172 |
+
className,
|
173 |
+
...props
|
174 |
+
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
175 |
+
return (
|
176 |
+
<span
|
177 |
+
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
178 |
+
{...props}
|
179 |
+
/>
|
180 |
+
)
|
181 |
+
}
|
182 |
+
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
183 |
+
|
184 |
+
export {
|
185 |
+
DropdownMenu,
|
186 |
+
DropdownMenuTrigger,
|
187 |
+
DropdownMenuContent,
|
188 |
+
DropdownMenuItem,
|
189 |
+
DropdownMenuCheckboxItem,
|
190 |
+
DropdownMenuRadioItem,
|
191 |
+
DropdownMenuLabel,
|
192 |
+
DropdownMenuSeparator,
|
193 |
+
DropdownMenuShortcut,
|
194 |
+
DropdownMenuGroup,
|
195 |
+
DropdownMenuPortal,
|
196 |
+
DropdownMenuSub,
|
197 |
+
DropdownMenuSubContent,
|
198 |
+
DropdownMenuSubTrigger,
|
199 |
+
DropdownMenuRadioGroup,
|
200 |
+
}
|
src/components/ui/input.tsx
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from "react"
|
2 |
+
|
3 |
+
import { cn } from "@/lib/utils"
|
4 |
+
|
5 |
+
export interface InputProps
|
6 |
+
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
7 |
+
|
8 |
+
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
9 |
+
({ className, type, ...props }, ref) => {
|
10 |
+
return (
|
11 |
+
<input
|
12 |
+
type={type}
|
13 |
+
className={cn(
|
14 |
+
"flex h-10 w-full rounded-md border border-zinc-200 border-zinc-200 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-800 dark:border-zinc-800 dark:bg-zinc-950 dark:ring-offset-zinc-950 dark:placeholder:text-zinc-400 dark:focus-visible:ring-zinc-800",
|
15 |
+
className
|
16 |
+
)}
|
17 |
+
ref={ref}
|
18 |
+
{...props}
|
19 |
+
/>
|
20 |
+
)
|
21 |
+
}
|
22 |
+
)
|
23 |
+
Input.displayName = "Input"
|
24 |
+
|
25 |
+
export { Input }
|
src/components/ui/popover.tsx
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
5 |
+
|
6 |
+
import { cn } from "@/lib/utils"
|
7 |
+
|
8 |
+
const Popover = PopoverPrimitive.Root
|
9 |
+
|
10 |
+
const PopoverTrigger = PopoverPrimitive.Trigger
|
11 |
+
|
12 |
+
const PopoverContent = React.forwardRef<
|
13 |
+
React.ElementRef<typeof PopoverPrimitive.Content>,
|
14 |
+
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
15 |
+
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
16 |
+
<PopoverPrimitive.Portal>
|
17 |
+
<PopoverPrimitive.Content
|
18 |
+
ref={ref}
|
19 |
+
align={align}
|
20 |
+
sideOffset={sideOffset}
|
21 |
+
className={cn(
|
22 |
+
"z-50 w-72 rounded-md border border-zinc-200 bg-white p-4 text-zinc-950 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50",
|
23 |
+
className
|
24 |
+
)}
|
25 |
+
{...props}
|
26 |
+
/>
|
27 |
+
</PopoverPrimitive.Portal>
|
28 |
+
))
|
29 |
+
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
30 |
+
|
31 |
+
export { Popover, PopoverTrigger, PopoverContent }
|
src/components/ui/select.tsx
ADDED
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as SelectPrimitive from "@radix-ui/react-select"
|
5 |
+
import { Check, ChevronDown } from "lucide-react"
|
6 |
+
|
7 |
+
import { cn } from "@/lib/utils"
|
8 |
+
|
9 |
+
const Select = SelectPrimitive.Root
|
10 |
+
|
11 |
+
const SelectGroup = SelectPrimitive.Group
|
12 |
+
|
13 |
+
const SelectValue = SelectPrimitive.Value
|
14 |
+
|
15 |
+
const SelectTrigger = React.forwardRef<
|
16 |
+
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
17 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
18 |
+
>(({ className, children, ...props }, ref) => (
|
19 |
+
<SelectPrimitive.Trigger
|
20 |
+
ref={ref}
|
21 |
+
className={cn(
|
22 |
+
"flex h-10 w-full items-center justify-between rounded-md border border-zinc-200 border-zinc-200 bg-transparent px-3 py-2 text-sm ring-offset-white placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-800 dark:border-zinc-800 dark:ring-offset-zinc-950 dark:placeholder:text-zinc-400 dark:focus:ring-zinc-800",
|
23 |
+
className
|
24 |
+
)}
|
25 |
+
{...props}
|
26 |
+
>
|
27 |
+
{children}
|
28 |
+
<SelectPrimitive.Icon asChild>
|
29 |
+
<ChevronDown className="h-4 w-4 opacity-50" />
|
30 |
+
</SelectPrimitive.Icon>
|
31 |
+
</SelectPrimitive.Trigger>
|
32 |
+
))
|
33 |
+
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
34 |
+
|
35 |
+
const SelectContent = React.forwardRef<
|
36 |
+
React.ElementRef<typeof SelectPrimitive.Content>,
|
37 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
38 |
+
>(({ className, children, position = "popper", ...props }, ref) => (
|
39 |
+
<SelectPrimitive.Portal>
|
40 |
+
<SelectPrimitive.Content
|
41 |
+
ref={ref}
|
42 |
+
className={cn(
|
43 |
+
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border border-zinc-200 bg-white text-zinc-950 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50",
|
44 |
+
position === "popper" &&
|
45 |
+
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
46 |
+
className
|
47 |
+
)}
|
48 |
+
position={position}
|
49 |
+
{...props}
|
50 |
+
>
|
51 |
+
<SelectPrimitive.Viewport
|
52 |
+
className={cn(
|
53 |
+
"p-1",
|
54 |
+
position === "popper" &&
|
55 |
+
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
56 |
+
)}
|
57 |
+
>
|
58 |
+
{children}
|
59 |
+
</SelectPrimitive.Viewport>
|
60 |
+
</SelectPrimitive.Content>
|
61 |
+
</SelectPrimitive.Portal>
|
62 |
+
))
|
63 |
+
SelectContent.displayName = SelectPrimitive.Content.displayName
|
64 |
+
|
65 |
+
const SelectLabel = React.forwardRef<
|
66 |
+
React.ElementRef<typeof SelectPrimitive.Label>,
|
67 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
68 |
+
>(({ className, ...props }, ref) => (
|
69 |
+
<SelectPrimitive.Label
|
70 |
+
ref={ref}
|
71 |
+
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
72 |
+
{...props}
|
73 |
+
/>
|
74 |
+
))
|
75 |
+
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
76 |
+
|
77 |
+
const SelectItem = React.forwardRef<
|
78 |
+
React.ElementRef<typeof SelectPrimitive.Item>,
|
79 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
80 |
+
>(({ className, children, ...props }, ref) => (
|
81 |
+
<SelectPrimitive.Item
|
82 |
+
ref={ref}
|
83 |
+
className={cn(
|
84 |
+
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-zinc-100 focus:text-zinc-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-zinc-800 dark:focus:text-zinc-50",
|
85 |
+
className
|
86 |
+
)}
|
87 |
+
{...props}
|
88 |
+
>
|
89 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
90 |
+
<SelectPrimitive.ItemIndicator>
|
91 |
+
<Check className="h-4 w-4" />
|
92 |
+
</SelectPrimitive.ItemIndicator>
|
93 |
+
</span>
|
94 |
+
|
95 |
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
96 |
+
</SelectPrimitive.Item>
|
97 |
+
))
|
98 |
+
SelectItem.displayName = SelectPrimitive.Item.displayName
|
99 |
+
|
100 |
+
const SelectSeparator = React.forwardRef<
|
101 |
+
React.ElementRef<typeof SelectPrimitive.Separator>,
|
102 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
103 |
+
>(({ className, ...props }, ref) => (
|
104 |
+
<SelectPrimitive.Separator
|
105 |
+
ref={ref}
|
106 |
+
className={cn("-mx-1 my-1 h-px bg-zinc-100 dark:bg-zinc-800", className)}
|
107 |
+
{...props}
|
108 |
+
/>
|
109 |
+
))
|
110 |
+
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
111 |
+
|
112 |
+
export {
|
113 |
+
Select,
|
114 |
+
SelectGroup,
|
115 |
+
SelectValue,
|
116 |
+
SelectTrigger,
|
117 |
+
SelectContent,
|
118 |
+
SelectLabel,
|
119 |
+
SelectItem,
|
120 |
+
SelectSeparator,
|
121 |
+
}
|
src/components/ui/separator.tsx
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
5 |
+
|
6 |
+
import { cn } from "@/lib/utils"
|
7 |
+
|
8 |
+
const Separator = React.forwardRef<
|
9 |
+
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
10 |
+
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
11 |
+
>(
|
12 |
+
(
|
13 |
+
{ className, orientation = "horizontal", decorative = true, ...props },
|
14 |
+
ref
|
15 |
+
) => (
|
16 |
+
<SeparatorPrimitive.Root
|
17 |
+
ref={ref}
|
18 |
+
decorative={decorative}
|
19 |
+
orientation={orientation}
|
20 |
+
className={cn(
|
21 |
+
"shrink-0 bg-zinc-200 dark:bg-zinc-800",
|
22 |
+
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
23 |
+
className
|
24 |
+
)}
|
25 |
+
{...props}
|
26 |
+
/>
|
27 |
+
)
|
28 |
+
)
|
29 |
+
Separator.displayName = SeparatorPrimitive.Root.displayName
|
30 |
+
|
31 |
+
export { Separator }
|
src/components/ui/table.tsx
ADDED
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from "react"
|
2 |
+
|
3 |
+
import { cn } from "@/lib/utils"
|
4 |
+
|
5 |
+
const Table = React.forwardRef<
|
6 |
+
HTMLTableElement,
|
7 |
+
React.HTMLAttributes<HTMLTableElement>
|
8 |
+
>(({ className, ...props }, ref) => (
|
9 |
+
<div className="w-full overflow-auto">
|
10 |
+
<table
|
11 |
+
ref={ref}
|
12 |
+
className={cn("w-full caption-bottom text-sm", className)}
|
13 |
+
{...props}
|
14 |
+
/>
|
15 |
+
</div>
|
16 |
+
))
|
17 |
+
Table.displayName = "Table"
|
18 |
+
|
19 |
+
const TableHeader = React.forwardRef<
|
20 |
+
HTMLTableSectionElement,
|
21 |
+
React.HTMLAttributes<HTMLTableSectionElement>
|
22 |
+
>(({ className, ...props }, ref) => (
|
23 |
+
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
24 |
+
))
|
25 |
+
TableHeader.displayName = "TableHeader"
|
26 |
+
|
27 |
+
const TableBody = React.forwardRef<
|
28 |
+
HTMLTableSectionElement,
|
29 |
+
React.HTMLAttributes<HTMLTableSectionElement>
|
30 |
+
>(({ className, ...props }, ref) => (
|
31 |
+
<tbody
|
32 |
+
ref={ref}
|
33 |
+
className={cn("[&_tr:last-child]:border-0", className)}
|
34 |
+
{...props}
|
35 |
+
/>
|
36 |
+
))
|
37 |
+
TableBody.displayName = "TableBody"
|
38 |
+
|
39 |
+
const TableFooter = React.forwardRef<
|
40 |
+
HTMLTableSectionElement,
|
41 |
+
React.HTMLAttributes<HTMLTableSectionElement>
|
42 |
+
>(({ className, ...props }, ref) => (
|
43 |
+
<tfoot
|
44 |
+
ref={ref}
|
45 |
+
className={cn("bg-zinc-900 font-medium text-zinc-50 dark:bg-zinc-50 dark:text-zinc-900", className)}
|
46 |
+
{...props}
|
47 |
+
/>
|
48 |
+
))
|
49 |
+
TableFooter.displayName = "TableFooter"
|
50 |
+
|
51 |
+
const TableRow = React.forwardRef<
|
52 |
+
HTMLTableRowElement,
|
53 |
+
React.HTMLAttributes<HTMLTableRowElement>
|
54 |
+
>(({ className, ...props }, ref) => (
|
55 |
+
<tr
|
56 |
+
ref={ref}
|
57 |
+
className={cn(
|
58 |
+
"border-b transition-colors hover:bg-zinc-100/50 data-[state=selected]:bg-zinc-100 dark:hover:bg-zinc-800/50 dark:data-[state=selected]:bg-zinc-800",
|
59 |
+
className
|
60 |
+
)}
|
61 |
+
{...props}
|
62 |
+
/>
|
63 |
+
))
|
64 |
+
TableRow.displayName = "TableRow"
|
65 |
+
|
66 |
+
const TableHead = React.forwardRef<
|
67 |
+
HTMLTableCellElement,
|
68 |
+
React.ThHTMLAttributes<HTMLTableCellElement>
|
69 |
+
>(({ className, ...props }, ref) => (
|
70 |
+
<th
|
71 |
+
ref={ref}
|
72 |
+
className={cn(
|
73 |
+
"h-12 px-4 text-left align-middle font-medium text-zinc-500 [&:has([role=checkbox])]:pr-0 dark:text-zinc-400",
|
74 |
+
className
|
75 |
+
)}
|
76 |
+
{...props}
|
77 |
+
/>
|
78 |
+
))
|
79 |
+
TableHead.displayName = "TableHead"
|
80 |
+
|
81 |
+
const TableCell = React.forwardRef<
|
82 |
+
HTMLTableCellElement,
|
83 |
+
React.TdHTMLAttributes<HTMLTableCellElement>
|
84 |
+
>(({ className, ...props }, ref) => (
|
85 |
+
<td
|
86 |
+
ref={ref}
|
87 |
+
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
88 |
+
{...props}
|
89 |
+
/>
|
90 |
+
))
|
91 |
+
TableCell.displayName = "TableCell"
|
92 |
+
|
93 |
+
const TableCaption = React.forwardRef<
|
94 |
+
HTMLTableCaptionElement,
|
95 |
+
React.HTMLAttributes<HTMLTableCaptionElement>
|
96 |
+
>(({ className, ...props }, ref) => (
|
97 |
+
<caption
|
98 |
+
ref={ref}
|
99 |
+
className={cn("mt-4 text-sm text-zinc-500 dark:text-zinc-400", className)}
|
100 |
+
{...props}
|
101 |
+
/>
|
102 |
+
))
|
103 |
+
TableCaption.displayName = "TableCaption"
|
104 |
+
|
105 |
+
export {
|
106 |
+
Table,
|
107 |
+
TableHeader,
|
108 |
+
TableBody,
|
109 |
+
TableFooter,
|
110 |
+
TableHead,
|
111 |
+
TableRow,
|
112 |
+
TableCell,
|
113 |
+
TableCaption,
|
114 |
+
}
|
src/core/hasBadWords.ts
CHANGED
@@ -2,7 +2,7 @@
|
|
2 |
// as the tool will be public, we need some level of moderation to bred unfair use
|
3 |
// note: another possibility could be to use the moderation api from openai,
|
4 |
// but it is paying
|
5 |
-
const config = JSON.parse(`${process.env.
|
6 |
|
7 |
export const hasBadWords = (userPrompt: string) => {
|
8 |
const normalized = userPrompt.trim().toLowerCase()
|
|
|
2 |
// as the tool will be public, we need some level of moderation to bred unfair use
|
3 |
// note: another possibility could be to use the moderation api from openai,
|
4 |
// but it is paying
|
5 |
+
const config = JSON.parse(`${process.env.VC_MODERATION_FILTER_PATTERN}`)
|
6 |
|
7 |
export const hasBadWords = (userPrompt: string) => {
|
8 |
const normalized = userPrompt.trim().toLowerCase()
|
src/core/interpolateVideo.ts
CHANGED
@@ -9,7 +9,7 @@ import tmpDir from "temp-dir"
|
|
9 |
import { downloadVideo } from "./downloadVideo"
|
10 |
|
11 |
const instances: string[] = [
|
12 |
-
`${process.env.
|
13 |
]
|
14 |
|
15 |
export const interpolateVideo = async (fileName: string) => {
|
|
|
9 |
import { downloadVideo } from "./downloadVideo"
|
10 |
|
11 |
const instances: string[] = [
|
12 |
+
`${process.env.VC_VIDEO_INTERPOLATION_SPACE_API_URL}`
|
13 |
]
|
14 |
|
15 |
export const interpolateVideo = async (fileName: string) => {
|