@contractspec/bundle.marketing 1.12.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.
- package/.turbo/turbo-build$colon$types.log +1 -0
- package/.turbo/turbo-build.log +175 -0
- package/.turbo/turbo-lint.log +3 -0
- package/AGENTS.md +36 -0
- package/CHANGELOG.md +416 -0
- package/README.md +57 -0
- package/dist/components/marketing/ChangelogPage.d.ts +21 -0
- package/dist/components/marketing/ChangelogPage.d.ts.map +1 -0
- package/dist/components/marketing/ChangelogPage.js +65 -0
- package/dist/components/marketing/ChangelogPage.js.map +1 -0
- package/dist/components/marketing/CofounderPage.d.ts +7 -0
- package/dist/components/marketing/CofounderPage.d.ts.map +1 -0
- package/dist/components/marketing/CofounderPage.js +468 -0
- package/dist/components/marketing/CofounderPage.js.map +1 -0
- package/dist/components/marketing/ContactClient.d.ts +7 -0
- package/dist/components/marketing/ContactClient.d.ts.map +1 -0
- package/dist/components/marketing/ContactClient.js +158 -0
- package/dist/components/marketing/ContactClient.js.map +1 -0
- package/dist/components/marketing/ContributePage.d.ts +9 -0
- package/dist/components/marketing/ContributePage.d.ts.map +1 -0
- package/dist/components/marketing/ContributePage.js +362 -0
- package/dist/components/marketing/ContributePage.js.map +1 -0
- package/dist/components/marketing/DesignPartnerPage.d.ts +9 -0
- package/dist/components/marketing/DesignPartnerPage.d.ts.map +1 -0
- package/dist/components/marketing/DesignPartnerPage.js +215 -0
- package/dist/components/marketing/DesignPartnerPage.js.map +1 -0
- package/dist/components/marketing/LandingPage.d.ts +7 -0
- package/dist/components/marketing/LandingPage.d.ts.map +1 -0
- package/dist/components/marketing/LandingPage.js +38 -0
- package/dist/components/marketing/LandingPage.js.map +1 -0
- package/dist/components/marketing/PricingClient.d.ts +7 -0
- package/dist/components/marketing/PricingClient.d.ts.map +1 -0
- package/dist/components/marketing/PricingClient.js +521 -0
- package/dist/components/marketing/PricingClient.js.map +1 -0
- package/dist/components/marketing/ProductClientPage.d.ts +7 -0
- package/dist/components/marketing/ProductClientPage.d.ts.map +1 -0
- package/dist/components/marketing/ProductClientPage.js +460 -0
- package/dist/components/marketing/ProductClientPage.js.map +1 -0
- package/dist/components/marketing/index.d.ts +11 -0
- package/dist/components/marketing/index.js +12 -0
- package/dist/components/marketing/pricing-thinking-modal.d.ts +16 -0
- package/dist/components/marketing/pricing-thinking-modal.d.ts.map +1 -0
- package/dist/components/marketing/pricing-thinking-modal.js +202 -0
- package/dist/components/marketing/pricing-thinking-modal.js.map +1 -0
- package/dist/components/marketing/sections/AudienceSection.d.ts +7 -0
- package/dist/components/marketing/sections/AudienceSection.d.ts.map +1 -0
- package/dist/components/marketing/sections/AudienceSection.js +68 -0
- package/dist/components/marketing/sections/AudienceSection.js.map +1 -0
- package/dist/components/marketing/sections/CorePositioningSection.d.ts +7 -0
- package/dist/components/marketing/sections/CorePositioningSection.d.ts.map +1 -0
- package/dist/components/marketing/sections/CorePositioningSection.js +59 -0
- package/dist/components/marketing/sections/CorePositioningSection.js.map +1 -0
- package/dist/components/marketing/sections/CtaSection.d.ts +7 -0
- package/dist/components/marketing/sections/CtaSection.d.ts.map +1 -0
- package/dist/components/marketing/sections/CtaSection.js +54 -0
- package/dist/components/marketing/sections/CtaSection.js.map +1 -0
- package/dist/components/marketing/sections/DevelopersSection.d.ts +7 -0
- package/dist/components/marketing/sections/DevelopersSection.d.ts.map +1 -0
- package/dist/components/marketing/sections/DevelopersSection.js +45 -0
- package/dist/components/marketing/sections/DevelopersSection.js.map +1 -0
- package/dist/components/marketing/sections/FearsSection.d.ts +7 -0
- package/dist/components/marketing/sections/FearsSection.d.ts.map +1 -0
- package/dist/components/marketing/sections/FearsSection.js +48 -0
- package/dist/components/marketing/sections/FearsSection.js.map +1 -0
- package/dist/components/marketing/sections/HeroMarketingSection.d.ts +7 -0
- package/dist/components/marketing/sections/HeroMarketingSection.d.ts.map +1 -0
- package/dist/components/marketing/sections/HeroMarketingSection.js +77 -0
- package/dist/components/marketing/sections/HeroMarketingSection.js.map +1 -0
- package/dist/components/marketing/sections/IconGridSection.d.ts +45 -0
- package/dist/components/marketing/sections/IconGridSection.d.ts.map +1 -0
- package/dist/components/marketing/sections/IconGridSection.js +44 -0
- package/dist/components/marketing/sections/IconGridSection.js.map +1 -0
- package/dist/components/marketing/sections/OutputsSection.d.ts +7 -0
- package/dist/components/marketing/sections/OutputsSection.d.ts.map +1 -0
- package/dist/components/marketing/sections/OutputsSection.js +59 -0
- package/dist/components/marketing/sections/OutputsSection.js.map +1 -0
- package/dist/components/marketing/sections/ProblemSection.d.ts +7 -0
- package/dist/components/marketing/sections/ProblemSection.d.ts.map +1 -0
- package/dist/components/marketing/sections/ProblemSection.js +46 -0
- package/dist/components/marketing/sections/ProblemSection.js.map +1 -0
- package/dist/components/marketing/sections/SolutionSection.d.ts +7 -0
- package/dist/components/marketing/sections/SolutionSection.d.ts.map +1 -0
- package/dist/components/marketing/sections/SolutionSection.js +46 -0
- package/dist/components/marketing/sections/SolutionSection.js.map +1 -0
- package/dist/components/marketing/sections/StepsSection.d.ts +7 -0
- package/dist/components/marketing/sections/StepsSection.d.ts.map +1 -0
- package/dist/components/marketing/sections/StepsSection.js +52 -0
- package/dist/components/marketing/sections/StepsSection.js.map +1 -0
- package/dist/components/marketing/waitlist-section.d.ts +15 -0
- package/dist/components/marketing/waitlist-section.d.ts.map +1 -0
- package/dist/components/marketing/waitlist-section.js +578 -0
- package/dist/components/marketing/waitlist-section.js.map +1 -0
- package/dist/components/templates/TemplatesClientPage.d.ts +7 -0
- package/dist/components/templates/TemplatesClientPage.d.ts.map +1 -0
- package/dist/components/templates/TemplatesClientPage.js +625 -0
- package/dist/components/templates/TemplatesClientPage.js.map +1 -0
- package/dist/components/templates/TemplatesPage.d.ts +7 -0
- package/dist/components/templates/TemplatesPage.d.ts.map +1 -0
- package/dist/components/templates/TemplatesPage.js +125 -0
- package/dist/components/templates/TemplatesPage.js.map +1 -0
- package/dist/components/templates/TemplatesPreviewModal.d.ts +15 -0
- package/dist/components/templates/TemplatesPreviewModal.d.ts.map +1 -0
- package/dist/components/templates/TemplatesPreviewModal.js +137 -0
- package/dist/components/templates/TemplatesPreviewModal.js.map +1 -0
- package/dist/components/templates/index.d.ts +4 -0
- package/dist/components/templates/index.js +5 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +28 -0
- package/dist/libs/email/client.d.ts +15 -0
- package/dist/libs/email/client.d.ts.map +1 -0
- package/dist/libs/email/client.js +113 -0
- package/dist/libs/email/client.js.map +1 -0
- package/dist/libs/email/contact.d.ts +7 -0
- package/dist/libs/email/contact.d.ts.map +1 -0
- package/dist/libs/email/contact.js +71 -0
- package/dist/libs/email/contact.js.map +1 -0
- package/dist/libs/email/newsletter.d.ts +7 -0
- package/dist/libs/email/newsletter.d.ts.map +1 -0
- package/dist/libs/email/newsletter.js +95 -0
- package/dist/libs/email/newsletter.js.map +1 -0
- package/dist/libs/email/types.d.ts +53 -0
- package/dist/libs/email/types.d.ts.map +1 -0
- package/dist/libs/email/types.js +1 -0
- package/dist/libs/email/utils.d.ts +6 -0
- package/dist/libs/email/utils.d.ts.map +1 -0
- package/dist/libs/email/utils.js +7 -0
- package/dist/libs/email/utils.js.map +1 -0
- package/dist/libs/email/waitlist-application.d.ts +7 -0
- package/dist/libs/email/waitlist-application.d.ts.map +1 -0
- package/dist/libs/email/waitlist-application.js +170 -0
- package/dist/libs/email/waitlist-application.js.map +1 -0
- package/dist/libs/email/waitlist.d.ts +7 -0
- package/dist/libs/email/waitlist.d.ts.map +1 -0
- package/dist/libs/email/waitlist.js +105 -0
- package/dist/libs/email/waitlist.js.map +1 -0
- package/dist/libs/pricing-examples.d.ts +22 -0
- package/dist/libs/pricing-examples.d.ts.map +1 -0
- package/dist/libs/pricing-examples.js +21 -0
- package/dist/libs/pricing-examples.js.map +1 -0
- package/dist/registry/engine.d.ts +17 -0
- package/dist/registry/engine.d.ts.map +1 -0
- package/dist/registry/engine.js +24 -0
- package/dist/registry/engine.js.map +1 -0
- package/dist/registry/factory.d.ts +64 -0
- package/dist/registry/factory.d.ts.map +1 -0
- package/dist/registry/factory.js +61 -0
- package/dist/registry/factory.js.map +1 -0
- package/dist/registry/index.d.ts +8 -0
- package/dist/registry/index.js +8 -0
- package/dist/registry/registry-docs.d.ts +15 -0
- package/dist/registry/registry-docs.d.ts.map +1 -0
- package/dist/registry/registry-docs.js +305 -0
- package/dist/registry/registry-docs.js.map +1 -0
- package/dist/registry/registry-landing.d.ts +19 -0
- package/dist/registry/registry-landing.d.ts.map +1 -0
- package/dist/registry/registry-landing.js +95 -0
- package/dist/registry/registry-landing.js.map +1 -0
- package/dist/registry/registry.d.ts +30 -0
- package/dist/registry/registry.d.ts.map +1 -0
- package/dist/registry/registry.js +61 -0
- package/dist/registry/registry.js.map +1 -0
- package/dist/registry/types.d.ts +19 -0
- package/dist/registry/types.d.ts.map +1 -0
- package/dist/registry/types.js +0 -0
- package/dist/registry/utils.d.ts +31 -0
- package/dist/registry/utils.d.ts.map +1 -0
- package/dist/registry/utils.js +54 -0
- package/dist/registry/utils.js.map +1 -0
- package/package.json +151 -0
- package/src/components/marketing/ChangelogPage.tsx +110 -0
- package/src/components/marketing/CofounderPage.tsx +409 -0
- package/src/components/marketing/ContactClient.tsx +174 -0
- package/src/components/marketing/ContributePage.tsx +319 -0
- package/src/components/marketing/DesignPartnerPage.tsx +181 -0
- package/src/components/marketing/LandingPage.tsx +30 -0
- package/src/components/marketing/PricingClient.tsx +446 -0
- package/src/components/marketing/ProductClientPage.tsx +391 -0
- package/src/components/marketing/index.ts +10 -0
- package/src/components/marketing/pricing-thinking-modal.tsx +224 -0
- package/src/components/marketing/sections/AudienceSection.tsx +66 -0
- package/src/components/marketing/sections/CorePositioningSection.tsx +44 -0
- package/src/components/marketing/sections/CtaSection.tsx +57 -0
- package/src/components/marketing/sections/DevelopersSection.tsx +38 -0
- package/src/components/marketing/sections/FearsSection.tsx +45 -0
- package/src/components/marketing/sections/HeroMarketingSection.tsx +73 -0
- package/src/components/marketing/sections/IconGridSection.tsx +91 -0
- package/src/components/marketing/sections/OutputsSection.tsx +59 -0
- package/src/components/marketing/sections/ProblemSection.tsx +47 -0
- package/src/components/marketing/sections/SolutionSection.tsx +47 -0
- package/src/components/marketing/sections/StepsSection.tsx +55 -0
- package/src/components/marketing/waitlist-section.tsx +606 -0
- package/src/components/templates/TemplatesClientPage.tsx +711 -0
- package/src/components/templates/TemplatesPage.tsx +129 -0
- package/src/components/templates/TemplatesPreviewModal.tsx +260 -0
- package/src/components/templates/index.ts +3 -0
- package/src/index.ts +15 -0
- package/src/libs/email/client.test.ts +107 -0
- package/src/libs/email/client.ts +146 -0
- package/src/libs/email/contact.ts +80 -0
- package/src/libs/email/newsletter.ts +108 -0
- package/src/libs/email/types.ts +59 -0
- package/src/libs/email/utils.ts +8 -0
- package/src/libs/email/waitlist-application.ts +192 -0
- package/src/libs/email/waitlist.ts +118 -0
- package/src/libs/pricing-examples.ts +19 -0
- package/src/registry/engine.ts +38 -0
- package/src/registry/factory.ts +110 -0
- package/src/registry/index.ts +7 -0
- package/src/registry/registry-docs.ts +843 -0
- package/src/registry/registry-landing.ts +118 -0
- package/src/registry/registry.ts +85 -0
- package/src/registry/types.ts +17 -0
- package/src/registry/utils.ts +99 -0
- package/tsconfig.json +13 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsdown.config.js +10 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
Button,
|
|
6
|
+
ButtonLink,
|
|
7
|
+
Input,
|
|
8
|
+
MarketingCard,
|
|
9
|
+
MarketingCardContent,
|
|
10
|
+
MarketingCardDescription,
|
|
11
|
+
MarketingCardHeader,
|
|
12
|
+
MarketingCardTitle,
|
|
13
|
+
MarketingSection,
|
|
14
|
+
} from '@contractspec/lib.design-system';
|
|
15
|
+
import { HStack, VStack } from '@contractspec/lib.ui-kit-web/ui/stack';
|
|
16
|
+
import { listTemplates } from '@contractspec/module.examples';
|
|
17
|
+
import type { TemplateDefinition } from '@contractspec/lib.example-shared-ui';
|
|
18
|
+
|
|
19
|
+
function matchesQuery(t: TemplateDefinition, query: string): boolean {
|
|
20
|
+
const q = query.trim().toLowerCase();
|
|
21
|
+
if (!q) return true;
|
|
22
|
+
const hay =
|
|
23
|
+
`${t.id} ${t.name} ${t.description} ${t.tags.join(' ')}`.toLowerCase();
|
|
24
|
+
return hay.includes(q);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function TemplatesMarketingPage() {
|
|
28
|
+
const [query, setQuery] = useState('');
|
|
29
|
+
|
|
30
|
+
const templates = useMemo(() => listTemplates(), []);
|
|
31
|
+
const filtered = useMemo(
|
|
32
|
+
() => templates.filter((t) => matchesQuery(t, query)),
|
|
33
|
+
[templates, query]
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<>
|
|
38
|
+
<MarketingSection tone="default">
|
|
39
|
+
<VStack as="header" gap="lg" align="center">
|
|
40
|
+
<VStack gap="sm" align="center">
|
|
41
|
+
<ButtonLink href="/docs" variant="ghost">
|
|
42
|
+
Docs
|
|
43
|
+
</ButtonLink>
|
|
44
|
+
<ButtonLink href="/sandbox" variant="ghost">
|
|
45
|
+
Open Sandbox
|
|
46
|
+
</ButtonLink>
|
|
47
|
+
</VStack>
|
|
48
|
+
<VStack gap="sm" align="center">
|
|
49
|
+
<ButtonLink href="/templates" variant="default">
|
|
50
|
+
Templates
|
|
51
|
+
</ButtonLink>
|
|
52
|
+
</VStack>
|
|
53
|
+
</VStack>
|
|
54
|
+
</MarketingSection>
|
|
55
|
+
|
|
56
|
+
<MarketingSection tone="muted">
|
|
57
|
+
<VStack gap="lg">
|
|
58
|
+
<VStack gap="sm">
|
|
59
|
+
<ButtonLink href="/templates" variant="ghost">
|
|
60
|
+
Browse all examples
|
|
61
|
+
</ButtonLink>
|
|
62
|
+
</VStack>
|
|
63
|
+
<HStack gap="md" align="center" justify="between" wrap="wrap">
|
|
64
|
+
<Input
|
|
65
|
+
aria-label="Search templates and examples"
|
|
66
|
+
placeholder="Search templates and examples…"
|
|
67
|
+
value={query}
|
|
68
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
69
|
+
/>
|
|
70
|
+
</HStack>
|
|
71
|
+
</VStack>
|
|
72
|
+
</MarketingSection>
|
|
73
|
+
|
|
74
|
+
<MarketingSection tone="default">
|
|
75
|
+
<VStack gap="lg">
|
|
76
|
+
<HStack gap="md" wrap="wrap">
|
|
77
|
+
{filtered.map((t) => (
|
|
78
|
+
<MarketingCard
|
|
79
|
+
key={t.id}
|
|
80
|
+
className="w-full md:w-[calc(50%-0.75rem)] lg:w-[calc(33.333%-1rem)]"
|
|
81
|
+
>
|
|
82
|
+
<MarketingCardHeader>
|
|
83
|
+
<MarketingCardTitle>
|
|
84
|
+
{t.icon} {t.name}
|
|
85
|
+
</MarketingCardTitle>
|
|
86
|
+
<MarketingCardDescription>
|
|
87
|
+
{t.description}
|
|
88
|
+
</MarketingCardDescription>
|
|
89
|
+
</MarketingCardHeader>
|
|
90
|
+
<MarketingCardContent>
|
|
91
|
+
<VStack gap="md">
|
|
92
|
+
<HStack gap="sm" wrap="wrap">
|
|
93
|
+
{t.tags.slice(0, 6).map((tag: string) => (
|
|
94
|
+
<ButtonLink
|
|
95
|
+
key={`${t.id}-${tag}`}
|
|
96
|
+
href={`/templates?tag=${encodeURIComponent(tag)}`}
|
|
97
|
+
variant="ghost"
|
|
98
|
+
>
|
|
99
|
+
{tag}
|
|
100
|
+
</ButtonLink>
|
|
101
|
+
))}
|
|
102
|
+
</HStack>
|
|
103
|
+
<HStack gap="sm" justify="between" wrap="wrap">
|
|
104
|
+
<ButtonLinkToSandbox templateId={t.id} />
|
|
105
|
+
<Button variant="outline" onClick={() => void 0} disabled>
|
|
106
|
+
Install to Studio (soon)
|
|
107
|
+
</Button>
|
|
108
|
+
</HStack>
|
|
109
|
+
</VStack>
|
|
110
|
+
</MarketingCardContent>
|
|
111
|
+
</MarketingCard>
|
|
112
|
+
))}
|
|
113
|
+
</HStack>
|
|
114
|
+
</VStack>
|
|
115
|
+
</MarketingSection>
|
|
116
|
+
</>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function ButtonLinkToSandbox({ templateId }: { templateId: string }) {
|
|
121
|
+
return (
|
|
122
|
+
<ButtonLink
|
|
123
|
+
href={`/sandbox?template=${encodeURIComponent(templateId)}`}
|
|
124
|
+
variant="default"
|
|
125
|
+
>
|
|
126
|
+
Preview
|
|
127
|
+
</ButtonLink>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
|
|
5
|
+
import dynamic from 'next/dynamic';
|
|
6
|
+
import { Dialog, DialogContent } from '@contractspec/lib.ui-kit-web/ui/dialog';
|
|
7
|
+
import { ScrollArea } from '@contractspec/lib.ui-kit-web/ui/scroll-area';
|
|
8
|
+
import { LoadingSpinner } from '@contractspec/lib.ui-kit-web/ui/atoms/LoadingSpinner';
|
|
9
|
+
import type { TemplateId } from '@contractspec/lib.example-shared-ui';
|
|
10
|
+
|
|
11
|
+
// Dynamically import template components with ssr: false
|
|
12
|
+
const TemplateShell = dynamic(
|
|
13
|
+
() =>
|
|
14
|
+
import('@contractspec/lib.example-shared-ui').then(
|
|
15
|
+
(mod) => mod.TemplateShell
|
|
16
|
+
),
|
|
17
|
+
{ ssr: false, loading: () => <LoadingSpinner /> }
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const TodosTaskList = dynamic(
|
|
21
|
+
() =>
|
|
22
|
+
import('@contractspec/bundle.library/components/templates/todos/TaskList').then(
|
|
23
|
+
(mod) => mod.TaskList
|
|
24
|
+
),
|
|
25
|
+
{ ssr: false, loading: () => <LoadingSpinner /> }
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const MessagingWorkspace = dynamic(
|
|
29
|
+
() =>
|
|
30
|
+
import('@contractspec/bundle.library/components/templates/messaging/MessagingWorkspace').then(
|
|
31
|
+
(mod) => mod.MessagingWorkspace
|
|
32
|
+
),
|
|
33
|
+
{ ssr: false, loading: () => <LoadingSpinner /> }
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const RecipesExperience = dynamic(
|
|
37
|
+
() =>
|
|
38
|
+
import('@contractspec/bundle.library/components/templates/recipes/RecipeList').then(
|
|
39
|
+
(mod) => mod.RecipeList
|
|
40
|
+
),
|
|
41
|
+
{ ssr: false, loading: () => <LoadingSpinner /> }
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const SaasDashboard = dynamic(
|
|
45
|
+
() =>
|
|
46
|
+
import('@contractspec/example.saas-boilerplate').then(
|
|
47
|
+
(mod) => mod.SaasDashboard
|
|
48
|
+
),
|
|
49
|
+
{ ssr: false, loading: () => <LoadingSpinner /> }
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const CrmDashboard = dynamic(
|
|
53
|
+
() =>
|
|
54
|
+
import('@contractspec/example.crm-pipeline').then(
|
|
55
|
+
(mod) => mod.CrmDashboard
|
|
56
|
+
),
|
|
57
|
+
{ ssr: false, loading: () => <LoadingSpinner /> }
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const AgentDashboard = dynamic(
|
|
61
|
+
() =>
|
|
62
|
+
import('@contractspec/example.agent-console/ui').then(
|
|
63
|
+
(mod) => mod.AgentDashboard
|
|
64
|
+
),
|
|
65
|
+
{ ssr: false, loading: () => <LoadingSpinner /> }
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const WorkflowDashboard = dynamic(
|
|
69
|
+
() =>
|
|
70
|
+
import('@contractspec/example.workflow-system/ui').then(
|
|
71
|
+
(mod) => mod.WorkflowDashboard
|
|
72
|
+
),
|
|
73
|
+
{ ssr: false, loading: () => <LoadingSpinner /> }
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const MarketplaceDashboard = dynamic(
|
|
77
|
+
() =>
|
|
78
|
+
import('@contractspec/example.marketplace/ui').then(
|
|
79
|
+
(mod) => mod.MarketplaceDashboard
|
|
80
|
+
),
|
|
81
|
+
{ ssr: false, loading: () => <LoadingSpinner /> }
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const IntegrationDashboard = dynamic(
|
|
85
|
+
() =>
|
|
86
|
+
import('@contractspec/example.integration-hub/ui').then(
|
|
87
|
+
(mod) => mod.IntegrationDashboard
|
|
88
|
+
),
|
|
89
|
+
{ ssr: false, loading: () => <LoadingSpinner /> }
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const AnalyticsDashboard = dynamic(
|
|
93
|
+
() =>
|
|
94
|
+
import('@contractspec/example.analytics-dashboard').then(
|
|
95
|
+
(mod) => mod.AnalyticsDashboard
|
|
96
|
+
),
|
|
97
|
+
{ ssr: false, loading: () => <LoadingSpinner /> }
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
interface TemplatePreviewModalProps {
|
|
101
|
+
templateId: TemplateId | null;
|
|
102
|
+
onClose: () => void;
|
|
103
|
+
}
|
|
104
|
+
//
|
|
105
|
+
// return (
|
|
106
|
+
// <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 px-4 py-10">
|
|
107
|
+
// <div className="bg-background relative h-full max-h-[90vh] w-full max-w-5xl overflow-y-auto rounded-3xl p-6 shadow-2xl">
|
|
108
|
+
// <button
|
|
109
|
+
// type="button"
|
|
110
|
+
// className="btn-ghost absolute top-4 right-4 text-sm"
|
|
111
|
+
// onClick={onClose}
|
|
112
|
+
// >
|
|
113
|
+
// Close
|
|
114
|
+
// </button>
|
|
115
|
+
// {previewComponent}
|
|
116
|
+
// </div>
|
|
117
|
+
// </div>
|
|
118
|
+
// );
|
|
119
|
+
// };
|
|
120
|
+
|
|
121
|
+
export const TemplatePreviewModal = ({
|
|
122
|
+
templateId,
|
|
123
|
+
onClose,
|
|
124
|
+
}: TemplatePreviewModalProps) => {
|
|
125
|
+
const previewComponent = useMemo(() => {
|
|
126
|
+
switch (templateId) {
|
|
127
|
+
case 'todos-app':
|
|
128
|
+
return (
|
|
129
|
+
<TemplateShell
|
|
130
|
+
title="Starter tasks"
|
|
131
|
+
description="Track work items with filters, priorities, and per-tenant data isolation."
|
|
132
|
+
showSaveAction={false}
|
|
133
|
+
>
|
|
134
|
+
<TodosTaskList />
|
|
135
|
+
</TemplateShell>
|
|
136
|
+
);
|
|
137
|
+
case 'messaging-app':
|
|
138
|
+
return (
|
|
139
|
+
<TemplateShell
|
|
140
|
+
title="Messaging workspace"
|
|
141
|
+
description="Realtime-ready messaging surface with optimistic delivery."
|
|
142
|
+
showSaveAction={false}
|
|
143
|
+
>
|
|
144
|
+
<MessagingWorkspace />
|
|
145
|
+
</TemplateShell>
|
|
146
|
+
);
|
|
147
|
+
case 'recipe-app-i18n':
|
|
148
|
+
return (
|
|
149
|
+
<TemplateShell
|
|
150
|
+
title="Ceremony recipes"
|
|
151
|
+
description="Switch locales and preview how rituals translate across teams."
|
|
152
|
+
showSaveAction={false}
|
|
153
|
+
>
|
|
154
|
+
<RecipesExperience />
|
|
155
|
+
</TemplateShell>
|
|
156
|
+
);
|
|
157
|
+
case 'saas-boilerplate':
|
|
158
|
+
return (
|
|
159
|
+
<TemplateShell
|
|
160
|
+
title="SaaS Boilerplate"
|
|
161
|
+
description="Multi-tenant organizations, projects, settings, and billing usage tracking."
|
|
162
|
+
showSaveAction={false}
|
|
163
|
+
>
|
|
164
|
+
<SaasDashboard />
|
|
165
|
+
</TemplateShell>
|
|
166
|
+
);
|
|
167
|
+
case 'crm-pipeline':
|
|
168
|
+
return (
|
|
169
|
+
<TemplateShell
|
|
170
|
+
title="CRM Pipeline"
|
|
171
|
+
description="Sales CRM with contacts, companies, deals, and pipeline stages."
|
|
172
|
+
showSaveAction={false}
|
|
173
|
+
>
|
|
174
|
+
<CrmDashboard />
|
|
175
|
+
</TemplateShell>
|
|
176
|
+
);
|
|
177
|
+
case 'agent-console':
|
|
178
|
+
return (
|
|
179
|
+
<TemplateShell
|
|
180
|
+
title="AI Agent Console"
|
|
181
|
+
description="AI agent orchestration with tools, agents, runs, and execution logs."
|
|
182
|
+
showSaveAction={false}
|
|
183
|
+
>
|
|
184
|
+
<AgentDashboard />
|
|
185
|
+
</TemplateShell>
|
|
186
|
+
);
|
|
187
|
+
case 'workflow-system':
|
|
188
|
+
return (
|
|
189
|
+
<TemplateShell
|
|
190
|
+
title="Workflow System"
|
|
191
|
+
description="Multi-step workflows with role-based approvals."
|
|
192
|
+
showSaveAction={false}
|
|
193
|
+
>
|
|
194
|
+
<WorkflowDashboard />
|
|
195
|
+
</TemplateShell>
|
|
196
|
+
);
|
|
197
|
+
case 'marketplace':
|
|
198
|
+
return (
|
|
199
|
+
<TemplateShell
|
|
200
|
+
title="Marketplace"
|
|
201
|
+
description="Two-sided marketplace with stores, products, and orders."
|
|
202
|
+
showSaveAction={false}
|
|
203
|
+
>
|
|
204
|
+
<MarketplaceDashboard />
|
|
205
|
+
</TemplateShell>
|
|
206
|
+
);
|
|
207
|
+
case 'integration-hub':
|
|
208
|
+
return (
|
|
209
|
+
<TemplateShell
|
|
210
|
+
title="Integration Hub"
|
|
211
|
+
description="Third-party integrations with sync and field mapping."
|
|
212
|
+
showSaveAction={false}
|
|
213
|
+
>
|
|
214
|
+
<IntegrationDashboard />
|
|
215
|
+
</TemplateShell>
|
|
216
|
+
);
|
|
217
|
+
case 'analytics-dashboard':
|
|
218
|
+
return (
|
|
219
|
+
<TemplateShell
|
|
220
|
+
title="Analytics Dashboard"
|
|
221
|
+
description="Custom dashboards with widgets and queries."
|
|
222
|
+
showSaveAction={false}
|
|
223
|
+
>
|
|
224
|
+
<AnalyticsDashboard />
|
|
225
|
+
</TemplateShell>
|
|
226
|
+
);
|
|
227
|
+
case null:
|
|
228
|
+
return null;
|
|
229
|
+
default:
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}, [templateId]);
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<Dialog open={!!previewComponent} onOpenChange={onClose}>
|
|
236
|
+
{/*<DialogTrigger asChild>*/}
|
|
237
|
+
{/* <Button variant="outline">Fullscreen Dialog</Button>*/}
|
|
238
|
+
{/*</DialogTrigger>*/}
|
|
239
|
+
<DialogContent className="mb-8 flex h-[calc(100vh-2rem)] min-w-[calc(100vw-2rem)] flex-col justify-between gap-0 p-0">
|
|
240
|
+
<ScrollArea className="flex flex-col justify-between overflow-hidden">
|
|
241
|
+
{/*<DialogHeader className="contents space-y-0 text-left">*/}
|
|
242
|
+
{/* <DialogTitle className="px-6 pt-6">Product Information</DialogTitle>*/}
|
|
243
|
+
{/* <DialogDescription asChild>*/}
|
|
244
|
+
{/* </DialogDescription>*/}
|
|
245
|
+
{/*</DialogHeader>*/}
|
|
246
|
+
{previewComponent}
|
|
247
|
+
</ScrollArea>
|
|
248
|
+
{/*<DialogFooter className="px-6 pb-6 sm:justify-end">*/}
|
|
249
|
+
{/* <DialogClose asChild>*/}
|
|
250
|
+
{/* <Button variant="outline">*/}
|
|
251
|
+
{/* <ChevronLeftIcon />*/}
|
|
252
|
+
{/* Back*/}
|
|
253
|
+
{/* </Button>*/}
|
|
254
|
+
{/* </DialogClose>*/}
|
|
255
|
+
{/* <Button type="button">Read More</Button>*/}
|
|
256
|
+
{/*</DialogFooter>*/}
|
|
257
|
+
</DialogContent>
|
|
258
|
+
</Dialog>
|
|
259
|
+
);
|
|
260
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from './components/marketing';
|
|
2
|
+
export * from './components/templates';
|
|
3
|
+
// Email utilities
|
|
4
|
+
export { submitContactForm } from './libs/email/contact';
|
|
5
|
+
export { subscribeToNewsletter } from './libs/email/newsletter';
|
|
6
|
+
export type {
|
|
7
|
+
SubmitContactFormResult,
|
|
8
|
+
SubmitNewsletterResult,
|
|
9
|
+
SubmitWaitlistApplicationResult,
|
|
10
|
+
SubmitWaitlistResult,
|
|
11
|
+
} from './libs/email/types';
|
|
12
|
+
export { joinWaitlist } from './libs/email/waitlist';
|
|
13
|
+
export { submitWaitlistApplication } from './libs/email/waitlist-application';
|
|
14
|
+
|
|
15
|
+
export * from './registry';
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion -- Pragmatic use of non-null assertion for tests */
|
|
4
|
+
import { beforeEach, describe, expect, it } from 'bun:test';
|
|
5
|
+
import { __internal, getEmailConfig, sendEmail } from './client';
|
|
6
|
+
|
|
7
|
+
const ENV_KEYS = [
|
|
8
|
+
'SCALEWAY_ACCESS_KEY',
|
|
9
|
+
'SCALEWAY_SECRET_KEY',
|
|
10
|
+
'SCALEWAY_PROJECT_ID',
|
|
11
|
+
'SCALEWAY_ACCESS_KEY_QUEUE',
|
|
12
|
+
'SCALEWAY_SECRET_KEY_QUEUE',
|
|
13
|
+
'SCALEWAY_REGION',
|
|
14
|
+
'SCALEWAY_EMAIL_FROM_EMAIL',
|
|
15
|
+
'SCALEWAY_EMAIL_FROM_NAME',
|
|
16
|
+
'SCALEWAY_EMAIL_TEAM_EMAIL',
|
|
17
|
+
'SCALEWAY_EMAIL_TEAM_NAME',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const clearEnv = () => {
|
|
21
|
+
ENV_KEYS.forEach((key) => {
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- Pragmatic use for environment cleanup in tests
|
|
23
|
+
delete process.env[key];
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const setEnv = (values: Partial<Record<string, string>>) => {
|
|
28
|
+
Object.entries(values).forEach(([key, value]) => {
|
|
29
|
+
process.env[key] = value;
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe('email client config', () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
__internal.resetCaches();
|
|
36
|
+
clearEnv();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('returns a failure result when credentials are missing', () => {
|
|
40
|
+
const result = getEmailConfig();
|
|
41
|
+
expect(result.ok).toBeFalse();
|
|
42
|
+
expect(result.config).toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('prefers Scaleway env variables for config and region mapping', () => {
|
|
46
|
+
setEnv({
|
|
47
|
+
SCALEWAY_ACCESS_KEY: 'access',
|
|
48
|
+
SCALEWAY_SECRET_KEY: 'secret',
|
|
49
|
+
SCALEWAY_PROJECT_ID: 'project-123',
|
|
50
|
+
SCALEWAY_EMAIL_FROM_EMAIL: 'from@example.com',
|
|
51
|
+
SCALEWAY_EMAIL_FROM_NAME: 'From Name',
|
|
52
|
+
SCALEWAY_EMAIL_TEAM_EMAIL: 'team@example.com',
|
|
53
|
+
SCALEWAY_EMAIL_TEAM_NAME: 'Team Name',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const result = getEmailConfig();
|
|
57
|
+
expect(result.ok).toBeTrue();
|
|
58
|
+
expect(result.config?.accessKey).toBe('access');
|
|
59
|
+
expect(result.config?.secretKey).toBe('secret');
|
|
60
|
+
expect(result.config?.projectId).toBe('project-123');
|
|
61
|
+
expect(result.config?.region).toBe('fr-par');
|
|
62
|
+
expect(result.config?.from.email).toBe('from@example.com');
|
|
63
|
+
expect(result.config?.from.name).toBe('From Name');
|
|
64
|
+
expect(result.config?.teamInbox.email).toBe('team@example.com');
|
|
65
|
+
expect(result.config?.teamInbox.name).toBe('Team Name');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('uses provided API factory to send email with reply-to header', async () => {
|
|
69
|
+
setEnv({
|
|
70
|
+
SCALEWAY_ACCESS_KEY: 'access',
|
|
71
|
+
SCALEWAY_SECRET_KEY: 'secret',
|
|
72
|
+
SCALEWAY_PROJECT_ID: 'project-123',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
let captured: unknown = null;
|
|
76
|
+
|
|
77
|
+
__internal.setClient({
|
|
78
|
+
async createEmail(request: unknown) {
|
|
79
|
+
captured = request;
|
|
80
|
+
return { emails: [] };
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const config = getEmailConfig().config!;
|
|
85
|
+
|
|
86
|
+
const response = await sendEmail(config, {
|
|
87
|
+
to: [{ email: 'user@example.com' }],
|
|
88
|
+
subject: 'Subject',
|
|
89
|
+
text: 'Plain text',
|
|
90
|
+
html: '<p>html</p>',
|
|
91
|
+
replyTo: 'reply@example.com',
|
|
92
|
+
context: 'test',
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(response.success).toBeTrue();
|
|
96
|
+
const createEmailRequest = captured as Record<string, unknown>;
|
|
97
|
+
expect(createEmailRequest.region).toBe(config.region);
|
|
98
|
+
expect(createEmailRequest.projectId).toBe(config.projectId);
|
|
99
|
+
expect(createEmailRequest.from).toStrictEqual(config.from);
|
|
100
|
+
expect(createEmailRequest.to).toStrictEqual([
|
|
101
|
+
{ email: 'user@example.com' },
|
|
102
|
+
]);
|
|
103
|
+
expect(createEmailRequest.additionalHeaders).toStrictEqual([
|
|
104
|
+
{ key: 'Reply-To', value: 'reply@example.com' },
|
|
105
|
+
]);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { createClient, Temv1alpha1 } from '@scaleway/sdk';
|
|
2
|
+
import type { Region } from '@scaleway/sdk-client';
|
|
3
|
+
import { Logger } from '@contractspec/lib.logger';
|
|
4
|
+
import type {
|
|
5
|
+
EmailAddress,
|
|
6
|
+
EmailConfigResult,
|
|
7
|
+
EmailSendOutcome,
|
|
8
|
+
EmailServiceConfig,
|
|
9
|
+
SendEmailRequest,
|
|
10
|
+
} from './types';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_FROM: EmailAddress = {
|
|
13
|
+
email: 'noreply@transactional.contractspec.io',
|
|
14
|
+
name: 'ContractSpec',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const DEFAULT_TEAM_INBOX: EmailAddress = {
|
|
18
|
+
email: 'contact@contractspec.io',
|
|
19
|
+
name: 'ContractSpec Team',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const DEFAULT_REGION: Region = 'fr-par';
|
|
23
|
+
|
|
24
|
+
type EmailApi = Pick<Temv1alpha1.API, 'createEmail'>;
|
|
25
|
+
|
|
26
|
+
let cachedConfig: EmailServiceConfig | null = null;
|
|
27
|
+
let cachedClient: EmailApi | null = null;
|
|
28
|
+
let apiFactory: (client: ReturnType<typeof createClient>) => EmailApi = (
|
|
29
|
+
client
|
|
30
|
+
) => new Temv1alpha1.API(client);
|
|
31
|
+
|
|
32
|
+
const mapRegion = (value?: string | null): Region => {
|
|
33
|
+
const normalized = value?.trim().toLowerCase();
|
|
34
|
+
if (normalized === 'par' || normalized === 'fr-par') return 'fr-par';
|
|
35
|
+
if (normalized === 'ams' || normalized === 'nl-ams') return 'nl-ams';
|
|
36
|
+
if (normalized === 'waw' || normalized === 'pl-waw') return 'pl-waw';
|
|
37
|
+
return DEFAULT_REGION;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const getEmailConfig = (): EmailConfigResult => {
|
|
41
|
+
if (cachedConfig) {
|
|
42
|
+
return { ok: true, config: cachedConfig };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const accessKey =
|
|
46
|
+
process.env.SCALEWAY_ACCESS_KEY || process.env.SCALEWAY_ACCESS_KEY_QUEUE;
|
|
47
|
+
const secretKey =
|
|
48
|
+
process.env.SCALEWAY_SECRET_KEY || process.env.SCALEWAY_SECRET_KEY_QUEUE;
|
|
49
|
+
const projectId = process.env.SCALEWAY_PROJECT_ID;
|
|
50
|
+
|
|
51
|
+
if (!accessKey || !secretKey || !projectId) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
errorMessage:
|
|
55
|
+
'Email service is not configured. Please contact us directly at contact@contractspec.io.',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const region = mapRegion(process.env.SCALEWAY_REGION);
|
|
60
|
+
|
|
61
|
+
cachedConfig = {
|
|
62
|
+
accessKey,
|
|
63
|
+
secretKey,
|
|
64
|
+
projectId,
|
|
65
|
+
region,
|
|
66
|
+
defaultZone: `${region}-1`,
|
|
67
|
+
from: {
|
|
68
|
+
email: process.env.SCALEWAY_EMAIL_FROM_EMAIL ?? DEFAULT_FROM.email,
|
|
69
|
+
name: process.env.SCALEWAY_EMAIL_FROM_NAME ?? DEFAULT_FROM.name,
|
|
70
|
+
},
|
|
71
|
+
teamInbox: {
|
|
72
|
+
email: process.env.SCALEWAY_EMAIL_TEAM_EMAIL ?? DEFAULT_TEAM_INBOX.email,
|
|
73
|
+
name: process.env.SCALEWAY_EMAIL_TEAM_NAME ?? DEFAULT_TEAM_INBOX.name,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return { ok: true, config: cachedConfig };
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const getTemClient = (config: EmailServiceConfig): EmailApi => {
|
|
81
|
+
if (cachedClient) {
|
|
82
|
+
return cachedClient;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const client = createClient({
|
|
86
|
+
accessKey: config.accessKey,
|
|
87
|
+
secretKey: config.secretKey,
|
|
88
|
+
defaultProjectId: config.projectId,
|
|
89
|
+
defaultRegion: config.region,
|
|
90
|
+
defaultZone: config.defaultZone,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
cachedClient = apiFactory(client);
|
|
94
|
+
return cachedClient;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const sendEmail = async (
|
|
98
|
+
config: EmailServiceConfig,
|
|
99
|
+
request: SendEmailRequest
|
|
100
|
+
): Promise<EmailSendOutcome> => {
|
|
101
|
+
try {
|
|
102
|
+
const client = getTemClient(config);
|
|
103
|
+
|
|
104
|
+
await client.createEmail({
|
|
105
|
+
region: config.region,
|
|
106
|
+
projectId: config.projectId,
|
|
107
|
+
from: config.from,
|
|
108
|
+
to: request.to,
|
|
109
|
+
subject: request.subject,
|
|
110
|
+
text: request.text,
|
|
111
|
+
html: request.html || request.text,
|
|
112
|
+
additionalHeaders: request.replyTo
|
|
113
|
+
? [{ key: 'Reply-To', value: request.replyTo }]
|
|
114
|
+
: undefined,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return { success: true };
|
|
118
|
+
} catch (error) {
|
|
119
|
+
new Logger().error('scaleway_tem_email_send_failed', {
|
|
120
|
+
context: request.context ?? 'email',
|
|
121
|
+
error: error instanceof Error ? error.message : error,
|
|
122
|
+
});
|
|
123
|
+
return {
|
|
124
|
+
success: false,
|
|
125
|
+
error,
|
|
126
|
+
errorMessage: 'Failed to send email via Scaleway.',
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export const __internal = {
|
|
132
|
+
resetCaches() {
|
|
133
|
+
cachedClient = null;
|
|
134
|
+
cachedConfig = null;
|
|
135
|
+
apiFactory = (client: ReturnType<typeof createClient>) =>
|
|
136
|
+
new Temv1alpha1.API(client);
|
|
137
|
+
},
|
|
138
|
+
setApiFactory(
|
|
139
|
+
factory: (client: ReturnType<typeof createClient>) => EmailApi
|
|
140
|
+
) {
|
|
141
|
+
apiFactory = factory;
|
|
142
|
+
},
|
|
143
|
+
setClient(client: EmailApi) {
|
|
144
|
+
cachedClient = client;
|
|
145
|
+
},
|
|
146
|
+
};
|