@tulip-systems/create-app 0.12.3 → 0.12.4
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/templates/basic/package.json +5 -5
- package/dist/templates/code/package.json +16 -14
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-black-italic.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-black.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-bold-italic.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-bold.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-extra-light-italic.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-extra-light.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-light-italic.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-light.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-medium-italic.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-medium.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-regular-italic.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-regular.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-thin-italic.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-thin.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-ultra-thin-italic.otf +0 -0
- package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-ultra-thin.otf +0 -0
- package/dist/templates/code/public/images/preview/hero-home.webp +0 -0
- package/dist/templates/code/src/app/admin/quotations/(list)/layout.tsx +24 -0
- package/dist/templates/code/src/app/admin/quotations/(list)/page.tsx +23 -0
- package/dist/templates/code/src/app/admin/quotations/[id]/(dashboard)/layout.tsx +40 -0
- package/dist/templates/code/src/app/admin/quotations/[id]/(dashboard)/page.tsx +56 -0
- package/dist/templates/code/src/app/admin/quotations/[id]/preview/page.tsx +17 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/commands.tsx +24 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/hero/config.ts +14 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/hero/create.tsx +152 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/hero/inline.tsx +90 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/hero/preview.tsx +90 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/registry.ts +13 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/render.tsx +190 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/styles.ts +24 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/summary/config.ts +13 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/summary/create.tsx +175 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/summary/inline.tsx +93 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/summary/preview.tsx +25 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/totals/config.ts +18 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/totals/create.tsx +177 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/totals/inline.tsx +96 -0
- package/dist/templates/code/src/app/admin/quotations/_blocks/totals/preview.tsx +32 -0
- package/dist/templates/code/src/app/admin/quotations/_components/create-command.tsx +97 -0
- package/dist/templates/code/src/app/admin/quotations/_components/tables.tsx +68 -0
- package/dist/templates/code/src/app/admin/quotations/_components/toolbars.tsx +18 -0
- package/dist/templates/code/src/app/admin/quotations/_config/columns-data.tsx +35 -0
- package/dist/templates/code/src/app/admin/quotations/_config/commands.tsx +71 -0
- package/dist/templates/code/src/app/admin/quotations/_config/filters.ts +22 -0
- package/dist/templates/code/src/app/admin/quotations/_config/sorting.ts +23 -0
- package/dist/templates/code/src/app/admin/quotations/_lib/router.ts +260 -0
- package/dist/templates/code/src/app/admin/quotations/_lib/schema.ts +42 -0
- package/dist/templates/code/src/app/admin/quotations/_lib/validators.ts +20 -0
- package/dist/templates/code/src/lib/config/paths.tsx +8 -0
- package/dist/templates/code/src/lib/utils/fonts.ts +102 -0
- package/dist/templates/code/src/server/auth/permissions.ts +4 -0
- package/dist/templates/code/src/server/db/schema.ts +1 -0
- package/dist/templates/code/src/server/router/router.ts +10 -0
- package/dist/templates/spark/package.json +13 -13
- package/package.json +1 -1
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
"@orpc/tanstack-query": "^1.14.15",
|
|
26
26
|
"@tailwindcss/typography": "^0.5.20",
|
|
27
27
|
"@tanstack/react-query": "^5.101.4",
|
|
28
|
-
"@tulip-systems/app": "0.12.
|
|
29
|
-
"@tulip-systems/auth": "0.12.
|
|
30
|
-
"@tulip-systems/commands": "0.12.
|
|
31
|
-
"@tulip-systems/core": "0.12.
|
|
32
|
-
"@tulip-systems/ui": "0.12.
|
|
28
|
+
"@tulip-systems/app": "0.12.4",
|
|
29
|
+
"@tulip-systems/auth": "0.12.4",
|
|
30
|
+
"@tulip-systems/commands": "0.12.4",
|
|
31
|
+
"@tulip-systems/core": "0.12.4",
|
|
32
|
+
"@tulip-systems/ui": "0.12.4",
|
|
33
33
|
"@vercel/functions": "^3.9.0",
|
|
34
34
|
"better-auth": "^1.6.26",
|
|
35
35
|
"class-variance-authority": "^0.7.1",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"@dnd-kit/helpers": "^0.5.0",
|
|
31
31
|
"@dnd-kit/modifiers": "^9.0.0",
|
|
32
32
|
"@dnd-kit/react": "^0.5.0",
|
|
33
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
33
34
|
"@hookform/resolvers": "^5.7.1",
|
|
34
35
|
"@js-temporal/polyfill": "^0.5.1",
|
|
35
36
|
"@modelcontextprotocol/server": "^2.0.0",
|
|
@@ -66,20 +67,21 @@
|
|
|
66
67
|
"@tiptap/extension-text-style": "^3.29.2",
|
|
67
68
|
"@tiptap/extension-underline": "^3.29.2",
|
|
68
69
|
"@tiptap/extensions": "^3.29.2",
|
|
69
|
-
"@tulip-systems/ai": "0.12.
|
|
70
|
-
"@tulip-systems/app": "0.12.
|
|
71
|
-
"@tulip-systems/auth": "0.12.
|
|
72
|
-
"@tulip-systems/
|
|
73
|
-
"@tulip-systems/
|
|
74
|
-
"@tulip-systems/
|
|
75
|
-
"@tulip-systems/
|
|
76
|
-
"@tulip-systems/drive
|
|
77
|
-
"@tulip-systems/
|
|
78
|
-
"@tulip-systems/editor
|
|
79
|
-
"@tulip-systems/
|
|
80
|
-
"@tulip-systems/
|
|
81
|
-
"@tulip-systems/
|
|
82
|
-
"@tulip-systems/
|
|
70
|
+
"@tulip-systems/ai": "0.12.4",
|
|
71
|
+
"@tulip-systems/app": "0.12.4",
|
|
72
|
+
"@tulip-systems/auth": "0.12.4",
|
|
73
|
+
"@tulip-systems/blocks": "0.12.4",
|
|
74
|
+
"@tulip-systems/commands": "0.12.4",
|
|
75
|
+
"@tulip-systems/core": "0.12.4",
|
|
76
|
+
"@tulip-systems/data-tables": "0.12.4",
|
|
77
|
+
"@tulip-systems/drive": "0.12.4",
|
|
78
|
+
"@tulip-systems/drive-google": "0.12.4",
|
|
79
|
+
"@tulip-systems/editor": "0.12.4",
|
|
80
|
+
"@tulip-systems/editor-storage": "0.12.4",
|
|
81
|
+
"@tulip-systems/inline": "0.12.4",
|
|
82
|
+
"@tulip-systems/query": "0.12.4",
|
|
83
|
+
"@tulip-systems/storage": "0.12.4",
|
|
84
|
+
"@tulip-systems/ui": "0.12.4",
|
|
83
85
|
"@uidotdev/usehooks": "^2.4.1",
|
|
84
86
|
"@vercel/functions": "^3.9.0",
|
|
85
87
|
"@vercel/otel": "^2.1.3",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-extra-light-italic.otf
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-medium-italic.otf
ADDED
|
Binary file
|
|
Binary file
|
package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-regular-italic.otf
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/templates/code/public/fonts/neue-haas-grotesk/neue-haas-grotesk-ultra-thin-italic.otf
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Header, Topbar, TopbarTitle, TopbarTools } from "@tulip-systems/app/components/client";
|
|
2
|
+
import { InlineCommandMenu, InlineCommandMenuLoading } from "@tulip-systems/commands/client";
|
|
3
|
+
import { type PropsWithChildren, Suspense } from "react";
|
|
4
|
+
import { quotationCreateCommand } from "../_config/commands";
|
|
5
|
+
|
|
6
|
+
export default function QuotationsLayout({ children }: PropsWithChildren) {
|
|
7
|
+
return (
|
|
8
|
+
<>
|
|
9
|
+
<Header>
|
|
10
|
+
<Topbar>
|
|
11
|
+
<TopbarTitle breadcrumbs={[{ label: "Offertes" }]} />
|
|
12
|
+
|
|
13
|
+
<TopbarTools>
|
|
14
|
+
<Suspense fallback={<InlineCommandMenuLoading />}>
|
|
15
|
+
<InlineCommandMenu data={{}} commands={[quotationCreateCommand]} />
|
|
16
|
+
</Suspense>
|
|
17
|
+
</TopbarTools>
|
|
18
|
+
</Topbar>
|
|
19
|
+
</Header>
|
|
20
|
+
|
|
21
|
+
{children}
|
|
22
|
+
</>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Allowed } from "@tulip-systems/auth/permissions/client";
|
|
2
|
+
import { TableLayout } from "@tulip-systems/data-tables";
|
|
3
|
+
import { DataTable } from "@tulip-systems/data-tables/client";
|
|
4
|
+
import { QuotationPaginationTableProvider } from "../_components/tables";
|
|
5
|
+
import { QuotationTableToolbar } from "../_components/toolbars";
|
|
6
|
+
import { quotationColumns } from "../_config/columns-data";
|
|
7
|
+
import { quotationDeleteCommand } from "../_config/commands";
|
|
8
|
+
|
|
9
|
+
export default function QuotationsPage() {
|
|
10
|
+
return (
|
|
11
|
+
<Allowed permission={{ quotation: ["view"] }}>
|
|
12
|
+
<TableLayout className="p-content">
|
|
13
|
+
<QuotationPaginationTableProvider
|
|
14
|
+
columns={quotationColumns}
|
|
15
|
+
commands={[quotationDeleteCommand]}
|
|
16
|
+
>
|
|
17
|
+
<QuotationTableToolbar />
|
|
18
|
+
<DataTable />
|
|
19
|
+
</QuotationPaginationTableProvider>
|
|
20
|
+
</TableLayout>
|
|
21
|
+
</Allowed>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useSuspenseQuery } from "@tanstack/react-query";
|
|
4
|
+
import { Header, Topbar, TopbarTitle, TopbarTools } from "@tulip-systems/app/components/client";
|
|
5
|
+
import { InlineCommandMenu, InlineCommandMenuLoading } from "@tulip-systems/commands/client";
|
|
6
|
+
import { notFound } from "next/navigation";
|
|
7
|
+
import { Suspense, use } from "react";
|
|
8
|
+
import { orpc } from "@/server/router/client";
|
|
9
|
+
import { quotationDeleteCommand, quotationPreviewCommand } from "../../_config/commands";
|
|
10
|
+
|
|
11
|
+
export default function QuotationsLayout(props: LayoutProps<"/admin/quotations/[id]">) {
|
|
12
|
+
const params = use(props.params);
|
|
13
|
+
const id = params.id?.toString();
|
|
14
|
+
|
|
15
|
+
const { data } = useSuspenseQuery(orpc.quotations.getById.queryOptions({ input: { id } }));
|
|
16
|
+
if (!data) notFound();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
<Header>
|
|
21
|
+
<Topbar>
|
|
22
|
+
<TopbarTitle
|
|
23
|
+
breadcrumbs={[{ label: "Offertes", href: "/admin/quotations" }, { label: data.title }]}
|
|
24
|
+
/>
|
|
25
|
+
|
|
26
|
+
<TopbarTools>
|
|
27
|
+
<Suspense fallback={<InlineCommandMenuLoading />}>
|
|
28
|
+
<InlineCommandMenu
|
|
29
|
+
data={data}
|
|
30
|
+
commands={[quotationPreviewCommand, quotationDeleteCommand]}
|
|
31
|
+
/>
|
|
32
|
+
</Suspense>
|
|
33
|
+
</TopbarTools>
|
|
34
|
+
</Topbar>
|
|
35
|
+
</Header>
|
|
36
|
+
|
|
37
|
+
{props.children}
|
|
38
|
+
</>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useSuspenseQuery } from "@tanstack/react-query";
|
|
4
|
+
import { Allowed } from "@tulip-systems/auth/permissions/client";
|
|
5
|
+
import {
|
|
6
|
+
InlineField,
|
|
7
|
+
InlineFieldError,
|
|
8
|
+
InlineFieldLabel,
|
|
9
|
+
InlineStringInput,
|
|
10
|
+
} from "@tulip-systems/inline/client";
|
|
11
|
+
import { DataStackLayout } from "@tulip-systems/ui/data-stack";
|
|
12
|
+
import { notFound } from "next/navigation";
|
|
13
|
+
import { use } from "react";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import { orpc } from "@/server/router/client";
|
|
16
|
+
import { CreateQuotationBlock, RenderQuotationInlineBlocks } from "../../_blocks/render";
|
|
17
|
+
|
|
18
|
+
const quotationMutation = orpc.quotations.update.mutationOptions({
|
|
19
|
+
meta: { invalidates: [orpc.quotations.key()] },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export default function QuotationDetailPage(props: PageProps<"/admin/quotations/[id]">) {
|
|
23
|
+
const params = use(props.params);
|
|
24
|
+
const id = params.id?.toString();
|
|
25
|
+
|
|
26
|
+
const { data } = useSuspenseQuery(orpc.quotations.getById.queryOptions({ input: { id } }));
|
|
27
|
+
if (!data) notFound();
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Allowed permission={{ quotation: ["view"] }}>
|
|
31
|
+
<DataStackLayout className="p-content">
|
|
32
|
+
<InlineField>
|
|
33
|
+
<InlineFieldLabel>Titel</InlineFieldLabel>
|
|
34
|
+
<InlineStringInput
|
|
35
|
+
initialValue={data.title}
|
|
36
|
+
variables={(value) => ({ id, data: { title: value ?? "" } })}
|
|
37
|
+
mutation={quotationMutation}
|
|
38
|
+
strategy={{ mutate: { on: "blur" }, validate: { blur: z.string().min(1) } }}
|
|
39
|
+
/>
|
|
40
|
+
<InlineFieldError />
|
|
41
|
+
</InlineField>
|
|
42
|
+
|
|
43
|
+
<InlineField>
|
|
44
|
+
<InlineFieldLabel>Blokken</InlineFieldLabel>
|
|
45
|
+
|
|
46
|
+
<div className="flex flex-col gap-2">
|
|
47
|
+
<RenderQuotationInlineBlocks quotationId={id} />
|
|
48
|
+
<CreateQuotationBlock quotationId={id} />
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<InlineFieldError />
|
|
52
|
+
</InlineField>
|
|
53
|
+
</DataStackLayout>
|
|
54
|
+
</Allowed>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useSuspenseQuery } from "@tanstack/react-query";
|
|
4
|
+
import { notFound } from "next/navigation";
|
|
5
|
+
import { use } from "react";
|
|
6
|
+
import { orpc } from "@/server/router/client";
|
|
7
|
+
import { RenderQuotationPreview } from "../../_blocks/render";
|
|
8
|
+
|
|
9
|
+
export default function QuotationPreviewPage(props: PageProps<"/admin/quotations/[id]/preview">) {
|
|
10
|
+
const params = use(props.params);
|
|
11
|
+
const id = params.id?.toString();
|
|
12
|
+
|
|
13
|
+
const { data } = useSuspenseQuery(orpc.quotations.getById.queryOptions({ input: { id } }));
|
|
14
|
+
if (!data) notFound();
|
|
15
|
+
|
|
16
|
+
return <RenderQuotationPreview quotationId={id} />;
|
|
17
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { commandBuilder } from "@tulip-systems/commands";
|
|
4
|
+
import { DeleteCommand } from "@tulip-systems/commands/client";
|
|
5
|
+
import { toast } from "@tulip-systems/ui/sonner";
|
|
6
|
+
import { orpc } from "@/server/router/client";
|
|
7
|
+
import type { QuotationBlock } from "./registry";
|
|
8
|
+
|
|
9
|
+
type QuotationBlockCommandItem = Pick<QuotationBlock, "id">;
|
|
10
|
+
|
|
11
|
+
export const quotationBlockDeleteCommand = commandBuilder
|
|
12
|
+
.$type<QuotationBlockCommandItem>()
|
|
13
|
+
.permission({ quotation: ["update"] })
|
|
14
|
+
.render(({ data }) => (
|
|
15
|
+
<DeleteCommand
|
|
16
|
+
variables={{ id: data.id }}
|
|
17
|
+
mutation={orpc.quotationBlocks.delete.mutationOptions({
|
|
18
|
+
meta: { invalidates: [orpc.quotationBlocks.key()] },
|
|
19
|
+
onSuccess: () => toast.success("Blok succesvol verwijderd"),
|
|
20
|
+
})}
|
|
21
|
+
/>
|
|
22
|
+
));
|
|
23
|
+
|
|
24
|
+
export const quotationInlineBlockCommands = [quotationBlockDeleteCommand];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BlockDefinition } from "@tulip-systems/blocks";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const quotationHeroBlock = {
|
|
5
|
+
slug: "hero",
|
|
6
|
+
label: "Hero",
|
|
7
|
+
description: "Homepage hero title, subtitle, and supporting copy.",
|
|
8
|
+
schema: z.object({
|
|
9
|
+
media: z.unknown().optional(),
|
|
10
|
+
titlePart1: z.string().min(1),
|
|
11
|
+
titlePart2: z.string().optional(),
|
|
12
|
+
description: z.string().optional(),
|
|
13
|
+
}),
|
|
14
|
+
} as const satisfies BlockDefinition;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
|
|
4
|
+
import type { DefaultError } from "@tanstack/react-query";
|
|
5
|
+
import type { BlockCreateComponentProps } from "@tulip-systems/blocks";
|
|
6
|
+
import {
|
|
7
|
+
CreateBlock,
|
|
8
|
+
CreateBlockCancel,
|
|
9
|
+
CreateBlockContent,
|
|
10
|
+
CreateBlockDescription,
|
|
11
|
+
CreateBlockFields,
|
|
12
|
+
CreateBlockFooter,
|
|
13
|
+
CreateBlockHeader,
|
|
14
|
+
CreateBlockSubmit,
|
|
15
|
+
CreateBlockTitle,
|
|
16
|
+
CreateBlockTrigger,
|
|
17
|
+
CreateBlockTriggerContent,
|
|
18
|
+
CreateBlockTriggerDescription,
|
|
19
|
+
CreateBlockTriggerIcon,
|
|
20
|
+
CreateBlockTriggerTitle,
|
|
21
|
+
} from "@tulip-systems/blocks/client";
|
|
22
|
+
import { Input } from "@tulip-systems/ui/input";
|
|
23
|
+
import {
|
|
24
|
+
Form,
|
|
25
|
+
FormControl,
|
|
26
|
+
FormField,
|
|
27
|
+
FormItem,
|
|
28
|
+
FormLabel,
|
|
29
|
+
FormMessage,
|
|
30
|
+
} from "@tulip-systems/ui/react-hook-form";
|
|
31
|
+
import { Textarea } from "@tulip-systems/ui/textarea";
|
|
32
|
+
import { BlocksIcon } from "lucide-react";
|
|
33
|
+
import { useForm } from "react-hook-form";
|
|
34
|
+
import type { z } from "zod";
|
|
35
|
+
import { quotationHeroBlock } from "./config";
|
|
36
|
+
|
|
37
|
+
export type QuotationHeroCreateValues = z.input<typeof quotationHeroBlock.schema>;
|
|
38
|
+
|
|
39
|
+
type QuotationHeroCreateBlockInput = {
|
|
40
|
+
slug: typeof quotationHeroBlock.slug;
|
|
41
|
+
data: QuotationHeroCreateValues;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type QuotationHeroCreateBlockProps<
|
|
45
|
+
TData = unknown,
|
|
46
|
+
TError = DefaultError,
|
|
47
|
+
TVariables = undefined,
|
|
48
|
+
TOnMutateResult = unknown,
|
|
49
|
+
> = BlockCreateComponentProps<
|
|
50
|
+
QuotationHeroCreateBlockInput,
|
|
51
|
+
TData,
|
|
52
|
+
TError,
|
|
53
|
+
TVariables,
|
|
54
|
+
TOnMutateResult
|
|
55
|
+
>;
|
|
56
|
+
|
|
57
|
+
export function QuotationHeroCreateBlock<
|
|
58
|
+
TData = unknown,
|
|
59
|
+
TError = DefaultError,
|
|
60
|
+
TVariables = undefined,
|
|
61
|
+
TOnMutateResult = unknown,
|
|
62
|
+
>({
|
|
63
|
+
variables,
|
|
64
|
+
mutation,
|
|
65
|
+
}: QuotationHeroCreateBlockProps<TData, TError, TVariables, TOnMutateResult>) {
|
|
66
|
+
const form = useForm<QuotationHeroCreateValues>({
|
|
67
|
+
resolver: standardSchemaResolver(quotationHeroBlock.schema),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<CreateBlock>
|
|
72
|
+
<CreateBlockTrigger>
|
|
73
|
+
<CreateBlockTriggerIcon>
|
|
74
|
+
<BlocksIcon />
|
|
75
|
+
</CreateBlockTriggerIcon>
|
|
76
|
+
|
|
77
|
+
<CreateBlockTriggerContent>
|
|
78
|
+
<CreateBlockTriggerTitle>{quotationHeroBlock.label}</CreateBlockTriggerTitle>
|
|
79
|
+
<CreateBlockTriggerDescription>
|
|
80
|
+
{quotationHeroBlock.description}
|
|
81
|
+
</CreateBlockTriggerDescription>
|
|
82
|
+
</CreateBlockTriggerContent>
|
|
83
|
+
</CreateBlockTrigger>
|
|
84
|
+
|
|
85
|
+
<Form {...form}>
|
|
86
|
+
<CreateBlockContent
|
|
87
|
+
className="sm:max-w-xl"
|
|
88
|
+
variables={(data: QuotationHeroCreateValues) =>
|
|
89
|
+
variables({ slug: quotationHeroBlock.slug, data })
|
|
90
|
+
}
|
|
91
|
+
mutation={mutation}
|
|
92
|
+
>
|
|
93
|
+
<CreateBlockHeader>
|
|
94
|
+
<CreateBlockTitle>{quotationHeroBlock.label} toevoegen</CreateBlockTitle>
|
|
95
|
+
<CreateBlockDescription>
|
|
96
|
+
Vul de verplichte velden in om het blok toe te voegen.
|
|
97
|
+
</CreateBlockDescription>
|
|
98
|
+
</CreateBlockHeader>
|
|
99
|
+
|
|
100
|
+
<CreateBlockFields className="grid gap-4 md:grid-cols-2">
|
|
101
|
+
<FormField
|
|
102
|
+
control={form.control}
|
|
103
|
+
name="titlePart1"
|
|
104
|
+
render={({ field }) => (
|
|
105
|
+
<FormItem>
|
|
106
|
+
<FormLabel>Titel deel 1</FormLabel>
|
|
107
|
+
<FormControl>
|
|
108
|
+
<Input {...field} />
|
|
109
|
+
</FormControl>
|
|
110
|
+
<FormMessage />
|
|
111
|
+
</FormItem>
|
|
112
|
+
)}
|
|
113
|
+
/>
|
|
114
|
+
|
|
115
|
+
<FormField
|
|
116
|
+
control={form.control}
|
|
117
|
+
name="titlePart2"
|
|
118
|
+
render={({ field }) => (
|
|
119
|
+
<FormItem>
|
|
120
|
+
<FormLabel>Titel deel 2</FormLabel>
|
|
121
|
+
<FormControl>
|
|
122
|
+
<Input {...field} />
|
|
123
|
+
</FormControl>
|
|
124
|
+
<FormMessage />
|
|
125
|
+
</FormItem>
|
|
126
|
+
)}
|
|
127
|
+
/>
|
|
128
|
+
|
|
129
|
+
<FormField
|
|
130
|
+
control={form.control}
|
|
131
|
+
name="description"
|
|
132
|
+
render={({ field }) => (
|
|
133
|
+
<FormItem className="md:col-span-2">
|
|
134
|
+
<FormLabel>Beschrijving</FormLabel>
|
|
135
|
+
<FormControl>
|
|
136
|
+
<Textarea {...field} />
|
|
137
|
+
</FormControl>
|
|
138
|
+
<FormMessage />
|
|
139
|
+
</FormItem>
|
|
140
|
+
)}
|
|
141
|
+
/>
|
|
142
|
+
</CreateBlockFields>
|
|
143
|
+
|
|
144
|
+
<CreateBlockFooter>
|
|
145
|
+
<CreateBlockCancel>Annuleren</CreateBlockCancel>
|
|
146
|
+
<CreateBlockSubmit>Blok toevoegen</CreateBlockSubmit>
|
|
147
|
+
</CreateBlockFooter>
|
|
148
|
+
</CreateBlockContent>
|
|
149
|
+
</Form>
|
|
150
|
+
</CreateBlock>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { BlockComponentProps } from "@tulip-systems/blocks";
|
|
4
|
+
import {
|
|
5
|
+
InlineBlock,
|
|
6
|
+
InlineBlockContent,
|
|
7
|
+
InlineBlockDragHandle,
|
|
8
|
+
InlineBlockHeader,
|
|
9
|
+
InlineBlockSortable,
|
|
10
|
+
InlineBlockTrigger,
|
|
11
|
+
} from "@tulip-systems/blocks/client";
|
|
12
|
+
import { DropdownCommandMenu } from "@tulip-systems/commands/client";
|
|
13
|
+
import {
|
|
14
|
+
InlineField,
|
|
15
|
+
InlineFieldError,
|
|
16
|
+
InlineFieldLabel,
|
|
17
|
+
InlineTextarea,
|
|
18
|
+
} from "@tulip-systems/inline/client";
|
|
19
|
+
import { DataStackLayout, DataStackRow } from "@tulip-systems/ui/data-stack";
|
|
20
|
+
import { GripIcon } from "lucide-react";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
import { orpc } from "@/server/router/client";
|
|
23
|
+
import { quotationInlineBlockCommands } from "../commands";
|
|
24
|
+
import { quotationHeroBlock } from "./config";
|
|
25
|
+
|
|
26
|
+
const heroMutation = orpc.quotationBlocks.update.mutationOptions({
|
|
27
|
+
meta: { invalidates: [orpc.quotationBlocks.key()] },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export function QuotationHeroInlineBlock({
|
|
31
|
+
block,
|
|
32
|
+
}: BlockComponentProps<typeof quotationHeroBlock>) {
|
|
33
|
+
return (
|
|
34
|
+
<InlineBlockSortable id={block.id}>
|
|
35
|
+
<InlineBlock>
|
|
36
|
+
<InlineBlockHeader>
|
|
37
|
+
<InlineBlockDragHandle>
|
|
38
|
+
<GripIcon className="h-4 w-4" />
|
|
39
|
+
<span className="sr-only">Blok verplaatsen</span>
|
|
40
|
+
</InlineBlockDragHandle>
|
|
41
|
+
<InlineBlockTrigger>{quotationHeroBlock.label}</InlineBlockTrigger>
|
|
42
|
+
<DropdownCommandMenu
|
|
43
|
+
data={{ id: block.id }}
|
|
44
|
+
commands={quotationInlineBlockCommands}
|
|
45
|
+
className="ml-2 size-7"
|
|
46
|
+
/>
|
|
47
|
+
</InlineBlockHeader>
|
|
48
|
+
|
|
49
|
+
<InlineBlockContent>
|
|
50
|
+
<DataStackLayout>
|
|
51
|
+
<DataStackRow>
|
|
52
|
+
<InlineField>
|
|
53
|
+
<InlineFieldLabel>Titel deel 1</InlineFieldLabel>
|
|
54
|
+
<InlineTextarea
|
|
55
|
+
initialValue={block.data.titlePart1}
|
|
56
|
+
variables={(value) => ({ id: block.id, data: { titlePart1: value ?? "" } })}
|
|
57
|
+
mutation={heroMutation}
|
|
58
|
+
strategy={{ mutate: { on: "blur" }, validate: { blur: z.string().min(1) } }}
|
|
59
|
+
/>
|
|
60
|
+
<InlineFieldError />
|
|
61
|
+
</InlineField>
|
|
62
|
+
|
|
63
|
+
<InlineField>
|
|
64
|
+
<InlineFieldLabel>Titel deel 2</InlineFieldLabel>
|
|
65
|
+
<InlineTextarea
|
|
66
|
+
initialValue={block.data.titlePart2 ?? ""}
|
|
67
|
+
variables={(value) => ({ id: block.id, data: { titlePart2: value ?? "" } })}
|
|
68
|
+
mutation={heroMutation}
|
|
69
|
+
strategy={{ mutate: { on: "blur" }, validate: { blur: z.string() } }}
|
|
70
|
+
/>
|
|
71
|
+
<InlineFieldError />
|
|
72
|
+
</InlineField>
|
|
73
|
+
</DataStackRow>
|
|
74
|
+
|
|
75
|
+
<InlineField className="md:col-span-2">
|
|
76
|
+
<InlineFieldLabel>Beschrijving</InlineFieldLabel>
|
|
77
|
+
<InlineTextarea
|
|
78
|
+
initialValue={block.data.description ?? ""}
|
|
79
|
+
variables={(value) => ({ id: block.id, data: { description: value ?? "" } })}
|
|
80
|
+
mutation={heroMutation}
|
|
81
|
+
strategy={{ mutate: { on: "blur" }, validate: { blur: z.string() } }}
|
|
82
|
+
/>
|
|
83
|
+
<InlineFieldError />
|
|
84
|
+
</InlineField>
|
|
85
|
+
</DataStackLayout>
|
|
86
|
+
</InlineBlockContent>
|
|
87
|
+
</InlineBlock>
|
|
88
|
+
</InlineBlockSortable>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { BlockComponentProps } from "@tulip-systems/blocks";
|
|
2
|
+
import { cn } from "@tulip-systems/ui/cn";
|
|
3
|
+
import Image from "next/image";
|
|
4
|
+
import { typographyVariants } from "../styles";
|
|
5
|
+
import type { quotationHeroBlock } from "./config";
|
|
6
|
+
|
|
7
|
+
export async function QuotationHeroPreview({
|
|
8
|
+
block,
|
|
9
|
+
}: BlockComponentProps<typeof quotationHeroBlock>) {
|
|
10
|
+
const { titlePart1, titlePart2, description } = block.data;
|
|
11
|
+
|
|
12
|
+
const fullTitle = [titlePart1, titlePart2].filter(Boolean).join(" ");
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<section className="relative flex min-h-svh items-stretch overflow-hidden bg-background px-5 pt-10 pb-10 md:gap-27 md:px-10 lg:items-stretch">
|
|
16
|
+
{fullTitle && <h1 className="sr-only">{fullTitle}</h1>}
|
|
17
|
+
|
|
18
|
+
<div className="absolute inset-0 z-0">
|
|
19
|
+
<Image
|
|
20
|
+
src="/images/preview/hero-home.webp"
|
|
21
|
+
alt="Hero home"
|
|
22
|
+
className="object-cover"
|
|
23
|
+
fill
|
|
24
|
+
sizes="100vw"
|
|
25
|
+
preload
|
|
26
|
+
/>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div className="relative z-10 flex w-full flex-col gap-2 lg:flex-row lg:justify-between lg:gap-10">
|
|
30
|
+
{/* Right Side — `grow lg:grow-0` on mobile so this column fills the
|
|
31
|
+
inner div's vertical space (inner div itself is stretched by the
|
|
32
|
+
section's `items-stretch`), which lets `justify-between` actually
|
|
33
|
+
push the title-group to the top and the description+buttons to
|
|
34
|
+
the bottom. On desktop the parent is `flex-row`; there we don't
|
|
35
|
+
want horizontal growth so we reset with `lg:grow-0`. */}
|
|
36
|
+
<div className="flex grow flex-col justify-between gap-5 lg:grow-0 lg:gap-0">
|
|
37
|
+
{/* Title group — subTitle + both title parts stack here so that the
|
|
38
|
+
right side div only sees TWO children (title-group and the
|
|
39
|
+
description+buttons block). With 3 children, `justify-between`
|
|
40
|
+
would strand titlePart2 in the middle. */}
|
|
41
|
+
<div className="flex flex-col gap-5 lg:gap-7.5">
|
|
42
|
+
{titlePart1 && (
|
|
43
|
+
<p
|
|
44
|
+
aria-hidden="true"
|
|
45
|
+
className={cn("lg:wrap-anywhere", typographyVariants({ variant: "title-large" }))}
|
|
46
|
+
>
|
|
47
|
+
{titlePart1}
|
|
48
|
+
</p>
|
|
49
|
+
)}
|
|
50
|
+
|
|
51
|
+
{titlePart2 && (
|
|
52
|
+
<p
|
|
53
|
+
aria-hidden="true"
|
|
54
|
+
className={cn(
|
|
55
|
+
"lg:wrap-anywhere text-right lg:hidden",
|
|
56
|
+
typographyVariants({ variant: "title-large" }),
|
|
57
|
+
)}
|
|
58
|
+
>
|
|
59
|
+
{titlePart2}
|
|
60
|
+
</p>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div className="flex flex-col gap-10 pt-5 lg:pt-0">
|
|
65
|
+
{description && (
|
|
66
|
+
<p className={cn("max-w-md", typographyVariants({ variant: "body-small" }))}>
|
|
67
|
+
{description}
|
|
68
|
+
</p>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{/* Left Side - Desktop */}
|
|
74
|
+
<div className="hidden flex-col gap-27.5 lg:flex lg:items-end lg:self-end">
|
|
75
|
+
{titlePart2 && (
|
|
76
|
+
<p
|
|
77
|
+
aria-hidden="true"
|
|
78
|
+
className={cn(
|
|
79
|
+
"lg:wrap-anywhere text-right",
|
|
80
|
+
typographyVariants({ variant: "title-large" }),
|
|
81
|
+
)}
|
|
82
|
+
>
|
|
83
|
+
{titlePart2}
|
|
84
|
+
</p>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</section>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createBlockRegistry, type InferBlock, type InferBlockInput } from "@tulip-systems/blocks";
|
|
2
|
+
import { quotationHeroBlock } from "./hero/config";
|
|
3
|
+
import { quotationSummaryBlock } from "./summary/config";
|
|
4
|
+
import { quotationTotalsBlock } from "./totals/config";
|
|
5
|
+
|
|
6
|
+
export const quotationBlocks = createBlockRegistry([
|
|
7
|
+
quotationHeroBlock,
|
|
8
|
+
quotationSummaryBlock,
|
|
9
|
+
quotationTotalsBlock,
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
export type QuotationBlock = InferBlock<typeof quotationBlocks>;
|
|
13
|
+
export type QuotationBlockInput = InferBlockInput<typeof quotationBlocks>;
|