create-tulip-app 0.6.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +3095 -0
- package/dist/index.mjs.map +1 -0
- package/dist/templates/basic/README.md +1 -0
- package/dist/templates/basic/drizzle.config.ts +11 -0
- package/dist/templates/basic/next.config.ts +15 -0
- package/dist/templates/basic/package.json +80 -0
- package/dist/templates/basic/postcss.config.js +6 -0
- package/dist/templates/basic/public/fonts/Inter/Inter-Black.ttf +0 -0
- package/dist/templates/basic/public/fonts/Inter/Inter-Bold.ttf +0 -0
- package/dist/templates/basic/public/fonts/Inter/Inter-ExtraBold.ttf +0 -0
- package/dist/templates/basic/public/fonts/Inter/Inter-ExtraLight.ttf +0 -0
- package/dist/templates/basic/public/fonts/Inter/Inter-Italic.ttf +0 -0
- package/dist/templates/basic/public/fonts/Inter/Inter-Light.ttf +0 -0
- package/dist/templates/basic/public/fonts/Inter/Inter-Medium.ttf +0 -0
- package/dist/templates/basic/public/fonts/Inter/Inter-Regular.ttf +0 -0
- package/dist/templates/basic/public/fonts/Inter/Inter-SemiBold.ttf +0 -0
- package/dist/templates/basic/public/fonts/Inter/Inter-Thin.ttf +0 -0
- package/dist/templates/basic/public/icons/icon-x192.png +0 -0
- package/dist/templates/basic/public/icons/icon-x512.png +0 -0
- package/dist/templates/basic/public/images/auth-background.jpeg +0 -0
- package/dist/templates/basic/public/images/placeholder.jpeg +0 -0
- package/dist/templates/basic/src/app/admin/(dashboard)/_components/budget-bar-chart.client.tsx +135 -0
- package/dist/templates/basic/src/app/admin/(dashboard)/_components/date-year-picker.tsx +49 -0
- package/dist/templates/basic/src/app/admin/(dashboard)/layout.tsx +16 -0
- package/dist/templates/basic/src/app/admin/(dashboard)/loading.tsx +15 -0
- package/dist/templates/basic/src/app/admin/(dashboard)/page.tsx +12 -0
- package/dist/templates/basic/src/app/admin/drive/(root)/layout.tsx +16 -0
- package/dist/templates/basic/src/app/admin/drive/(root)/loading.tsx +5 -0
- package/dist/templates/basic/src/app/admin/drive/(root)/page.tsx +3 -0
- package/dist/templates/basic/src/app/admin/drive/[namespace]/layout.tsx +21 -0
- package/dist/templates/basic/src/app/admin/drive/[namespace]/loading.tsx +5 -0
- package/dist/templates/basic/src/app/admin/drive/[namespace]/page.tsx +5 -0
- package/dist/templates/basic/src/app/admin/drive/_components/command-create.tsx +104 -0
- package/dist/templates/basic/src/app/admin/drive/_components/command-upload.tsx +31 -0
- package/dist/templates/basic/src/app/admin/drive/_components/drive-context.client.tsx +175 -0
- package/dist/templates/basic/src/app/admin/drive/_components/drive-header.client.tsx +219 -0
- package/dist/templates/basic/src/app/admin/drive/_components/drive-sidebar.tsx +61 -0
- package/dist/templates/basic/src/app/admin/drive/_components/drive-view.client.tsx +49 -0
- package/dist/templates/basic/src/app/admin/drive/_components/grid.client.tsx +372 -0
- package/dist/templates/basic/src/app/admin/drive/_components/list.client.tsx +83 -0
- package/dist/templates/basic/src/app/admin/drive/_components/toolbars.tsx +74 -0
- package/dist/templates/basic/src/app/admin/drive/_config/columns-data.tsx +98 -0
- package/dist/templates/basic/src/app/admin/drive/_config/commands.tsx +197 -0
- package/dist/templates/basic/src/app/admin/drive/_config/types.tsx +90 -0
- package/dist/templates/basic/src/app/admin/drive/_lib/router.ts +72 -0
- package/dist/templates/basic/src/app/admin/drive/_lib/search-params.ts +21 -0
- package/dist/templates/basic/src/app/admin/drive/loading.tsx +10 -0
- package/dist/templates/basic/src/app/admin/error.tsx +5 -0
- package/dist/templates/basic/src/app/admin/layout.tsx +21 -0
- package/dist/templates/basic/src/app/admin/not-found.tsx +3 -0
- package/dist/templates/basic/src/app/api/auth/[...all]/route.ts +4 -0
- package/dist/templates/basic/src/app/api/rpc/[[...rest]]/route.ts +5 -0
- package/dist/templates/basic/src/app/api/storage/files/route.ts +4 -0
- package/dist/templates/basic/src/app/apple-icon.png +0 -0
- package/dist/templates/basic/src/app/auth/forget-password/loading.tsx +3 -0
- package/dist/templates/basic/src/app/auth/forget-password/page.tsx +5 -0
- package/dist/templates/basic/src/app/auth/layout.tsx +13 -0
- package/dist/templates/basic/src/app/auth/login/loading.tsx +3 -0
- package/dist/templates/basic/src/app/auth/login/page.tsx +5 -0
- package/dist/templates/basic/src/app/auth/reset-password/loading.tsx +3 -0
- package/dist/templates/basic/src/app/auth/reset-password/page.tsx +5 -0
- package/dist/templates/basic/src/app/favicon.ico +0 -0
- package/dist/templates/basic/src/app/globals.css +3 -0
- package/dist/templates/basic/src/app/layout.tsx +17 -0
- package/dist/templates/basic/src/app/loading.tsx +3 -0
- package/dist/templates/basic/src/app/manifest.ts +4 -0
- package/dist/templates/basic/src/app/not-found.tsx +3 -0
- package/dist/templates/basic/src/instrumentation.ts +5 -0
- package/dist/templates/basic/src/lib/config/base.ts +11 -0
- package/dist/templates/basic/src/lib/config/paths.tsx +33 -0
- package/dist/templates/basic/src/proxy.ts +8 -0
- package/dist/templates/basic/src/server/auth/client.ts +6 -0
- package/dist/templates/basic/src/server/auth/init.ts +7 -0
- package/dist/templates/basic/src/server/auth/permissions.ts +46 -0
- package/dist/templates/basic/src/server/context.ts +9 -0
- package/dist/templates/basic/src/server/db/init.ts +16 -0
- package/dist/templates/basic/src/server/db/schema.ts +22 -0
- package/dist/templates/basic/src/server/db/types.ts +3 -0
- package/dist/templates/basic/src/server/providers/email.ts +3 -0
- package/dist/templates/basic/src/server/router/caller.ts +10 -0
- package/dist/templates/basic/src/server/router/client.ts +9 -0
- package/dist/templates/basic/src/server/router/init.ts +7 -0
- package/dist/templates/basic/src/server/router/register.ts +4 -0
- package/dist/templates/basic/src/server/router/router.ts +11 -0
- package/dist/templates/basic/src/server/storage/client.ts +14 -0
- package/dist/templates/basic/src/server/storage/config.ts +9 -0
- package/dist/templates/basic/src/server/storage/init.ts +15 -0
- package/dist/templates/basic/tsconfig.json +12 -0
- package/package.json +2 -1
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { pointerIntersection } from "@dnd-kit/collision";
|
|
4
|
+
import { useDroppable } from "@dnd-kit/react";
|
|
5
|
+
import { useSuspenseQuery } from "@tanstack/react-query";
|
|
6
|
+
import { InlineCommandMenu } from "@tulip-systems/core/commands/client";
|
|
7
|
+
import { BreadcrumbItem, BreadcrumbLink, BreadcrumbPage } from "@tulip-systems/core/components";
|
|
8
|
+
import {
|
|
9
|
+
Header,
|
|
10
|
+
type HeaderBreadcrumb,
|
|
11
|
+
HeaderBreadcrumbSeparator,
|
|
12
|
+
HeaderBreadcrumbs,
|
|
13
|
+
HeaderBreadcrumbsDesktopList,
|
|
14
|
+
HeaderBreadcrumbsDropdownMenu,
|
|
15
|
+
HeaderBreadcrumbsDropdownMenuItem,
|
|
16
|
+
HeaderBreadcrumbsLink,
|
|
17
|
+
HeaderBreadcrumbsMobileList,
|
|
18
|
+
HeaderTopbarBackButton,
|
|
19
|
+
HeaderTopbarMobileNavSwitcher,
|
|
20
|
+
ToggleGroup,
|
|
21
|
+
ToggleGroupItem,
|
|
22
|
+
Topbar,
|
|
23
|
+
TopbarTools,
|
|
24
|
+
} from "@tulip-systems/core/components/client";
|
|
25
|
+
import { cn } from "@tulip-systems/core/lib";
|
|
26
|
+
import { LayoutGridIcon, StretchVerticalIcon } from "lucide-react";
|
|
27
|
+
import Link from "next/link";
|
|
28
|
+
import { createSerializer, useQueryStates } from "nuqs";
|
|
29
|
+
import { type ComponentProps, Fragment } from "react";
|
|
30
|
+
import { orpc } from "@/server/router/client";
|
|
31
|
+
import { driveGlobalCommands } from "../_config/commands.js";
|
|
32
|
+
import { driveSearchParams } from "../_lib/search-params.js";
|
|
33
|
+
import { useDriveContext } from "./drive-context.client.js";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Drive Header
|
|
37
|
+
*/
|
|
38
|
+
export function DriveHeader({
|
|
39
|
+
namespace,
|
|
40
|
+
breadcrumbs,
|
|
41
|
+
...props
|
|
42
|
+
}: ComponentProps<"div"> & { namespace: string; breadcrumbs?: DriveHeaderBreadCrumb[] }) {
|
|
43
|
+
return (
|
|
44
|
+
<Header {...props}>
|
|
45
|
+
<Topbar>
|
|
46
|
+
<div className="flex h-full items-center gap-4">
|
|
47
|
+
<div className="h-full">
|
|
48
|
+
<HeaderTopbarBackButton />
|
|
49
|
+
<HeaderTopbarMobileNavSwitcher />
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<DriveHeaderBreadCrumbs namespace={namespace} breadcrumbs={breadcrumbs} />
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<DriveViewSwitcher />
|
|
56
|
+
|
|
57
|
+
<TopbarTools>
|
|
58
|
+
<InlineCommandMenu data={{ namespace }} commands={driveGlobalCommands} />
|
|
59
|
+
</TopbarTools>
|
|
60
|
+
</Topbar>
|
|
61
|
+
</Header>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Drive Page Topbar
|
|
67
|
+
*/
|
|
68
|
+
export function DrivePageTopbar({ className, ...props }: ComponentProps<"div">) {
|
|
69
|
+
const { namespace } = useDriveContext();
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div
|
|
73
|
+
{...props}
|
|
74
|
+
className={cn("flex items-center justify-between overflow-x-auto pt-3", className)}
|
|
75
|
+
>
|
|
76
|
+
<DriveHeaderBreadCrumbs namespace={namespace} />
|
|
77
|
+
|
|
78
|
+
<DriveViewSwitcher />
|
|
79
|
+
|
|
80
|
+
<InlineCommandMenu data={{ namespace }} commands={driveGlobalCommands} />
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Drive view switcher
|
|
87
|
+
*/
|
|
88
|
+
export function DriveViewSwitcher() {
|
|
89
|
+
const [query, setQuery] = useQueryStates(driveSearchParams);
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<ToggleGroup
|
|
93
|
+
type="single"
|
|
94
|
+
variant="outline"
|
|
95
|
+
value={query.view}
|
|
96
|
+
onValueChange={(value) => setQuery({ view: value ? (value as "list" | "grid") : null })}
|
|
97
|
+
>
|
|
98
|
+
<ToggleGroupItem value="grid" aria-label="Toggle grid view">
|
|
99
|
+
<LayoutGridIcon />
|
|
100
|
+
</ToggleGroupItem>
|
|
101
|
+
|
|
102
|
+
{/* <Separator orientation="vertical" className="mx-1" /> */}
|
|
103
|
+
|
|
104
|
+
<ToggleGroupItem value="list" aria-label="Toggle list view">
|
|
105
|
+
<StretchVerticalIcon />
|
|
106
|
+
</ToggleGroupItem>
|
|
107
|
+
</ToggleGroup>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Drive Header Breadcrumbs
|
|
113
|
+
*/
|
|
114
|
+
type DriveHeaderBreadCrumb = HeaderBreadcrumb & { droppable?: boolean };
|
|
115
|
+
|
|
116
|
+
function DriveHeaderBreadCrumbs(props: {
|
|
117
|
+
namespace: string;
|
|
118
|
+
breadcrumbs?: DriveHeaderBreadCrumb[];
|
|
119
|
+
}) {
|
|
120
|
+
const { namespace } = props;
|
|
121
|
+
const [query] = useQueryStates(driveSearchParams);
|
|
122
|
+
const serialize = createSerializer(driveSearchParams);
|
|
123
|
+
|
|
124
|
+
const parents = useSuspenseQuery(
|
|
125
|
+
orpc.drive.getFolderParents.queryOptions({ input: { id: query.parentId, namespace } }),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const breadcrumbs = [
|
|
129
|
+
...(props.breadcrumbs ?? []),
|
|
130
|
+
{
|
|
131
|
+
label: "Drive",
|
|
132
|
+
href: serialize({ ...query, parentId: null }),
|
|
133
|
+
droppable: true,
|
|
134
|
+
},
|
|
135
|
+
...(parents.data?.flatMap(({ name, id }) => ({
|
|
136
|
+
label: name,
|
|
137
|
+
href: serialize({ ...query, parentId: id }),
|
|
138
|
+
droppable: true,
|
|
139
|
+
})) ?? []),
|
|
140
|
+
] satisfies DriveHeaderBreadCrumb[];
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<HeaderBreadcrumbs>
|
|
144
|
+
<HeaderBreadcrumbsMobileList>
|
|
145
|
+
{breadcrumbs.length > 1 && (
|
|
146
|
+
<>
|
|
147
|
+
<HeaderBreadcrumbsDropdownMenu>
|
|
148
|
+
{breadcrumbs.slice(0, -1).map((breadcrumb, index) => (
|
|
149
|
+
<HeaderBreadcrumbsDropdownMenuItem key={index} breadcrumb={breadcrumb} />
|
|
150
|
+
))}
|
|
151
|
+
</HeaderBreadcrumbsDropdownMenu>
|
|
152
|
+
|
|
153
|
+
<HeaderBreadcrumbSeparator />
|
|
154
|
+
</>
|
|
155
|
+
)}
|
|
156
|
+
|
|
157
|
+
{breadcrumbs.slice(-1).map((breadcrumb, index, array) => (
|
|
158
|
+
<Fragment key={index}>
|
|
159
|
+
<HeaderBreadcrumbsLink breadcrumb={breadcrumb} />
|
|
160
|
+
{index < array.length - 1 && <HeaderBreadcrumbSeparator />}
|
|
161
|
+
</Fragment>
|
|
162
|
+
))}
|
|
163
|
+
</HeaderBreadcrumbsMobileList>
|
|
164
|
+
|
|
165
|
+
<HeaderBreadcrumbsDesktopList>
|
|
166
|
+
{breadcrumbs.length > 2 && (
|
|
167
|
+
<>
|
|
168
|
+
<HeaderBreadcrumbsDropdownMenu>
|
|
169
|
+
{breadcrumbs.slice(0, -2).map((breadcrumb, index) => (
|
|
170
|
+
<HeaderBreadcrumbsDropdownMenuItem key={index} breadcrumb={breadcrumb} />
|
|
171
|
+
))}
|
|
172
|
+
</HeaderBreadcrumbsDropdownMenu>
|
|
173
|
+
|
|
174
|
+
<HeaderBreadcrumbSeparator />
|
|
175
|
+
</>
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
{breadcrumbs.slice(-2).map((breadcrumb, index, array) => (
|
|
179
|
+
<Fragment key={index}>
|
|
180
|
+
{breadcrumb.droppable ? (
|
|
181
|
+
<DriveHeaderBreadcrumbsLink breadcrumb={breadcrumb} />
|
|
182
|
+
) : (
|
|
183
|
+
<HeaderBreadcrumbsLink breadcrumb={breadcrumb} />
|
|
184
|
+
)}
|
|
185
|
+
{index < array.length - 1 && <HeaderBreadcrumbSeparator />}
|
|
186
|
+
</Fragment>
|
|
187
|
+
))}
|
|
188
|
+
</HeaderBreadcrumbsDesktopList>
|
|
189
|
+
</HeaderBreadcrumbs>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function DriveHeaderBreadcrumbsLink(props: { breadcrumb: DriveHeaderBreadCrumb }) {
|
|
194
|
+
const id = props.breadcrumb.href?.split("=")[1] ?? null;
|
|
195
|
+
const [{ parentId }] = useQueryStates(driveSearchParams);
|
|
196
|
+
|
|
197
|
+
const { ref, isDropTarget } = useDroppable({
|
|
198
|
+
id: id ?? "drive",
|
|
199
|
+
collisionDetector: pointerIntersection,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<BreadcrumbItem
|
|
204
|
+
ref={ref}
|
|
205
|
+
data-over={isDropTarget && parentId !== id}
|
|
206
|
+
className="data-[over=true]:bg-primary/35"
|
|
207
|
+
>
|
|
208
|
+
{props.breadcrumb.href ? (
|
|
209
|
+
<BreadcrumbLink asChild>
|
|
210
|
+
<Link href={props.breadcrumb.href} className="truncate">
|
|
211
|
+
{props.breadcrumb.label}
|
|
212
|
+
</Link>
|
|
213
|
+
</BreadcrumbLink>
|
|
214
|
+
) : (
|
|
215
|
+
<BreadcrumbPage className="truncate">{props.breadcrumb.label}</BreadcrumbPage>
|
|
216
|
+
)}
|
|
217
|
+
</BreadcrumbItem>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// "use client";
|
|
2
|
+
|
|
3
|
+
// import { asyncDataLoaderFeature, hotkeysCoreFeature } from "@headless-tree/core";
|
|
4
|
+
// import { useTree } from "@headless-tree/react";
|
|
5
|
+
|
|
6
|
+
// import { AppRouter } from "@/trpc/router";
|
|
7
|
+
// import {
|
|
8
|
+
// Sidebar,
|
|
9
|
+
// SidebarContent,
|
|
10
|
+
// Tree,
|
|
11
|
+
// TreeItem,
|
|
12
|
+
// TreeItemLabel,
|
|
13
|
+
// } from "@tulip-systems/core/components/client";
|
|
14
|
+
// import { useTRPCClient } from "@tulip-systems/core/config/client";
|
|
15
|
+
// import { isFolder, Node } from "@tulip-systems/core/storage";
|
|
16
|
+
|
|
17
|
+
// const rootItemId = "--root--";
|
|
18
|
+
// const indent = 20;
|
|
19
|
+
|
|
20
|
+
// export default function DriveSidebar() {
|
|
21
|
+
// const trpcClient = useTRPCClient<AppRouter>();
|
|
22
|
+
|
|
23
|
+
// const tree = useTree<Node>({
|
|
24
|
+
// indent,
|
|
25
|
+
// rootItemId,
|
|
26
|
+
// getItemName: (item) => item.getItemData()?.name,
|
|
27
|
+
// isItemFolder: (node) => isFolder(node.getItemData() ?? {}),
|
|
28
|
+
// dataLoader: {
|
|
29
|
+
// getItem: async (id) => {
|
|
30
|
+
// const node = await trpcClient.drive.getNodeById.query({ id });
|
|
31
|
+
// if (!node) throw new Error("Error in getItem");
|
|
32
|
+
|
|
33
|
+
// return { ...node, children: node?.children.flatMap(({ id }) => id) };
|
|
34
|
+
// },
|
|
35
|
+
// getChildren: async (itemId) => {
|
|
36
|
+
// const children = await trpcClient.drive.getNodesByParentId.query({
|
|
37
|
+
// parentId: itemId === rootItemId ? null : itemId,
|
|
38
|
+
// });
|
|
39
|
+
|
|
40
|
+
// return children.filter(isFolder).flatMap(({ id }) => id);
|
|
41
|
+
// },
|
|
42
|
+
// },
|
|
43
|
+
// features: [asyncDataLoaderFeature, hotkeysCoreFeature],
|
|
44
|
+
// });
|
|
45
|
+
|
|
46
|
+
// return (
|
|
47
|
+
// <Sidebar className="ml-[var(--sidebar-width)] mt-[var(--header-top-bar-height)]">
|
|
48
|
+
// <SidebarContent className="px-2 py-4">
|
|
49
|
+
// <Tree indent={indent} tree={tree}>
|
|
50
|
+
// {tree.getItems().map((item) => {
|
|
51
|
+
// return (
|
|
52
|
+
// <TreeItem key={item.getId()} item={item}>
|
|
53
|
+
// <TreeItemLabel className="bg-transparent" />
|
|
54
|
+
// </TreeItem>
|
|
55
|
+
// );
|
|
56
|
+
// })}
|
|
57
|
+
// </Tree>
|
|
58
|
+
// </SidebarContent>
|
|
59
|
+
// </Sidebar>
|
|
60
|
+
// );
|
|
61
|
+
// }
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { TableLayout, TableSkeleton } from "@tulip-systems/core/data-tables";
|
|
4
|
+
import { cn } from "@tulip-systems/core/lib";
|
|
5
|
+
import { useQueryStates } from "nuqs";
|
|
6
|
+
import { type ComponentProps, Suspense } from "react";
|
|
7
|
+
import { driveSearchParams } from "../_lib/search-params.js";
|
|
8
|
+
import { DriveGrid, DriveGridLoading } from "./grid.client.js";
|
|
9
|
+
import { DriveList } from "./list.client.js";
|
|
10
|
+
import { DriveToolbar } from "./toolbars.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* DriveGridView
|
|
14
|
+
*/
|
|
15
|
+
export function DriveView({
|
|
16
|
+
className,
|
|
17
|
+
...props
|
|
18
|
+
}: ComponentProps<"div"> & Pick<ComponentProps<typeof DriveToolbar>, "filtersVisibility">) {
|
|
19
|
+
const [{ view }] = useQueryStates(driveSearchParams);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div {...props} className={cn("flex h-full flex-1 flex-col gap-y-12", className)}>
|
|
23
|
+
{view === "grid" && (
|
|
24
|
+
<Suspense fallback={<DriveGridLoading />}>
|
|
25
|
+
<TableLayout>
|
|
26
|
+
<DriveToolbar />
|
|
27
|
+
<DriveGrid />
|
|
28
|
+
</TableLayout>
|
|
29
|
+
</Suspense>
|
|
30
|
+
)}
|
|
31
|
+
{view === "list" && (
|
|
32
|
+
<Suspense fallback={<TableSkeleton />}>
|
|
33
|
+
<DriveList />
|
|
34
|
+
</Suspense>
|
|
35
|
+
)}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Drive Content
|
|
42
|
+
*/
|
|
43
|
+
export function DriveContent({ className, ...props }: ComponentProps<"div">) {
|
|
44
|
+
return (
|
|
45
|
+
<div {...props} className={cn("space-y-6", className)}>
|
|
46
|
+
{props.children}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useDraggable, useDroppable } from "@dnd-kit/react";
|
|
4
|
+
import { useSuspenseInfiniteQuery } from "@tanstack/react-query";
|
|
5
|
+
import {
|
|
6
|
+
ContextCommandMenu,
|
|
7
|
+
ContextCommandMenuContent,
|
|
8
|
+
ContextCommandMenuTrigger,
|
|
9
|
+
DropdownCommandMenu,
|
|
10
|
+
} from "@tulip-systems/core/commands/client";
|
|
11
|
+
import {
|
|
12
|
+
Card,
|
|
13
|
+
CardContent,
|
|
14
|
+
CardHeader,
|
|
15
|
+
CardTitle,
|
|
16
|
+
EmptyPage,
|
|
17
|
+
EmptyPageTitle,
|
|
18
|
+
findStatus,
|
|
19
|
+
Skeleton,
|
|
20
|
+
} from "@tulip-systems/core/components";
|
|
21
|
+
import { tableSearchParams } from "@tulip-systems/core/data-tables";
|
|
22
|
+
import { cn } from "@tulip-systems/core/lib";
|
|
23
|
+
import {
|
|
24
|
+
type FileNode,
|
|
25
|
+
type FolderNode,
|
|
26
|
+
getFileUrl,
|
|
27
|
+
type ImageVariant,
|
|
28
|
+
isFile,
|
|
29
|
+
isFolder,
|
|
30
|
+
} from "@tulip-systems/core/storage";
|
|
31
|
+
import { FolderIcon } from "lucide-react";
|
|
32
|
+
import { useInView } from "motion/react";
|
|
33
|
+
import Image from "next/image";
|
|
34
|
+
import { useRouter } from "next/navigation";
|
|
35
|
+
import { useQueryStates } from "nuqs";
|
|
36
|
+
import {
|
|
37
|
+
type ComponentProps,
|
|
38
|
+
type PropsWithChildren,
|
|
39
|
+
useDeferredValue,
|
|
40
|
+
useEffect,
|
|
41
|
+
useMemo,
|
|
42
|
+
useRef,
|
|
43
|
+
} from "react";
|
|
44
|
+
import { orpc } from "@/server/router/client";
|
|
45
|
+
import { driveCommands } from "../_config/commands.js";
|
|
46
|
+
import { nodeSubtypeConfig, nodeSubtypeVariants } from "../_config/types.js";
|
|
47
|
+
import { driveFilterSearchParams, driveSearchParams } from "../_lib/search-params.js";
|
|
48
|
+
import { useDriveContext } from "./drive-context.client.js";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* DriveGrid
|
|
52
|
+
*/
|
|
53
|
+
export function DriveGrid(props: ComponentProps<"div">) {
|
|
54
|
+
const [filters] = useQueryStates(driveFilterSearchParams);
|
|
55
|
+
const [{ parentId }] = useQueryStates(driveSearchParams);
|
|
56
|
+
const { namespace, where } = useDriveContext();
|
|
57
|
+
|
|
58
|
+
const [query] = useQueryStates(tableSearchParams);
|
|
59
|
+
const input = useDeferredValue({
|
|
60
|
+
...query,
|
|
61
|
+
filters: { ...filters, ...where, parentId, namespace },
|
|
62
|
+
});
|
|
63
|
+
// const { data } = useSuspenseQuery(orpc.drive.getNodesByParentId.queryOptions({ input }));
|
|
64
|
+
|
|
65
|
+
const response = useSuspenseInfiniteQuery(
|
|
66
|
+
orpc.drive.list.infiniteOptions({
|
|
67
|
+
initialPageParam: 0,
|
|
68
|
+
input: (cursor: number | undefined) => ({ ...input, cursor }),
|
|
69
|
+
getNextPageParam: ({ pagination }) => pagination.nextCursor,
|
|
70
|
+
getPreviousPageParam: ({ pagination }) => pagination.previousCursor,
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const queryData = useMemo(
|
|
75
|
+
() => response.data.pages.flatMap(({ data }) => data) ?? [],
|
|
76
|
+
[response.data],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<DriveGridContent {...props}>
|
|
81
|
+
{queryData.length > 0 ? (
|
|
82
|
+
queryData
|
|
83
|
+
.filter(Boolean)
|
|
84
|
+
.filter((node) => !node.hidden)
|
|
85
|
+
.flatMap((node) => {
|
|
86
|
+
if (node.isPending) {
|
|
87
|
+
return <DriveGridCardSkeleton key={node.id} />;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (isFolder(node)) {
|
|
91
|
+
const folder = node as FolderNode;
|
|
92
|
+
return <DriveGridFolderCard key={folder.id} folder={folder} />;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (isFile(node)) {
|
|
96
|
+
const file = node as FileNode;
|
|
97
|
+
return <DriveGridFileCard key={file.id} file={file} />;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return null;
|
|
101
|
+
})
|
|
102
|
+
) : (
|
|
103
|
+
<EmptyPage className="col-span-full min-h-[80dvh]">
|
|
104
|
+
<EmptyPageTitle>Geen resultaten gevonden</EmptyPageTitle>
|
|
105
|
+
</EmptyPage>
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
{response.isFetchingNextPage &&
|
|
109
|
+
Array.from({ length: response.data.pages[0]?.pagination.limit ?? 0 }).map((_, index) => (
|
|
110
|
+
<DriveGridCardSkeleton key={index} />
|
|
111
|
+
))}
|
|
112
|
+
|
|
113
|
+
<DriveGridBottombar
|
|
114
|
+
hasNextPage={response.hasNextPage}
|
|
115
|
+
fetchNextPage={response.fetchNextPage}
|
|
116
|
+
isFetching={response.isFetching}
|
|
117
|
+
isFetchingNextPage={response.isFetchingNextPage}
|
|
118
|
+
/>
|
|
119
|
+
</DriveGridContent>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* DriveGridContent
|
|
125
|
+
*/
|
|
126
|
+
function DriveGridContent(props: PropsWithChildren) {
|
|
127
|
+
return (
|
|
128
|
+
<div className="grid grid-cols-1 gap-5 md:grid-cols-[repeat(auto-fill,minmax(13rem,1fr))]">
|
|
129
|
+
{props.children}
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Folder card
|
|
136
|
+
*/
|
|
137
|
+
function DriveGridFolderCard({ folder }: { folder: FolderNode }) {
|
|
138
|
+
const router = useRouter();
|
|
139
|
+
|
|
140
|
+
const droppable = useDroppable({ id: folder.id });
|
|
141
|
+
const draggable = useDraggable({ id: folder.id });
|
|
142
|
+
|
|
143
|
+
if (folder.hidden) return null;
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<ContextCommandMenu>
|
|
147
|
+
<ContextCommandMenuTrigger asChild>
|
|
148
|
+
<Card
|
|
149
|
+
ref={draggable.ref}
|
|
150
|
+
data-dragging={draggable.isDragging}
|
|
151
|
+
className="group @container relative flex max-h-52 max-w-full cursor-pointer flex-col items-center gap-3 overflow-hidden bg-transparent p-2 hover:bg-muted/70 active:bg-muted data-[dragging=true]:opacity-50"
|
|
152
|
+
// onMouseOver={() =>
|
|
153
|
+
// queryClient.prefetchQuery(
|
|
154
|
+
// trpc.drive.getNodesByParentId.queryOptions({ parentId: folder.id, namespace }),
|
|
155
|
+
// )
|
|
156
|
+
// }
|
|
157
|
+
onDoubleClick={() => router.push(`?parentId=${folder.id}`)}
|
|
158
|
+
>
|
|
159
|
+
<div
|
|
160
|
+
ref={droppable.ref}
|
|
161
|
+
data-over={droppable.isDropTarget && !draggable.isDragging}
|
|
162
|
+
className="absolute inset-0 -z-10 data-[over=true]:bg-primary/35"
|
|
163
|
+
/>
|
|
164
|
+
|
|
165
|
+
<CardHeader className="flex w-full flex-row items-center justify-between gap-2.5 p-0 px-1.5">
|
|
166
|
+
<FolderIcon className="w-4 min-w-4 fill-foreground" />
|
|
167
|
+
|
|
168
|
+
<CardTitle className="w-full truncate text-sm leading-[1.2]" title={folder.name}>
|
|
169
|
+
{folder.name}
|
|
170
|
+
</CardTitle>
|
|
171
|
+
|
|
172
|
+
<DropdownCommandMenu data={folder} commands={driveCommands} />
|
|
173
|
+
</CardHeader>
|
|
174
|
+
|
|
175
|
+
<CardContent className="h-52 w-full overflow-hidden rounded-lg bg-background/50 p-0 group-hover:bg-background">
|
|
176
|
+
<div className="flex h-full w-full items-center justify-center">
|
|
177
|
+
<FolderIcon className="size-12 fill-foreground" />
|
|
178
|
+
</div>
|
|
179
|
+
</CardContent>
|
|
180
|
+
</Card>
|
|
181
|
+
</ContextCommandMenuTrigger>
|
|
182
|
+
|
|
183
|
+
<ContextCommandMenuContent data={folder} commands={driveCommands} />
|
|
184
|
+
</ContextCommandMenu>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* File card
|
|
190
|
+
*/
|
|
191
|
+
export function DriveGridFileCard({ file }: { file: FileNode }) {
|
|
192
|
+
const { id } = file;
|
|
193
|
+
|
|
194
|
+
const { ref, isDragging } = useDraggable({ id });
|
|
195
|
+
|
|
196
|
+
const subtype = findStatus(nodeSubtypeConfig, file.subtype);
|
|
197
|
+
const Icon = subtype?.icon;
|
|
198
|
+
if (file.hidden) return null;
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<ContextCommandMenu>
|
|
202
|
+
<ContextCommandMenuTrigger asChild>
|
|
203
|
+
<Card
|
|
204
|
+
key={id}
|
|
205
|
+
ref={ref}
|
|
206
|
+
data-dragging={isDragging}
|
|
207
|
+
className="@container flex max-h-52 max-w-full cursor-pointer flex-col items-center gap-3 overflow-hidden p-2 hover:bg-muted/70 active:bg-muted data-[dragging=true]:opacity-50"
|
|
208
|
+
onDoubleClick={() => window.open(getFileUrl(id), "_blank")}
|
|
209
|
+
>
|
|
210
|
+
<CardHeader className="flex w-full flex-row items-center justify-between gap-2.5 p-0 px-1.5">
|
|
211
|
+
{Icon && (
|
|
212
|
+
<Icon
|
|
213
|
+
className={nodeSubtypeVariants({ status: file.subtype, className: "size-4" })}
|
|
214
|
+
/>
|
|
215
|
+
)}
|
|
216
|
+
|
|
217
|
+
<CardTitle className="w-full truncate text-sm leading-[1.2]" title={file.name}>
|
|
218
|
+
{file.name}
|
|
219
|
+
</CardTitle>
|
|
220
|
+
|
|
221
|
+
<DropdownCommandMenu data={file} commands={driveCommands} />
|
|
222
|
+
</CardHeader>
|
|
223
|
+
|
|
224
|
+
<CardContent className="h-52 w-full overflow-hidden rounded-lg bg-background p-0">
|
|
225
|
+
{file.subtype === "image" ? (
|
|
226
|
+
<Image
|
|
227
|
+
src={getFileUrl(id)}
|
|
228
|
+
alt={file.name}
|
|
229
|
+
width={200}
|
|
230
|
+
height={200}
|
|
231
|
+
className="h-full w-full object-cover"
|
|
232
|
+
loader={({ width }) =>
|
|
233
|
+
getFileUrl(id, {
|
|
234
|
+
variant: `preview-${width}` as ImageVariant,
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
/>
|
|
238
|
+
) : (
|
|
239
|
+
<div className="flex h-full w-full items-center justify-center">
|
|
240
|
+
{Icon && (
|
|
241
|
+
<Icon
|
|
242
|
+
className={nodeSubtypeVariants({ status: file.subtype, className: "size-12" })}
|
|
243
|
+
/>
|
|
244
|
+
)}
|
|
245
|
+
</div>
|
|
246
|
+
)}
|
|
247
|
+
</CardContent>
|
|
248
|
+
</Card>
|
|
249
|
+
</ContextCommandMenuTrigger>
|
|
250
|
+
|
|
251
|
+
<ContextCommandMenuContent data={file} commands={driveCommands} />
|
|
252
|
+
</ContextCommandMenu>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Infinite table bottombar
|
|
258
|
+
*/
|
|
259
|
+
export function DriveGridBottombar(props: {
|
|
260
|
+
hasNextPage: boolean;
|
|
261
|
+
fetchNextPage: () => void;
|
|
262
|
+
isFetching: boolean;
|
|
263
|
+
isFetchingNextPage: boolean;
|
|
264
|
+
}) {
|
|
265
|
+
const scrollRef = useRef(null);
|
|
266
|
+
const isInView = useInView(scrollRef);
|
|
267
|
+
|
|
268
|
+
useEffect(() => {
|
|
269
|
+
if (isInView && props.hasNextPage && !props.isFetching && !props.isFetchingNextPage) {
|
|
270
|
+
props.fetchNextPage();
|
|
271
|
+
}
|
|
272
|
+
}, [
|
|
273
|
+
isInView,
|
|
274
|
+
props.fetchNextPage,
|
|
275
|
+
props.hasNextPage,
|
|
276
|
+
props.isFetching,
|
|
277
|
+
props.isFetchingNextPage,
|
|
278
|
+
]);
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<div className="relative col-span-full">
|
|
282
|
+
<div ref={scrollRef} className="absolute bottom-0 -z-50 min-h-[500px] bg-primary" />
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* DriveGridSkeleton
|
|
289
|
+
*/
|
|
290
|
+
export function DriveGridLoading({ className, ...props }: ComponentProps<"div">) {
|
|
291
|
+
return (
|
|
292
|
+
<div {...props} className={cn("flex flex-col gap-y-12", className)}>
|
|
293
|
+
<DriveGridContent>
|
|
294
|
+
{Array.from({ length: 12 }).map((_, index) => (
|
|
295
|
+
<DriveGridCardSkeleton key={index} />
|
|
296
|
+
))}
|
|
297
|
+
</DriveGridContent>
|
|
298
|
+
</div>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Card Skeleton
|
|
304
|
+
*/
|
|
305
|
+
|
|
306
|
+
function DriveGridCardSkeleton() {
|
|
307
|
+
return (
|
|
308
|
+
<Card className="flex max-h-52 cursor-pointer flex-col items-center gap-3 overflow-hidden bg-muted/70 p-3">
|
|
309
|
+
<Skeleton className="h-7" />
|
|
310
|
+
<CardContent className="h-52 w-full overflow-hidden rounded-lg bg-background p-0" />
|
|
311
|
+
</Card>
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Drive Grid Back Droppable
|
|
317
|
+
*/
|
|
318
|
+
// function DriveGridBackDroppable({ parentId }: { parentId: string }) {
|
|
319
|
+
// const [isVisible, setVisible] = useState(false);
|
|
320
|
+
|
|
321
|
+
// const { ref, isDropTarget } = useDroppable({ id: parentId });
|
|
322
|
+
|
|
323
|
+
// useDragDropMonitor({
|
|
324
|
+
// onDragStart: () => setVisible(true),
|
|
325
|
+
// onDragEnd: () => setVisible(false),
|
|
326
|
+
// });
|
|
327
|
+
|
|
328
|
+
// return (
|
|
329
|
+
// <AnimatePresence>
|
|
330
|
+
// {isVisible && (
|
|
331
|
+
// <div
|
|
332
|
+
// ref={ref}
|
|
333
|
+
// data-over={isDropTarget}
|
|
334
|
+
// className="border-primary/35 data-[over=true]:bg-primary/35 absolute left-0 right-0 top-0 z-50 flex h-[var(--header-top-bar-height)] justify-center border"
|
|
335
|
+
// />
|
|
336
|
+
// )}
|
|
337
|
+
// </AnimatePresence>
|
|
338
|
+
// );
|
|
339
|
+
// }
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Drive Grid Header
|
|
343
|
+
*/
|
|
344
|
+
// export function DriveGridHeader() {
|
|
345
|
+
// const trpc = useTRPC<AppRouter>();
|
|
346
|
+
// const [{ parentId }] = useQueryStates(driveGridSearchParams);
|
|
347
|
+
// const { namespace } = useDriveGridContext();
|
|
348
|
+
|
|
349
|
+
// const parents = useSuspenseQuery(
|
|
350
|
+
// trpc.drive.getFolderParents.queryOptions({ id: parentId, namespace }),
|
|
351
|
+
// );
|
|
352
|
+
|
|
353
|
+
// return (
|
|
354
|
+
// <Header>
|
|
355
|
+
// <Topbar>
|
|
356
|
+
// <TopbarTitle
|
|
357
|
+
// breadcrumbs={[
|
|
358
|
+
// { label: "Drive", href: "/admin/drive" },
|
|
359
|
+
// ...(parents.data?.flatMap((node) => ({
|
|
360
|
+
// label: node.name,
|
|
361
|
+
// href: `?parentId=${node.id}`,
|
|
362
|
+
// })) ?? []),
|
|
363
|
+
// ]}
|
|
364
|
+
// />
|
|
365
|
+
|
|
366
|
+
// <TopbarTools>
|
|
367
|
+
// <InlineCommandMenu data={{ parentId, namespace }} commands={driveGlobalCommands} />
|
|
368
|
+
// </TopbarTools>
|
|
369
|
+
// </Topbar>
|
|
370
|
+
// </Header>
|
|
371
|
+
// );
|
|
372
|
+
// }
|