@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
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useSuspenseQuery } from "@tanstack/react-query";
|
|
4
|
+
import type { InferBlockComponents, InferBlockCreateComponents } from "@tulip-systems/blocks";
|
|
5
|
+
import { RenderBlocks, RenderCreateBlocks, SortableBlocks } from "@tulip-systems/blocks/client";
|
|
6
|
+
import { Button } from "@tulip-systems/ui/button";
|
|
7
|
+
import { cn } from "@tulip-systems/ui/cn";
|
|
8
|
+
import {
|
|
9
|
+
Dialog,
|
|
10
|
+
DialogClose,
|
|
11
|
+
DialogContent,
|
|
12
|
+
DialogDescription,
|
|
13
|
+
DialogFooter,
|
|
14
|
+
DialogHeader,
|
|
15
|
+
DialogTitle,
|
|
16
|
+
DialogTrigger,
|
|
17
|
+
} from "@tulip-systems/ui/dialog";
|
|
18
|
+
import { toast } from "@tulip-systems/ui/sonner";
|
|
19
|
+
import { type CSSProperties, useState } from "react";
|
|
20
|
+
import { Icons } from "@/components/icons";
|
|
21
|
+
import { fontNeueHaas } from "@/lib/utils/fonts";
|
|
22
|
+
import { orpc } from "@/server/router/client";
|
|
23
|
+
import { QuotationHeroCreateBlock } from "./hero/create";
|
|
24
|
+
import { QuotationHeroInlineBlock } from "./hero/inline";
|
|
25
|
+
import { QuotationHeroPreview } from "./hero/preview";
|
|
26
|
+
import { quotationBlocks } from "./registry";
|
|
27
|
+
import { QuotationSummaryCreateBlock } from "./summary/create";
|
|
28
|
+
import { QuotationSummaryInlineBlock } from "./summary/inline";
|
|
29
|
+
import { QuotationSummaryPreview } from "./summary/preview";
|
|
30
|
+
import { QuotationTotalsCreateBlock } from "./totals/create";
|
|
31
|
+
import { QuotationTotalsInlineBlock } from "./totals/inline";
|
|
32
|
+
import { QuotationTotalsPreview } from "./totals/preview";
|
|
33
|
+
|
|
34
|
+
const quotationInlineBlockComponents = {
|
|
35
|
+
hero: QuotationHeroInlineBlock,
|
|
36
|
+
summary: QuotationSummaryInlineBlock,
|
|
37
|
+
totals: QuotationTotalsInlineBlock,
|
|
38
|
+
} satisfies InferBlockComponents<typeof quotationBlocks>;
|
|
39
|
+
|
|
40
|
+
const quotationPreviewComponents = {
|
|
41
|
+
hero: QuotationHeroPreview,
|
|
42
|
+
summary: QuotationSummaryPreview,
|
|
43
|
+
totals: QuotationTotalsPreview,
|
|
44
|
+
} satisfies InferBlockComponents<typeof quotationBlocks>;
|
|
45
|
+
|
|
46
|
+
const quotationCreateBlockComponents = {
|
|
47
|
+
hero: QuotationHeroCreateBlock,
|
|
48
|
+
summary: QuotationSummaryCreateBlock,
|
|
49
|
+
totals: QuotationTotalsCreateBlock,
|
|
50
|
+
} satisfies InferBlockCreateComponents<typeof quotationBlocks>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Renders the quotation inline blocks by fetching the block data and rendering them using the appropriate components.
|
|
54
|
+
*
|
|
55
|
+
* @remarks
|
|
56
|
+
* This function uses the `useSuspenseQuery` hook to fetch the block data for a specific quotation (with id "quotation-demo").
|
|
57
|
+
* It then renders the blocks using the `RenderBlocks` component, passing in the fetched data and the corresponding inline block components for each block type.
|
|
58
|
+
*/
|
|
59
|
+
export function RenderQuotationInlineBlocks({ quotationId }: { quotationId: string }) {
|
|
60
|
+
const blocksQueryOptions = orpc.quotationBlocks.list.queryOptions({ input: { quotationId } });
|
|
61
|
+
const { data } = useSuspenseQuery(blocksQueryOptions);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<SortableBlocks
|
|
65
|
+
data={data}
|
|
66
|
+
variables={({ activeBlock, toIndex }) => ({
|
|
67
|
+
quotationId,
|
|
68
|
+
id: activeBlock.id,
|
|
69
|
+
toIndex,
|
|
70
|
+
})}
|
|
71
|
+
mutation={orpc.quotationBlocks.move.mutationOptions({
|
|
72
|
+
meta: { invalidates: [orpc.quotationBlocks.key()] },
|
|
73
|
+
})}
|
|
74
|
+
queryKey={blocksQueryOptions.queryKey}
|
|
75
|
+
>
|
|
76
|
+
<RenderBlocks data={data} components={quotationInlineBlockComponents} />
|
|
77
|
+
</SortableBlocks>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Renders a dialog for creating a new block for a specific quotation.
|
|
83
|
+
*
|
|
84
|
+
* @remarks
|
|
85
|
+
* This function uses the `Dialog` component to render a modal dialog that allows the user to select the type of block they want to create for the specified quotation.
|
|
86
|
+
* It provides options for creating hero, summary, and totals blocks, and handles the creation process using the `orpc.quotationBlocks.create` mutation.
|
|
87
|
+
*
|
|
88
|
+
* @param quotationId - The ID of the quotation for which a new block is being created.
|
|
89
|
+
*/
|
|
90
|
+
type CreateQuotationBlockCommandProps = {
|
|
91
|
+
quotationId: string;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export function CreateQuotationBlock({ quotationId }: CreateQuotationBlockCommandProps) {
|
|
95
|
+
const [open, setOpen] = useState(false);
|
|
96
|
+
|
|
97
|
+
const createBlockMutation = orpc.quotationBlocks.create.mutationOptions({
|
|
98
|
+
meta: { invalidates: [orpc.quotationBlocks.key()] },
|
|
99
|
+
onSuccess: () => {
|
|
100
|
+
toast.success("Blok succesvol toegevoegd");
|
|
101
|
+
setOpen(false);
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
107
|
+
<DialogTrigger asChild>
|
|
108
|
+
<Button variant="outline" className="h-10">
|
|
109
|
+
<Icons.add className="size-4" />
|
|
110
|
+
</Button>
|
|
111
|
+
</DialogTrigger>
|
|
112
|
+
|
|
113
|
+
<DialogContent className="sm:max-w-lg">
|
|
114
|
+
<DialogHeader>
|
|
115
|
+
<DialogTitle>Blok toevoegen</DialogTitle>
|
|
116
|
+
<DialogDescription>
|
|
117
|
+
Kies het type blok dat je aan de offerte wilt toevoegen.
|
|
118
|
+
</DialogDescription>
|
|
119
|
+
</DialogHeader>
|
|
120
|
+
|
|
121
|
+
<div className="grid gap-2 py-4">
|
|
122
|
+
<RenderCreateBlocks
|
|
123
|
+
registry={quotationBlocks}
|
|
124
|
+
components={quotationCreateBlockComponents}
|
|
125
|
+
variables={(block) => ({ block, quotationId })}
|
|
126
|
+
mutation={createBlockMutation}
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<DialogFooter>
|
|
131
|
+
<DialogClose asChild>
|
|
132
|
+
<Button type="button" variant="ghost" className="w-full">
|
|
133
|
+
Annuleren
|
|
134
|
+
</Button>
|
|
135
|
+
</DialogClose>
|
|
136
|
+
</DialogFooter>
|
|
137
|
+
</DialogContent>
|
|
138
|
+
</Dialog>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Renders the quotation preview by fetching the block data and rendering them using the appropriate preview components.
|
|
144
|
+
*
|
|
145
|
+
* @remarks
|
|
146
|
+
* This function uses the `useSuspenseQuery` hook to fetch the block data for a specific quotation (with id "quotation-demo").
|
|
147
|
+
* It then renders the blocks using the `RenderBlocks` component, passing in the fetched data and the corresponding preview components for each block type.
|
|
148
|
+
*/
|
|
149
|
+
export function RenderQuotationPreview({ quotationId }: { quotationId: string }) {
|
|
150
|
+
const { data } = useSuspenseQuery(
|
|
151
|
+
orpc.quotationBlocks.list.queryOptions({ input: { quotationId } }),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<main
|
|
156
|
+
className={cn(
|
|
157
|
+
fontNeueHaas.variable,
|
|
158
|
+
"min-h-svh bg-background font-sans text-foreground",
|
|
159
|
+
)}
|
|
160
|
+
style={
|
|
161
|
+
{
|
|
162
|
+
"--background": "oklch(10.396% 0.02936 272.582)",
|
|
163
|
+
"--foreground": "oklch(92.021% 0.02815 62.204)",
|
|
164
|
+
"--primary": "oklch(62.793% 0.25768 29.223)",
|
|
165
|
+
"--primary-foreground": "oklch(92.021% 0.02815 62.204)",
|
|
166
|
+
"--secondary": "oklch(27.55% 0.02031 293.331)",
|
|
167
|
+
"--secondary-foreground": "oklch(92.021% 0.02815 62.204)",
|
|
168
|
+
"--muted": "oklch(54.214% 0.02484 291.243)",
|
|
169
|
+
"--muted-foreground": "oklch(92.021% 0.02815 62.204)",
|
|
170
|
+
"--accent": "oklch(62.793% 0.25768 29.223)",
|
|
171
|
+
"--accent-foreground": "oklch(92.021% 0.02815 62.204)",
|
|
172
|
+
"--destructive": "oklch(62.793% 0.25768 29.223)",
|
|
173
|
+
"--destructive-foreground": "oklch(92.021% 0.02815 62.204)",
|
|
174
|
+
"--card": "oklch(27.55% 0.02031 293.331)",
|
|
175
|
+
"--card-foreground": "oklch(92.021% 0.02815 62.204)",
|
|
176
|
+
"--popover": "oklch(100% 0.00011 271.152)",
|
|
177
|
+
"--popover-foreground": "oklch(14.479% 0.00002 271.152)",
|
|
178
|
+
"--border": "oklch(11.786% 0.03271 343.321)",
|
|
179
|
+
"--input": "oklch(11.786% 0.03271 343.321)",
|
|
180
|
+
"--ring": "oklch(62.793% 0.25768 29.223)",
|
|
181
|
+
"--radius": "0.5rem",
|
|
182
|
+
"--page-spacing": "7vw",
|
|
183
|
+
"--page-spacing-xl": "20vw",
|
|
184
|
+
} as CSSProperties
|
|
185
|
+
}
|
|
186
|
+
>
|
|
187
|
+
<RenderBlocks data={data} components={quotationPreviewComponents} />
|
|
188
|
+
</main>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { cva } from "class-variance-authority";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Typography scale.
|
|
5
|
+
*
|
|
6
|
+
* Font-family: Neue Haas Grotesk Display Pro (de app-sans, via `font-sans`).
|
|
7
|
+
*
|
|
8
|
+
* Responsive: mobile/kleine schermen (< md, 768px) krijgen een kleinere basis,
|
|
9
|
+
* `md:` herstelt de desktopgrootte. Titels schalen sterker af dan body-tekst,
|
|
10
|
+
* zodat kleine tekst niet onleesbaar wordt.
|
|
11
|
+
*/
|
|
12
|
+
export const typographyVariants = cva("whitespace-pre-line font-sans text-foreground", {
|
|
13
|
+
variants: {
|
|
14
|
+
variant: {
|
|
15
|
+
"title-large": "text-6xl md:text-8xl font-[450] leading-none", // 72px → 100px (−25%) / Roman 55 (Figma "500" = afgerond 450) / 100%
|
|
16
|
+
"title-semi-large": "text-5xl md:text-6xl font-[450] leading-none", // 48px → 60px (−20%) / Roman 55 / 100%
|
|
17
|
+
"title-medium": "text-3xl md:text-4xl font-[450] leading-normal", // 30px → 40px (−17%) / Roman 55 / normal
|
|
18
|
+
"body-large": "text-2xl md:text-3xl font-normal leading-[1.2]", // 26px → 30px (−13%) / 400 / 120%
|
|
19
|
+
"body-medium": "text-xl md:text-2xl font-normal leading-[1.2]", // 22px → 25px (−8%) / 400 / 120%
|
|
20
|
+
"body-small": "text-lg md:text-xl font-normal leading-[1.2]", // 18px → 20px (−10%) / 400 / 120%
|
|
21
|
+
button: "text-base md:text-lg font-normal leading-[1.2]", // 16px → 18px (−11%) / 400 / 120%
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { BlockDefinition } from "@tulip-systems/blocks";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const quotationSummaryBlock = {
|
|
5
|
+
slug: "summary",
|
|
6
|
+
label: "Summary",
|
|
7
|
+
description: "Short commercial summary for the quotation.",
|
|
8
|
+
schema: z.object({
|
|
9
|
+
heading: z.string().min(1),
|
|
10
|
+
body: z.string().min(1),
|
|
11
|
+
highlights: z.array(z.string().min(1)),
|
|
12
|
+
}),
|
|
13
|
+
} as const satisfies BlockDefinition;
|
|
@@ -0,0 +1,175 @@
|
|
|
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
|
+
FormDescription,
|
|
27
|
+
FormField,
|
|
28
|
+
FormItem,
|
|
29
|
+
FormLabel,
|
|
30
|
+
FormMessage,
|
|
31
|
+
} from "@tulip-systems/ui/react-hook-form";
|
|
32
|
+
import { Textarea } from "@tulip-systems/ui/textarea";
|
|
33
|
+
import { BlocksIcon } from "lucide-react";
|
|
34
|
+
import { useForm } from "react-hook-form";
|
|
35
|
+
import type { z } from "zod";
|
|
36
|
+
import { quotationSummaryBlock } from "./config";
|
|
37
|
+
|
|
38
|
+
export type QuotationSummaryCreateValues = z.input<typeof quotationSummaryBlock.schema>;
|
|
39
|
+
|
|
40
|
+
type QuotationSummaryFormValues = Omit<QuotationSummaryCreateValues, "highlights"> & {
|
|
41
|
+
highlights: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type QuotationSummaryCreateBlockInput = {
|
|
45
|
+
slug: typeof quotationSummaryBlock.slug;
|
|
46
|
+
data: QuotationSummaryCreateValues;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type QuotationSummaryCreateBlockProps<
|
|
50
|
+
TData = unknown,
|
|
51
|
+
TError = DefaultError,
|
|
52
|
+
TVariables = undefined,
|
|
53
|
+
TOnMutateResult = unknown,
|
|
54
|
+
> = BlockCreateComponentProps<
|
|
55
|
+
QuotationSummaryCreateBlockInput,
|
|
56
|
+
TData,
|
|
57
|
+
TError,
|
|
58
|
+
TVariables,
|
|
59
|
+
TOnMutateResult
|
|
60
|
+
>;
|
|
61
|
+
|
|
62
|
+
export function QuotationSummaryCreateBlock<
|
|
63
|
+
TData = unknown,
|
|
64
|
+
TError = DefaultError,
|
|
65
|
+
TVariables = undefined,
|
|
66
|
+
TOnMutateResult = unknown,
|
|
67
|
+
>({
|
|
68
|
+
variables,
|
|
69
|
+
mutation,
|
|
70
|
+
}: QuotationSummaryCreateBlockProps<TData, TError, TVariables, TOnMutateResult>) {
|
|
71
|
+
const form = useForm<QuotationSummaryFormValues>({
|
|
72
|
+
resolver: standardSchemaResolver(
|
|
73
|
+
quotationSummaryBlock.schema.extend({ highlights: quotationSummaryBlock.schema.shape.body }),
|
|
74
|
+
),
|
|
75
|
+
defaultValues: {
|
|
76
|
+
heading: "Heldere scope, snelle oplevering",
|
|
77
|
+
body: "Beschrijf hier kort de waarde en scope van deze offerte.",
|
|
78
|
+
highlights: "Vaste opleverdatum, Duidelijke scope, Transparante prijs",
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<CreateBlock>
|
|
84
|
+
<CreateBlockTrigger>
|
|
85
|
+
<CreateBlockTriggerIcon>
|
|
86
|
+
<BlocksIcon />
|
|
87
|
+
</CreateBlockTriggerIcon>
|
|
88
|
+
|
|
89
|
+
<CreateBlockTriggerContent>
|
|
90
|
+
<CreateBlockTriggerTitle>{quotationSummaryBlock.label}</CreateBlockTriggerTitle>
|
|
91
|
+
<CreateBlockTriggerDescription>
|
|
92
|
+
{quotationSummaryBlock.description}
|
|
93
|
+
</CreateBlockTriggerDescription>
|
|
94
|
+
</CreateBlockTriggerContent>
|
|
95
|
+
</CreateBlockTrigger>
|
|
96
|
+
|
|
97
|
+
<Form {...form}>
|
|
98
|
+
<CreateBlockContent
|
|
99
|
+
className="sm:max-w-xl"
|
|
100
|
+
variables={(values: QuotationSummaryFormValues) =>
|
|
101
|
+
variables({
|
|
102
|
+
slug: quotationSummaryBlock.slug,
|
|
103
|
+
data: {
|
|
104
|
+
heading: values.heading,
|
|
105
|
+
body: values.body,
|
|
106
|
+
highlights: values.highlights
|
|
107
|
+
.split(",")
|
|
108
|
+
.map((item) => item.trim())
|
|
109
|
+
.filter(Boolean),
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
mutation={mutation}
|
|
114
|
+
>
|
|
115
|
+
<CreateBlockHeader>
|
|
116
|
+
<CreateBlockTitle>{quotationSummaryBlock.label} toevoegen</CreateBlockTitle>
|
|
117
|
+
<CreateBlockDescription>
|
|
118
|
+
Vul de verplichte velden in om het blok toe te voegen.
|
|
119
|
+
</CreateBlockDescription>
|
|
120
|
+
</CreateBlockHeader>
|
|
121
|
+
|
|
122
|
+
<CreateBlockFields>
|
|
123
|
+
<FormField
|
|
124
|
+
control={form.control}
|
|
125
|
+
name="heading"
|
|
126
|
+
render={({ field }) => (
|
|
127
|
+
<FormItem>
|
|
128
|
+
<FormLabel>Kop</FormLabel>
|
|
129
|
+
<FormControl>
|
|
130
|
+
<Input {...field} />
|
|
131
|
+
</FormControl>
|
|
132
|
+
<FormMessage />
|
|
133
|
+
</FormItem>
|
|
134
|
+
)}
|
|
135
|
+
/>
|
|
136
|
+
|
|
137
|
+
<FormField
|
|
138
|
+
control={form.control}
|
|
139
|
+
name="body"
|
|
140
|
+
render={({ field }) => (
|
|
141
|
+
<FormItem>
|
|
142
|
+
<FormLabel>Beschrijving</FormLabel>
|
|
143
|
+
<FormControl>
|
|
144
|
+
<Textarea {...field} />
|
|
145
|
+
</FormControl>
|
|
146
|
+
<FormMessage />
|
|
147
|
+
</FormItem>
|
|
148
|
+
)}
|
|
149
|
+
/>
|
|
150
|
+
|
|
151
|
+
<FormField
|
|
152
|
+
control={form.control}
|
|
153
|
+
name="highlights"
|
|
154
|
+
render={({ field }) => (
|
|
155
|
+
<FormItem>
|
|
156
|
+
<FormLabel>Highlights</FormLabel>
|
|
157
|
+
<FormControl>
|
|
158
|
+
<Input {...field} />
|
|
159
|
+
</FormControl>
|
|
160
|
+
<FormDescription>Komma-gescheiden lijst.</FormDescription>
|
|
161
|
+
<FormMessage />
|
|
162
|
+
</FormItem>
|
|
163
|
+
)}
|
|
164
|
+
/>
|
|
165
|
+
</CreateBlockFields>
|
|
166
|
+
|
|
167
|
+
<CreateBlockFooter>
|
|
168
|
+
<CreateBlockCancel>Annuleren</CreateBlockCancel>
|
|
169
|
+
<CreateBlockSubmit>Blok toevoegen</CreateBlockSubmit>
|
|
170
|
+
</CreateBlockFooter>
|
|
171
|
+
</CreateBlockContent>
|
|
172
|
+
</Form>
|
|
173
|
+
</CreateBlock>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
InlineStringInput,
|
|
18
|
+
} from "@tulip-systems/inline/client";
|
|
19
|
+
import { GripIcon } from "lucide-react";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
import { orpc } from "@/server/router/client";
|
|
22
|
+
import { quotationInlineBlockCommands } from "../commands";
|
|
23
|
+
import { quotationSummaryBlock } from "./config";
|
|
24
|
+
|
|
25
|
+
const summaryMutation = orpc.quotationBlocks.update.mutationOptions({
|
|
26
|
+
meta: { invalidates: [orpc.quotationBlocks.key()] },
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export function QuotationSummaryInlineBlock({
|
|
30
|
+
block,
|
|
31
|
+
}: BlockComponentProps<typeof quotationSummaryBlock>) {
|
|
32
|
+
return (
|
|
33
|
+
<InlineBlockSortable id={block.id}>
|
|
34
|
+
<InlineBlock>
|
|
35
|
+
<InlineBlockHeader>
|
|
36
|
+
<InlineBlockDragHandle>
|
|
37
|
+
<GripIcon className="h-4 w-4" />
|
|
38
|
+
<span className="sr-only">Blok verplaatsen</span>
|
|
39
|
+
</InlineBlockDragHandle>
|
|
40
|
+
<InlineBlockTrigger>{quotationSummaryBlock.label}</InlineBlockTrigger>
|
|
41
|
+
<DropdownCommandMenu
|
|
42
|
+
data={{ id: block.id }}
|
|
43
|
+
commands={quotationInlineBlockCommands}
|
|
44
|
+
className="ml-2 size-7"
|
|
45
|
+
/>
|
|
46
|
+
</InlineBlockHeader>
|
|
47
|
+
|
|
48
|
+
<InlineBlockContent className="grid gap-4 md:grid-cols-2">
|
|
49
|
+
<InlineField>
|
|
50
|
+
<InlineFieldLabel>Kop</InlineFieldLabel>
|
|
51
|
+
<InlineStringInput
|
|
52
|
+
initialValue={block.data.heading}
|
|
53
|
+
variables={(value) => ({ id: block.id, data: { heading: value ?? "" } })}
|
|
54
|
+
mutation={summaryMutation}
|
|
55
|
+
strategy={{ mutate: { on: "blur" }, validate: { blur: z.string().min(1) } }}
|
|
56
|
+
/>
|
|
57
|
+
<InlineFieldError />
|
|
58
|
+
</InlineField>
|
|
59
|
+
|
|
60
|
+
<InlineField className="md:col-span-2">
|
|
61
|
+
<InlineFieldLabel>Beschrijving</InlineFieldLabel>
|
|
62
|
+
<InlineStringInput
|
|
63
|
+
initialValue={block.data.body}
|
|
64
|
+
variables={(value) => ({ id: block.id, data: { body: value ?? "" } })}
|
|
65
|
+
mutation={summaryMutation}
|
|
66
|
+
strategy={{ mutate: { on: "blur" }, validate: { blur: z.string().min(1) } }}
|
|
67
|
+
/>
|
|
68
|
+
<InlineFieldError />
|
|
69
|
+
</InlineField>
|
|
70
|
+
|
|
71
|
+
<InlineField className="md:col-span-2">
|
|
72
|
+
<InlineFieldLabel>Highlights</InlineFieldLabel>
|
|
73
|
+
<InlineStringInput
|
|
74
|
+
initialValue={block.data.highlights.join(", ")}
|
|
75
|
+
variables={(value) => ({
|
|
76
|
+
id: block.id,
|
|
77
|
+
data: {
|
|
78
|
+
highlights: (value ?? "")
|
|
79
|
+
.split(",")
|
|
80
|
+
.map((item) => item.trim())
|
|
81
|
+
.filter(Boolean),
|
|
82
|
+
},
|
|
83
|
+
})}
|
|
84
|
+
mutation={summaryMutation}
|
|
85
|
+
strategy={{ mutate: { on: "blur" }, validate: { blur: z.string().min(1) } }}
|
|
86
|
+
/>
|
|
87
|
+
<InlineFieldError />
|
|
88
|
+
</InlineField>
|
|
89
|
+
</InlineBlockContent>
|
|
90
|
+
</InlineBlock>
|
|
91
|
+
</InlineBlockSortable>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BlockComponentProps } from "@tulip-systems/blocks";
|
|
2
|
+
import type { quotationSummaryBlock } from "./config";
|
|
3
|
+
|
|
4
|
+
export function QuotationSummaryPreview({
|
|
5
|
+
block,
|
|
6
|
+
}: BlockComponentProps<typeof quotationSummaryBlock>) {
|
|
7
|
+
const { body, heading, highlights } = block.data;
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<section className="grid gap-6 rounded-3xl border bg-background p-8 md:grid-cols-[1fr_18rem]">
|
|
11
|
+
<div>
|
|
12
|
+
<h2 className="font-semibold text-2xl tracking-tight">{heading}</h2>
|
|
13
|
+
<p className="mt-4 text-muted-foreground leading-7">{body}</p>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<ul className="space-y-3">
|
|
17
|
+
{highlights.map((highlight) => (
|
|
18
|
+
<li key={highlight} className="rounded-2xl bg-muted/50 px-4 py-3 text-sm">
|
|
19
|
+
{highlight}
|
|
20
|
+
</li>
|
|
21
|
+
))}
|
|
22
|
+
</ul>
|
|
23
|
+
</section>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { BlockDefinition } from "@tulip-systems/blocks";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const quotationTotalsBlock = {
|
|
5
|
+
slug: "totals",
|
|
6
|
+
label: "Totals",
|
|
7
|
+
description: "Line totals and final quotation amount.",
|
|
8
|
+
schema: z.object({
|
|
9
|
+
currency: z.string().min(1),
|
|
10
|
+
items: z.array(
|
|
11
|
+
z.object({
|
|
12
|
+
label: z.string().min(1),
|
|
13
|
+
amount: z.number(),
|
|
14
|
+
}),
|
|
15
|
+
),
|
|
16
|
+
total: z.number(),
|
|
17
|
+
}),
|
|
18
|
+
} as const satisfies BlockDefinition;
|