create-tulip-app 0.6.1 → 0.7.0

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.
Files changed (90) hide show
  1. package/dist/index.d.mts +1 -0
  2. package/dist/index.mjs +3095 -0
  3. package/dist/index.mjs.map +1 -0
  4. package/dist/templates/basic/README.md +1 -0
  5. package/dist/templates/basic/drizzle.config.ts +11 -0
  6. package/dist/templates/basic/next.config.ts +15 -0
  7. package/dist/templates/basic/package.json +80 -0
  8. package/dist/templates/basic/postcss.config.js +6 -0
  9. package/dist/templates/basic/public/fonts/Inter/Inter-Black.ttf +0 -0
  10. package/dist/templates/basic/public/fonts/Inter/Inter-Bold.ttf +0 -0
  11. package/dist/templates/basic/public/fonts/Inter/Inter-ExtraBold.ttf +0 -0
  12. package/dist/templates/basic/public/fonts/Inter/Inter-ExtraLight.ttf +0 -0
  13. package/dist/templates/basic/public/fonts/Inter/Inter-Italic.ttf +0 -0
  14. package/dist/templates/basic/public/fonts/Inter/Inter-Light.ttf +0 -0
  15. package/dist/templates/basic/public/fonts/Inter/Inter-Medium.ttf +0 -0
  16. package/dist/templates/basic/public/fonts/Inter/Inter-Regular.ttf +0 -0
  17. package/dist/templates/basic/public/fonts/Inter/Inter-SemiBold.ttf +0 -0
  18. package/dist/templates/basic/public/fonts/Inter/Inter-Thin.ttf +0 -0
  19. package/dist/templates/basic/public/icons/icon-x192.png +0 -0
  20. package/dist/templates/basic/public/icons/icon-x512.png +0 -0
  21. package/dist/templates/basic/public/images/auth-background.jpeg +0 -0
  22. package/dist/templates/basic/public/images/placeholder.jpeg +0 -0
  23. package/dist/templates/basic/src/app/admin/(dashboard)/_components/budget-bar-chart.client.tsx +135 -0
  24. package/dist/templates/basic/src/app/admin/(dashboard)/_components/date-year-picker.tsx +49 -0
  25. package/dist/templates/basic/src/app/admin/(dashboard)/layout.tsx +16 -0
  26. package/dist/templates/basic/src/app/admin/(dashboard)/loading.tsx +15 -0
  27. package/dist/templates/basic/src/app/admin/(dashboard)/page.tsx +12 -0
  28. package/dist/templates/basic/src/app/admin/drive/(root)/layout.tsx +16 -0
  29. package/dist/templates/basic/src/app/admin/drive/(root)/loading.tsx +5 -0
  30. package/dist/templates/basic/src/app/admin/drive/(root)/page.tsx +3 -0
  31. package/dist/templates/basic/src/app/admin/drive/[namespace]/layout.tsx +21 -0
  32. package/dist/templates/basic/src/app/admin/drive/[namespace]/loading.tsx +5 -0
  33. package/dist/templates/basic/src/app/admin/drive/[namespace]/page.tsx +5 -0
  34. package/dist/templates/basic/src/app/admin/drive/_components/command-create.tsx +104 -0
  35. package/dist/templates/basic/src/app/admin/drive/_components/command-upload.tsx +31 -0
  36. package/dist/templates/basic/src/app/admin/drive/_components/drive-context.client.tsx +175 -0
  37. package/dist/templates/basic/src/app/admin/drive/_components/drive-header.client.tsx +219 -0
  38. package/dist/templates/basic/src/app/admin/drive/_components/drive-sidebar.tsx +61 -0
  39. package/dist/templates/basic/src/app/admin/drive/_components/drive-view.client.tsx +49 -0
  40. package/dist/templates/basic/src/app/admin/drive/_components/grid.client.tsx +372 -0
  41. package/dist/templates/basic/src/app/admin/drive/_components/list.client.tsx +83 -0
  42. package/dist/templates/basic/src/app/admin/drive/_components/toolbars.tsx +74 -0
  43. package/dist/templates/basic/src/app/admin/drive/_config/columns-data.tsx +98 -0
  44. package/dist/templates/basic/src/app/admin/drive/_config/commands.tsx +197 -0
  45. package/dist/templates/basic/src/app/admin/drive/_config/types.tsx +90 -0
  46. package/dist/templates/basic/src/app/admin/drive/_lib/router.ts +72 -0
  47. package/dist/templates/basic/src/app/admin/drive/_lib/search-params.ts +21 -0
  48. package/dist/templates/basic/src/app/admin/drive/loading.tsx +10 -0
  49. package/dist/templates/basic/src/app/admin/error.tsx +5 -0
  50. package/dist/templates/basic/src/app/admin/layout.tsx +21 -0
  51. package/dist/templates/basic/src/app/admin/not-found.tsx +3 -0
  52. package/dist/templates/basic/src/app/api/auth/[...all]/route.ts +4 -0
  53. package/dist/templates/basic/src/app/api/rpc/[[...rest]]/route.ts +5 -0
  54. package/dist/templates/basic/src/app/api/storage/files/route.ts +4 -0
  55. package/dist/templates/basic/src/app/apple-icon.png +0 -0
  56. package/dist/templates/basic/src/app/auth/forget-password/loading.tsx +3 -0
  57. package/dist/templates/basic/src/app/auth/forget-password/page.tsx +5 -0
  58. package/dist/templates/basic/src/app/auth/layout.tsx +13 -0
  59. package/dist/templates/basic/src/app/auth/login/loading.tsx +3 -0
  60. package/dist/templates/basic/src/app/auth/login/page.tsx +5 -0
  61. package/dist/templates/basic/src/app/auth/reset-password/loading.tsx +3 -0
  62. package/dist/templates/basic/src/app/auth/reset-password/page.tsx +5 -0
  63. package/dist/templates/basic/src/app/favicon.ico +0 -0
  64. package/dist/templates/basic/src/app/globals.css +3 -0
  65. package/dist/templates/basic/src/app/layout.tsx +17 -0
  66. package/dist/templates/basic/src/app/loading.tsx +3 -0
  67. package/dist/templates/basic/src/app/manifest.ts +4 -0
  68. package/dist/templates/basic/src/app/not-found.tsx +3 -0
  69. package/dist/templates/basic/src/instrumentation.ts +5 -0
  70. package/dist/templates/basic/src/lib/config/base.ts +11 -0
  71. package/dist/templates/basic/src/lib/config/paths.tsx +33 -0
  72. package/dist/templates/basic/src/proxy.ts +8 -0
  73. package/dist/templates/basic/src/server/auth/client.ts +6 -0
  74. package/dist/templates/basic/src/server/auth/init.ts +7 -0
  75. package/dist/templates/basic/src/server/auth/permissions.ts +46 -0
  76. package/dist/templates/basic/src/server/context.ts +9 -0
  77. package/dist/templates/basic/src/server/db/init.ts +16 -0
  78. package/dist/templates/basic/src/server/db/schema.ts +22 -0
  79. package/dist/templates/basic/src/server/db/types.ts +3 -0
  80. package/dist/templates/basic/src/server/providers/email.ts +3 -0
  81. package/dist/templates/basic/src/server/router/caller.ts +10 -0
  82. package/dist/templates/basic/src/server/router/client.ts +9 -0
  83. package/dist/templates/basic/src/server/router/init.ts +7 -0
  84. package/dist/templates/basic/src/server/router/register.ts +4 -0
  85. package/dist/templates/basic/src/server/router/router.ts +11 -0
  86. package/dist/templates/basic/src/server/storage/client.ts +14 -0
  87. package/dist/templates/basic/src/server/storage/config.ts +9 -0
  88. package/dist/templates/basic/src/server/storage/init.ts +14 -0
  89. package/dist/templates/basic/tsconfig.json +13 -0
  90. package/package.json +1 -1
@@ -0,0 +1,135 @@
1
+ "use client";
2
+
3
+ import { useSuspenseQuery } from "@tanstack/react-query";
4
+ import { Button, Card, CardContent, CardHeader, CardTitle } from "@tulip-systems/core/components";
5
+ import {
6
+ type ChartConfig,
7
+ ChartContainer,
8
+ ChartTooltip,
9
+ ChartTooltipContent,
10
+ Popover,
11
+ PopoverContent,
12
+ PopoverTrigger,
13
+ } from "@tulip-systems/core/components/client";
14
+ import { formatCurrency } from "@tulip-systems/core/lib";
15
+ import { ChevronDownIcon } from "lucide-react";
16
+ import { parseAsInteger, useQueryState } from "nuqs";
17
+ import { useState } from "react";
18
+ import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts";
19
+ import { orpc } from "@/server/router/client";
20
+ import { DateYearPicker } from "./date-year-picker.js";
21
+
22
+ const chartConfig = {
23
+ budget: {
24
+ label: "Budget",
25
+ color: "var(--chart-1)",
26
+ },
27
+ } satisfies ChartConfig;
28
+
29
+ const MONTHS = [
30
+ "January",
31
+ "February",
32
+ "March",
33
+ "April",
34
+ "May",
35
+ "June",
36
+ "July",
37
+ "August",
38
+ "September",
39
+ "October",
40
+ "November",
41
+ "December",
42
+ ];
43
+
44
+ export function BudgetBarChart() {
45
+ const [open, setOpen] = useState(false);
46
+ const [year, setYear] = useQueryState(
47
+ "year",
48
+ parseAsInteger.withDefault(new Date().getFullYear()),
49
+ );
50
+
51
+ const { data } = useSuspenseQuery(
52
+ orpc.projects.getBudgetByMonth.queryOptions({
53
+ input: { year },
54
+ select: (data) => {
55
+ if (data.length === 0) return null;
56
+
57
+ const budgetMap = new Map(data.map((item) => [item.month.trim(), item.budget]));
58
+
59
+ return MONTHS.map((month) => ({
60
+ month,
61
+ budget: budgetMap.get(month) ?? 0,
62
+ }));
63
+ },
64
+ }),
65
+ );
66
+
67
+ return (
68
+ <Card>
69
+ <CardHeader className="flex w-full flex-row flex-wrap items-center justify-between">
70
+ <CardTitle>Budget</CardTitle>
71
+
72
+ <Popover open={open} onOpenChange={setOpen}>
73
+ <PopoverTrigger asChild>
74
+ <Button variant="outline" className="w-36 justify-between font-normal">
75
+ {year ? year : "Selecteer jaar"}
76
+ <ChevronDownIcon />
77
+ </Button>
78
+ </PopoverTrigger>
79
+ <PopoverContent className="mx-10 w-auto overflow-hidden p-0" align="start">
80
+ <DateYearPicker
81
+ year={new Date(year, 0, 1)}
82
+ onYearChange={(date) => {
83
+ if (!date) return;
84
+
85
+ setYear(date.getFullYear());
86
+ }}
87
+ />
88
+ </PopoverContent>
89
+ </Popover>
90
+ </CardHeader>
91
+
92
+ <CardContent className="px-2">
93
+ <div className="h-80 w-full">
94
+ {data ? (
95
+ <ChartContainer config={chartConfig} className="h-80 w-full">
96
+ <BarChart accessibilityLayer data={data}>
97
+ <CartesianGrid vertical={false} />
98
+ <XAxis
99
+ dataKey="month"
100
+ tickLine={false}
101
+ tickMargin={10}
102
+ axisLine={false}
103
+ tickFormatter={(value) => value.slice(0, 3)}
104
+ interval={0}
105
+ angle={-45}
106
+ textAnchor="end"
107
+ height={60}
108
+ className="md:angle-0 text-xs md:text-sm"
109
+ />
110
+ <YAxis
111
+ tickLine={false}
112
+ axisLine={false}
113
+ tickFormatter={(value) => `€${(value / 1000).toFixed(0)}k`}
114
+ />
115
+ <ChartTooltip
116
+ content={
117
+ <ChartTooltipContent
118
+ indicator="dashed"
119
+ formatter={(value) => formatCurrency(Number(value))}
120
+ />
121
+ }
122
+ />
123
+ <Bar dataKey="budget" fill="var(--color-budget)" radius={4} />
124
+ </BarChart>
125
+ </ChartContainer>
126
+ ) : (
127
+ <div className="flex h-full w-full items-center justify-center">
128
+ <p className="text-muted-foreground">{year} heeft nog geen budget</p>
129
+ </div>
130
+ )}
131
+ </div>
132
+ </CardContent>
133
+ </Card>
134
+ );
135
+ }
@@ -0,0 +1,49 @@
1
+ "use client";
2
+
3
+ import { Button } from "@tulip-systems/core/components";
4
+
5
+ interface DateYearPickerProps {
6
+ year?: Date;
7
+ onYearChange: (year: Date | undefined) => void;
8
+ }
9
+
10
+ export function DateYearPicker({ year, onYearChange }: DateYearPickerProps) {
11
+ const currentYear = year?.getFullYear() || new Date().getFullYear();
12
+
13
+ const years = Array.from({ length: 21 }, (_, i) => currentYear - 10 + i);
14
+
15
+ return (
16
+ <div className="w-auto p-4">
17
+ <div className="grid grid-cols-3 gap-2 pb-5">
18
+ {years.map((y) => (
19
+ <Button
20
+ key={y}
21
+ variant={y === currentYear ? "default" : "ghost"}
22
+ size="sm"
23
+ onClick={() => {
24
+ const newDate = new Date(year || new Date());
25
+ newDate.setFullYear(y);
26
+ newDate.setHours(12, 0, 0, 0);
27
+ onYearChange(newDate);
28
+ }}
29
+ >
30
+ {y}
31
+ </Button>
32
+ ))}
33
+ </div>
34
+ <div className="flex w-full justify-center border-t p-3">
35
+ <Button
36
+ size="sm"
37
+ variant="outline"
38
+ onClick={() => {
39
+ const today = new Date();
40
+ today.setHours(12, 0, 0, 0);
41
+ onYearChange(today);
42
+ }}
43
+ >
44
+ Dit jaar
45
+ </Button>
46
+ </div>
47
+ </div>
48
+ );
49
+ }
@@ -0,0 +1,16 @@
1
+ import { Header, Topbar, TopbarTitle } from "@tulip-systems/core/components/client";
2
+ import type { PropsWithChildren } from "react";
3
+
4
+ export default function Layout(props: PropsWithChildren) {
5
+ return (
6
+ <>
7
+ <Header>
8
+ <Topbar>
9
+ <TopbarTitle breadcrumbs={[{ label: "Dashboard" }]} />
10
+ </Topbar>
11
+ </Header>
12
+
13
+ {props.children}
14
+ </>
15
+ );
16
+ }
@@ -0,0 +1,15 @@
1
+ import { Skeleton } from "@tulip-systems/core/components";
2
+
3
+ export default function Loading() {
4
+ return (
5
+ <div className="p-content space-y-5">
6
+ <div className="grid grid-rows-1 gap-5 lg:grid-cols-2 xl:grid-cols-3">
7
+ <Skeleton className="h-80" />
8
+ <Skeleton className="h-80" />
9
+ <Skeleton className="h-80" />
10
+ </div>
11
+
12
+ <Skeleton className="h-80" />
13
+ </div>
14
+ );
15
+ }
@@ -0,0 +1,12 @@
1
+ import { Allowed } from "@tulip-systems/core/auth/client";
2
+ import { BudgetBarChart } from "./_components/budget-bar-chart.client.js";
3
+
4
+ export default function Page() {
5
+ return (
6
+ <div className="space-y-5 p-content">
7
+ <Allowed permission={{ dashboard: ["view.budget"] }}>
8
+ <BudgetBarChart />
9
+ </Allowed>
10
+ </div>
11
+ );
12
+ }
@@ -0,0 +1,16 @@
1
+ import { DRIVE_NAMESPACES } from "@/server/storage/config";
2
+ import { DriveContextProvider } from "../_components/drive-context.client.js";
3
+ import { DriveHeader } from "../_components/drive-header.client.js";
4
+ import { DriveContent } from "../_components/drive-view.client.js";
5
+
6
+ const { namespace, permission } = DRIVE_NAMESPACES.global;
7
+
8
+ export default function Layout(props: LayoutProps<"/admin/drive">) {
9
+ return (
10
+ <DriveContextProvider namespace={namespace} permission={permission}>
11
+ <DriveHeader namespace={namespace} />
12
+
13
+ <DriveContent>{props.children}</DriveContent>
14
+ </DriveContextProvider>
15
+ );
16
+ }
@@ -0,0 +1,5 @@
1
+ import { DriveGridLoading } from "../_components/grid.client.js";
2
+
3
+ export default function Loading() {
4
+ return <DriveGridLoading className="p-content" />;
5
+ }
@@ -0,0 +1,3 @@
1
+ import Page from "../[namespace]/page.js";
2
+
3
+ export default Page;
@@ -0,0 +1,21 @@
1
+ import type { PropsWithChildren } from "react";
2
+ import { DRIVE_NAMESPACES } from "@/server/storage/config";
3
+ import { DriveContextProvider } from "../_components/drive-context.client.js";
4
+ import { DriveHeader } from "../_components/drive-header.client.js";
5
+ import { DriveContent } from "../_components/drive-view.client.js";
6
+
7
+ export default async function Layout(
8
+ props: PropsWithChildren<{ params: Promise<{ namespace: string }> }>,
9
+ ) {
10
+ const params = await props.params;
11
+ const { namespace, permission } =
12
+ DRIVE_NAMESPACES[params.namespace as keyof typeof DRIVE_NAMESPACES];
13
+
14
+ return (
15
+ <DriveContextProvider namespace={namespace} permission={permission}>
16
+ <DriveHeader namespace={namespace} />
17
+
18
+ <DriveContent>{props.children}</DriveContent>
19
+ </DriveContextProvider>
20
+ );
21
+ }
@@ -0,0 +1,5 @@
1
+ import { DriveGridLoading } from "../_components/grid.client.js";
2
+
3
+ export default function Loading() {
4
+ return <DriveGridLoading className="p-content" />;
5
+ }
@@ -0,0 +1,5 @@
1
+ import { DriveView } from "../_components/drive-view.client.js";
2
+
3
+ export default async function DriveFolderPage() {
4
+ return <DriveView className="p-content" />;
5
+ }
@@ -0,0 +1,104 @@
1
+ "use client";
2
+
3
+ import { zodResolver } from "@hookform/resolvers/zod";
4
+ import type { DefaultError, MutationOptions } from "@tanstack/react-query";
5
+ import {
6
+ CommandFormDialog,
7
+ CommandFormDialogCancel,
8
+ CommandFormDialogContent,
9
+ CommandFormDialogFields,
10
+ CommandFormDialogFooter,
11
+ CommandFormDialogHeader,
12
+ CommandFormDialogSubmit,
13
+ CommandFormDialogTitle,
14
+ CommandFormDialogTrigger,
15
+ } from "@tulip-systems/core/commands/client";
16
+ import { Input } from "@tulip-systems/core/components";
17
+ import {
18
+ Form,
19
+ FormControl,
20
+ FormField,
21
+ FormItem,
22
+ FormLabel,
23
+ FormMessage,
24
+ } from "@tulip-systems/core/components/client";
25
+ import {
26
+ type CreateNodeInput,
27
+ type CreateNodeSchema,
28
+ createNodeSchema,
29
+ } from "@tulip-systems/core/storage";
30
+ import { useQueryStates } from "nuqs";
31
+ import type { PropsWithChildren } from "react";
32
+ import { useForm, useFormContext } from "react-hook-form";
33
+ import { driveSearchParams } from "../_lib/search-params.js";
34
+
35
+ /**
36
+ * Create folder command content
37
+ */
38
+ export type NodeDialogCommandContentProps<
39
+ TData = unknown,
40
+ TError = DefaultError,
41
+ TVariables = undefined,
42
+ TOnMutateResult = unknown,
43
+ > = PropsWithChildren<{
44
+ variables: (values: CreateNodeSchema) => TVariables;
45
+ mutation: MutationOptions<TData, TError, TVariables, TOnMutateResult>;
46
+ defaultValues?: Partial<CreateNodeSchema>;
47
+ message?: string;
48
+ }>;
49
+
50
+ export function NodeDialogCommandContent<
51
+ TData = unknown,
52
+ TError = DefaultError,
53
+ TVariables = undefined,
54
+ TOnMutateResult = unknown,
55
+ >(props: NodeDialogCommandContentProps<TData, TError, TVariables, TOnMutateResult>) {
56
+ const [{ parentId }] = useQueryStates(driveSearchParams);
57
+
58
+ const form = useForm<CreateNodeInput>({
59
+ resolver: zodResolver(createNodeSchema),
60
+ values: { ...props.defaultValues, parentId } as CreateNodeSchema,
61
+ resetOptions: { keepDirtyValues: true },
62
+ });
63
+
64
+ return (
65
+ <Form {...form}>
66
+ <CommandFormDialogContent variables={props.variables} mutation={props.mutation}>
67
+ {props.children}
68
+ </CommandFormDialogContent>
69
+ </Form>
70
+ );
71
+ }
72
+
73
+ /**
74
+ * Create folder command fields
75
+ */
76
+ export function NodeDialogCommandFields() {
77
+ const form = useFormContext<CreateNodeSchema>();
78
+
79
+ return (
80
+ <CommandFormDialogFields>
81
+ <FormField
82
+ control={form.control}
83
+ name="name"
84
+ render={({ field }) => (
85
+ <FormItem>
86
+ <FormLabel>Naam</FormLabel>
87
+ <FormControl>
88
+ <Input {...field} />
89
+ </FormControl>
90
+ <FormMessage />
91
+ </FormItem>
92
+ )}
93
+ />
94
+ </CommandFormDialogFields>
95
+ );
96
+ }
97
+
98
+ export const NodeDialogCommand = CommandFormDialog;
99
+ export const NodeDialogCommandTrigger = CommandFormDialogTrigger;
100
+ export const NodeDialogCommandHeader = CommandFormDialogHeader;
101
+ export const NodeDialogCommandTitle = CommandFormDialogTitle;
102
+ export const NodeDialogCommandFooter = CommandFormDialogFooter;
103
+ export const NodeDialogCommandCancel = CommandFormDialogCancel;
104
+ export const NodeDialogCommandSubmit = CommandFormDialogSubmit;
@@ -0,0 +1,31 @@
1
+ "use client";
2
+
3
+ import { CommandEmpty, CommandLabel } from "@tulip-systems/core/commands/client";
4
+ import { Input } from "@tulip-systems/core/components";
5
+ import { cn } from "@tulip-systems/core/lib";
6
+ import { useUploadZone } from "@tulip-systems/core/storage/client";
7
+ import { UploadIcon } from "lucide-react";
8
+ import type { ComponentProps } from "react";
9
+
10
+ export function UploadCommand({ className, ...props }: ComponentProps<"div">) {
11
+ const { onUpload } = useUploadZone();
12
+
13
+ console.log("UploadCommand");
14
+
15
+ return (
16
+ <CommandEmpty {...props} className={cn("relative gap-2", className)} label="Upload">
17
+ <UploadIcon className="h-3 w-3" />
18
+ <CommandLabel />
19
+
20
+ <Input
21
+ type="file"
22
+ className="absolute inset-0 z-10 h-full w-full cursor-pointer opacity-0"
23
+ multiple
24
+ onChange={(e) => {
25
+ if (!e.target.files) return;
26
+ Array.from(e.target.files).forEach(onUpload);
27
+ }}
28
+ />
29
+ </CommandEmpty>
30
+ );
31
+ }
@@ -0,0 +1,175 @@
1
+ "use client";
2
+
3
+ import { DragDropProvider } from "@dnd-kit/react";
4
+ import { useQueryClient } from "@tanstack/react-query";
5
+ import type { Permission } from "@tulip-systems/core/auth";
6
+ import { Allowed } from "@tulip-systems/core/auth/client";
7
+ import { tableSearchParams } from "@tulip-systems/core/data-tables";
8
+ import { cn } from "@tulip-systems/core/lib";
9
+ import { useAction } from "@tulip-systems/core/lib/client";
10
+ import type { Node, NodesTableFilters } from "@tulip-systems/core/storage";
11
+ import { UploadZone, type UploadZoneProps } from "@tulip-systems/core/storage/client";
12
+ import { useQueryStates } from "nuqs";
13
+ import { type ComponentProps, createContext, use, useDeferredValue } from "react";
14
+ import { orpc } from "@/server/router/client";
15
+ import { uploadClient } from "@/server/storage/client";
16
+ import { driveFilterSearchParams, driveSearchParams } from "../_lib/search-params.js";
17
+
18
+ /**
19
+ * DriveContext
20
+ */
21
+ export type DriveContextValue = {
22
+ namespace: string;
23
+ where?: Omit<NodesTableFilters, "namespace" | "parentId">;
24
+ onUploadCompleted?: (node: Node) => Promise<unknown> | unknown;
25
+ permission?: Permission;
26
+ };
27
+ export const DriveContext = createContext({} as DriveContextValue);
28
+
29
+ /**
30
+ * useDriveContext
31
+ */
32
+ export function useDriveContext() {
33
+ const context = use(DriveContext);
34
+ if (!context) throw new Error("DriveContext not found!");
35
+ return context;
36
+ }
37
+
38
+ /**
39
+ * DriveContextProvider
40
+ */
41
+ export function DriveContextProvider({
42
+ namespace,
43
+ where,
44
+ uploadHooks,
45
+ onUploadCompleted,
46
+ onUploadFailed,
47
+ permission = { drive: ["view"] },
48
+ className,
49
+ children,
50
+ ...props
51
+ }: ComponentProps<"div"> &
52
+ DriveContextValue &
53
+ Pick<UploadZoneProps, "uploadHooks" | "onUploadCompleted" | "onUploadFailed">) {
54
+ const queryClient = useQueryClient();
55
+
56
+ const [{ parentId }] = useQueryStates(driveSearchParams);
57
+ const [filters] = useQueryStates(driveFilterSearchParams);
58
+
59
+ const [query] = useQueryStates(tableSearchParams);
60
+ const input = useDeferredValue({
61
+ ...query,
62
+ filters: { ...filters, ...where, parentId, namespace },
63
+ });
64
+ const queryKey = orpc.drive.list.infiniteKey({
65
+ initialPageParam: 0,
66
+ input: (cursor: number | undefined) => ({ ...input, cursor }),
67
+ });
68
+
69
+ const changeParent = useAction(
70
+ orpc.drive.changeParent.mutationOptions({
71
+ onMutate: async (data) => {
72
+ // Cancel any outgoing refetches
73
+ await queryClient.cancelQueries({ queryKey });
74
+
75
+ // Snapshot the previous value
76
+ const previousNodes = queryClient.getQueryData(queryKey);
77
+
78
+ // Remove the deleted node
79
+ queryClient.setQueryData(queryKey, (old) => {
80
+ if (!old) return old;
81
+
82
+ return {
83
+ ...old,
84
+ pages: old.pages.map((page) => ({
85
+ ...page,
86
+ data: page.data.filter(({ id }) => id !== data.id),
87
+ })),
88
+ };
89
+ });
90
+
91
+ return { previousNodes };
92
+ },
93
+ onError: (err, _, context) => {
94
+ console.error("changeParent error: ", err);
95
+ queryClient.setQueryData(queryKey, context?.previousNodes);
96
+ },
97
+ onSettled: () => {
98
+ queryClient.invalidateQueries({ queryKey });
99
+ },
100
+ }),
101
+ );
102
+
103
+ return (
104
+ <Allowed permission={permission}>
105
+ <DriveContext.Provider value={{ namespace, where, onUploadCompleted }}>
106
+ <DragDropProvider
107
+ onDragEnd={(event) => {
108
+ const { operation, canceled } = event;
109
+ const { source, target } = operation;
110
+
111
+ if (!source || !target || canceled) return;
112
+
113
+ const id = source.id.toString();
114
+ const parentId = target.id.toString() === "drive" ? null : target.id.toString();
115
+
116
+ if (id === parentId) return;
117
+
118
+ changeParent.mutate({ id, parentId });
119
+ }}
120
+ >
121
+ <UploadZone
122
+ {...props}
123
+ className={cn("min-h-dvh", className)}
124
+ variables={{ namespace, parentId }}
125
+ uploadClient={uploadClient}
126
+ uploadHooks={uploadHooks}
127
+ onUploadCompleted={onUploadCompleted}
128
+ onUploadFailed={onUploadFailed}
129
+ optimistic={{
130
+ add: (node) => {
131
+ queryClient.setQueryData(queryKey, (old) => {
132
+ if (!old) return old;
133
+
134
+ const firstPage = old.pages[0];
135
+ if (!firstPage) return old;
136
+
137
+ /**
138
+ * Add the new node to the first page as the first item
139
+ */
140
+ return {
141
+ ...old,
142
+ pages: [
143
+ {
144
+ ...firstPage,
145
+ data: [node, ...(firstPage.data ?? [])],
146
+ },
147
+ ...old.pages.slice(1),
148
+ ],
149
+ };
150
+ });
151
+ },
152
+ remove: (ids) => {
153
+ queryClient.setQueryData(queryKey, (old) => {
154
+ if (!old) return old;
155
+
156
+ return {
157
+ ...old,
158
+ pages: old.pages.map((page) => ({
159
+ ...page,
160
+ data: page.data.filter(({ id }) => !ids.includes(id)),
161
+ })),
162
+ };
163
+ });
164
+ },
165
+ cancel: () => queryClient.cancelQueries({ queryKey }),
166
+ invalidate: () => queryClient.invalidateQueries({ queryKey }),
167
+ }}
168
+ >
169
+ {children}
170
+ </UploadZone>
171
+ </DragDropProvider>
172
+ </DriveContext.Provider>
173
+ </Allowed>
174
+ );
175
+ }