@djangocfg/layouts 1.4.20 → 1.4.21
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/README.md +72 -109
- package/package.json +5 -5
- package/src/snippets/ContactForm/ContactForm.tsx +282 -0
- package/src/snippets/ContactForm/ContactFormProvider.tsx +140 -0
- package/src/snippets/ContactForm/ContactInfo.tsx +114 -0
- package/src/snippets/ContactForm/ContactPage.tsx +139 -0
- package/src/snippets/ContactForm/index.ts +31 -0
- package/src/snippets/ContactForm/types.ts +109 -0
- package/src/snippets/index.ts +1 -0
package/README.md
CHANGED
|
@@ -1,140 +1,103 @@
|
|
|
1
1
|
# @djangocfg/layouts
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Pre-built layouts, auth, and snippets for Next.js + Tailwind CSS.
|
|
4
4
|
|
|
5
|
-
[
|
|
6
|
-
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
**Part of [DjangoCFG](https://djangocfg.com)** — modern Django framework for production-ready SaaS applications.
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
## Install
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @djangocfg/layouts
|
|
11
|
+
```
|
|
11
12
|
|
|
12
|
-
##
|
|
13
|
+
## Layouts
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
```tsx
|
|
16
|
+
import { AppLayout } from '@djangocfg/layouts/layouts';
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
<AppLayout config={config}>{children}</AppLayout>
|
|
19
|
+
```
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
| Layout | Description |
|
|
22
|
+
|--------|-------------|
|
|
23
|
+
| `AppLayout` | Main app shell with sidebar, header, footer |
|
|
24
|
+
| `ProfileLayout` | User profile pages |
|
|
25
|
+
| `SupportLayout` | Support/help pages |
|
|
26
|
+
| `PaymentsLayout` | Payment flows |
|
|
27
|
+
| `UILayout` | UI showcase |
|
|
28
|
+
| `ErrorLayout` | Error pages (404, 500) |
|
|
26
29
|
|
|
27
|
-
##
|
|
30
|
+
## Auth
|
|
28
31
|
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
```tsx
|
|
33
|
+
import { AuthProvider, useAuth } from '@djangocfg/layouts/auth';
|
|
34
|
+
import { AuthDialog } from '@djangocfg/layouts/snippets';
|
|
35
|
+
|
|
36
|
+
<AuthProvider>
|
|
37
|
+
<AuthDialog trigger={<Button>Sign In</Button>} />
|
|
38
|
+
</AuthProvider>
|
|
35
39
|
```
|
|
36
40
|
|
|
37
|
-
|
|
41
|
+
| Export | Description |
|
|
42
|
+
|--------|-------------|
|
|
43
|
+
| `AuthProvider` | Auth context provider |
|
|
44
|
+
| `useAuth` | Auth hook (user, login, logout) |
|
|
45
|
+
| `useAuthGuard` | Route protection hook |
|
|
46
|
+
| `authMiddleware` | Next.js middleware |
|
|
38
47
|
|
|
39
|
-
|
|
48
|
+
## Snippets
|
|
40
49
|
|
|
41
50
|
```tsx
|
|
42
|
-
|
|
43
|
-
import { AppLayout } from '@djangocfg/layouts';
|
|
44
|
-
import { appLayoutConfig } from '@/core';
|
|
45
|
-
|
|
46
|
-
export default function RootLayout({ children }) {
|
|
47
|
-
return (
|
|
48
|
-
<AppLayout config={appLayoutConfig}>
|
|
49
|
-
{children}
|
|
50
|
-
</AppLayout>
|
|
51
|
-
);
|
|
52
|
-
}
|
|
51
|
+
import { ContactPage, VideoPlayer, Breadcrumbs } from '@djangocfg/layouts/snippets';
|
|
53
52
|
```
|
|
54
53
|
|
|
55
|
-
|
|
54
|
+
| Snippet | Description |
|
|
55
|
+
|---------|-------------|
|
|
56
|
+
| `ContactPage` | Ready-to-use contact page with form |
|
|
57
|
+
| `ContactForm` | Contact form with API integration |
|
|
58
|
+
| `ContactInfo` | Contact details card |
|
|
59
|
+
| `AuthDialog` | Auth modal (login/register) |
|
|
60
|
+
| `VideoPlayer` | Vidstack video player |
|
|
61
|
+
| `Breadcrumbs` | Navigation breadcrumbs |
|
|
62
|
+
| `Chat` | Chat widget |
|
|
63
|
+
|
|
64
|
+
### ContactPage
|
|
56
65
|
|
|
57
66
|
```tsx
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
},
|
|
68
|
-
api: {
|
|
69
|
-
baseUrl: process.env.NEXT_PUBLIC_API_URL,
|
|
70
|
-
},
|
|
71
|
-
routes: {
|
|
72
|
-
// Route detection functions
|
|
73
|
-
},
|
|
74
|
-
publicLayout: {
|
|
75
|
-
// Public layout config
|
|
76
|
-
},
|
|
77
|
-
privateLayout: {
|
|
78
|
-
// Private layout config
|
|
79
|
-
},
|
|
80
|
-
};
|
|
67
|
+
// Minimal - all defaults configured
|
|
68
|
+
<ContactPage />
|
|
69
|
+
|
|
70
|
+
// Custom
|
|
71
|
+
<ContactPage
|
|
72
|
+
apiUrl="https://api.example.com"
|
|
73
|
+
email="hello@example.com"
|
|
74
|
+
calendlyUrl="https://calendly.com/..."
|
|
75
|
+
/>
|
|
81
76
|
```
|
|
82
77
|
|
|
83
|
-
|
|
78
|
+
**Defaults:**
|
|
79
|
+
- API: `http://localhost:8000` (dev) / `https://api.reforms.ai` (prod)
|
|
80
|
+
- Email: `markolofsen@gmail.com`
|
|
81
|
+
- Calendly: `https://calendly.com/markolofsen/meeting`
|
|
84
82
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
function App() {
|
|
90
|
-
return (
|
|
91
|
-
<AuthProvider>
|
|
92
|
-
<LoginForm onSuccess={handleLogin} />
|
|
93
|
-
</AuthProvider>
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
```
|
|
83
|
+
**Features:**
|
|
84
|
+
- localStorage draft saving
|
|
85
|
+
- Success state with icon
|
|
86
|
+
- Zod validation from `@djangocfg/api`
|
|
97
87
|
|
|
98
88
|
## Exports
|
|
99
89
|
|
|
100
|
-
| Path |
|
|
101
|
-
|
|
90
|
+
| Path | Content |
|
|
91
|
+
|------|---------|
|
|
102
92
|
| `@djangocfg/layouts` | Main exports |
|
|
103
93
|
| `@djangocfg/layouts/layouts` | Layout components |
|
|
104
|
-
| `@djangocfg/layouts/auth` | Auth
|
|
105
|
-
| `@djangocfg/layouts/
|
|
106
|
-
| `@djangocfg/layouts/
|
|
107
|
-
| `@djangocfg/layouts/
|
|
108
|
-
| `@djangocfg/layouts/styles` | CSS stylesheets |
|
|
109
|
-
|
|
110
|
-
## Available Layouts
|
|
111
|
-
|
|
112
|
-
- **PublicLayout** - For public pages (landing, docs)
|
|
113
|
-
- **PrivateLayout** - For authenticated pages (dashboard)
|
|
114
|
-
- **AuthLayout** - For auth pages (login, signup)
|
|
115
|
-
|
|
116
|
-
## Components
|
|
117
|
-
|
|
118
|
-
- `Seo` - SEO meta tags with OG image
|
|
119
|
-
- `PageProgress` - Loading progress bar
|
|
120
|
-
- `Footer` - Configurable footer
|
|
121
|
-
- `Sidebar` - Dashboard sidebar
|
|
122
|
-
- `Navigation` - Header navigation
|
|
94
|
+
| `@djangocfg/layouts/auth` | Auth context & hooks |
|
|
95
|
+
| `@djangocfg/layouts/snippets` | Reusable components |
|
|
96
|
+
| `@djangocfg/layouts/utils` | Utilities |
|
|
97
|
+
| `@djangocfg/layouts/styles` | CSS |
|
|
123
98
|
|
|
124
99
|
## Requirements
|
|
125
100
|
|
|
126
|
-
- Next.js >= 15
|
|
127
|
-
- React >= 19
|
|
128
|
-
- Tailwind CSS >= 4
|
|
129
|
-
|
|
130
|
-
## Documentation
|
|
131
|
-
|
|
132
|
-
Full documentation available at [djangocfg.com](https://djangocfg.com)
|
|
133
|
-
|
|
134
|
-
## Contributing
|
|
135
|
-
|
|
136
|
-
Issues and pull requests are welcome at [GitHub](https://github.com/markolofsen/django-cfg)
|
|
137
|
-
|
|
138
|
-
## License
|
|
139
|
-
|
|
140
|
-
MIT - see [LICENSE](./LICENSE) for details
|
|
101
|
+
- Next.js >= 15
|
|
102
|
+
- React >= 19
|
|
103
|
+
- Tailwind CSS >= 4
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.21",
|
|
4
4
|
"description": "Pre-built dashboard layouts, authentication pages, and admin templates for Next.js applications with Tailwind CSS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"layouts",
|
|
@@ -85,9 +85,9 @@
|
|
|
85
85
|
"check": "tsc --noEmit"
|
|
86
86
|
},
|
|
87
87
|
"peerDependencies": {
|
|
88
|
-
"@djangocfg/api": "^1.4.
|
|
89
|
-
"@djangocfg/og-image": "^1.4.
|
|
90
|
-
"@djangocfg/ui": "^1.4.
|
|
88
|
+
"@djangocfg/api": "^1.4.21",
|
|
89
|
+
"@djangocfg/og-image": "^1.4.21",
|
|
90
|
+
"@djangocfg/ui": "^1.4.21",
|
|
91
91
|
"@hookform/resolvers": "^5.2.0",
|
|
92
92
|
"consola": "^3.4.2",
|
|
93
93
|
"lucide-react": "^0.468.0",
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"vidstack": "0.6.15"
|
|
109
109
|
},
|
|
110
110
|
"devDependencies": {
|
|
111
|
-
"@djangocfg/typescript-config": "^1.4.
|
|
111
|
+
"@djangocfg/typescript-config": "^1.4.21",
|
|
112
112
|
"@types/node": "^24.7.2",
|
|
113
113
|
"@types/react": "19.2.2",
|
|
114
114
|
"@types/react-dom": "19.2.1",
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from 'react';
|
|
4
|
+
import { useForm, useWatch } from 'react-hook-form';
|
|
5
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
6
|
+
import { Schemas, type Schemas as SchemaTypes } from '@djangocfg/api';
|
|
7
|
+
import {
|
|
8
|
+
Form,
|
|
9
|
+
FormControl,
|
|
10
|
+
FormField,
|
|
11
|
+
FormItem,
|
|
12
|
+
FormLabel,
|
|
13
|
+
FormMessage,
|
|
14
|
+
Input,
|
|
15
|
+
Textarea,
|
|
16
|
+
Button,
|
|
17
|
+
useToast,
|
|
18
|
+
Card,
|
|
19
|
+
CardHeader,
|
|
20
|
+
CardTitle,
|
|
21
|
+
CardDescription,
|
|
22
|
+
CardContent,
|
|
23
|
+
cn,
|
|
24
|
+
useLocalStorage,
|
|
25
|
+
} from '@djangocfg/ui';
|
|
26
|
+
import { Send, CheckCircle2, ArrowLeft } from 'lucide-react';
|
|
27
|
+
import type { ContactFormTexts, LeadSubmissionResult } from './types';
|
|
28
|
+
import { DEFAULT_FORM_TEXTS } from './types';
|
|
29
|
+
import { ContactFormProvider, useContactForm } from './ContactFormProvider';
|
|
30
|
+
|
|
31
|
+
type FormData = SchemaTypes.LeadSubmissionRequest;
|
|
32
|
+
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// Props
|
|
35
|
+
// ============================================================================
|
|
36
|
+
|
|
37
|
+
export interface ContactFormProps {
|
|
38
|
+
/** API base URL for lead submission */
|
|
39
|
+
apiUrl: string;
|
|
40
|
+
/** Customizable texts */
|
|
41
|
+
texts?: Partial<ContactFormTexts>;
|
|
42
|
+
/** Show card wrapper (default: true) */
|
|
43
|
+
showCard?: boolean;
|
|
44
|
+
/** Submit button icon */
|
|
45
|
+
submitIcon?: React.ReactNode;
|
|
46
|
+
/** Additional className for form */
|
|
47
|
+
className?: string;
|
|
48
|
+
/** Reset form after successful submit (default: true) */
|
|
49
|
+
resetOnSuccess?: boolean;
|
|
50
|
+
/** Callback after successful submit */
|
|
51
|
+
onSuccess?: (result: LeadSubmissionResult) => void;
|
|
52
|
+
/** Callback on error */
|
|
53
|
+
onError?: (error: Error) => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// Component
|
|
58
|
+
// ============================================================================
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* ContactForm - Contact form using Django-CFG Lead API
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```tsx
|
|
65
|
+
* <ContactForm
|
|
66
|
+
* apiUrl="https://api.example.com"
|
|
67
|
+
* onSuccess={(result) => console.log('Lead ID:', result.lead_id)}
|
|
68
|
+
* />
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export function ContactForm({ apiUrl, ...props }: ContactFormProps) {
|
|
72
|
+
return (
|
|
73
|
+
<ContactFormProvider apiUrl={apiUrl}>
|
|
74
|
+
<ContactFormInner {...props} />
|
|
75
|
+
</ContactFormProvider>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ============================================================================
|
|
80
|
+
// Inner Form Component
|
|
81
|
+
// ============================================================================
|
|
82
|
+
|
|
83
|
+
const STORAGE_KEY = 'contact-form-draft';
|
|
84
|
+
|
|
85
|
+
type FormDraft = {
|
|
86
|
+
name: string;
|
|
87
|
+
email: string;
|
|
88
|
+
company: string;
|
|
89
|
+
subject: string;
|
|
90
|
+
message: string;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const emptyDraft: FormDraft = {
|
|
94
|
+
name: '',
|
|
95
|
+
email: '',
|
|
96
|
+
company: '',
|
|
97
|
+
subject: '',
|
|
98
|
+
message: '',
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
function ContactFormInner({
|
|
102
|
+
texts = {},
|
|
103
|
+
showCard = true,
|
|
104
|
+
submitIcon,
|
|
105
|
+
className,
|
|
106
|
+
resetOnSuccess = true,
|
|
107
|
+
onSuccess,
|
|
108
|
+
onError,
|
|
109
|
+
}: Omit<ContactFormProps, 'apiUrl'>) {
|
|
110
|
+
const { toast } = useToast();
|
|
111
|
+
const { submit, isSubmitting } = useContactForm();
|
|
112
|
+
const t = { ...DEFAULT_FORM_TEXTS, ...texts };
|
|
113
|
+
const [draft, setDraft, clearDraft] = useLocalStorage<FormDraft>(STORAGE_KEY, emptyDraft);
|
|
114
|
+
const [isSuccess, setIsSuccess] = useState(false);
|
|
115
|
+
|
|
116
|
+
const form = useForm<FormData>({
|
|
117
|
+
resolver: zodResolver(Schemas.LeadSubmissionRequestSchema),
|
|
118
|
+
defaultValues: {
|
|
119
|
+
...emptyDraft,
|
|
120
|
+
...draft,
|
|
121
|
+
site_url: typeof window !== 'undefined' ? window.location.href : '',
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Watch form values and save to localStorage
|
|
126
|
+
const watchedValues = useWatch({ control: form.control });
|
|
127
|
+
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
const { name, email, company, subject, message } = watchedValues;
|
|
130
|
+
if (name || email || company || subject || message) {
|
|
131
|
+
setDraft({
|
|
132
|
+
name: name || '',
|
|
133
|
+
email: email || '',
|
|
134
|
+
company: company || '',
|
|
135
|
+
subject: subject || '',
|
|
136
|
+
message: message || '',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}, [watchedValues, setDraft]);
|
|
140
|
+
|
|
141
|
+
const handleSubmit = async (data: FormData) => {
|
|
142
|
+
try {
|
|
143
|
+
const result = await submit(data);
|
|
144
|
+
if (resetOnSuccess) {
|
|
145
|
+
form.reset();
|
|
146
|
+
clearDraft();
|
|
147
|
+
}
|
|
148
|
+
setIsSuccess(true);
|
|
149
|
+
onSuccess?.(result);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
152
|
+
toast({ title: t.errorTitle, description: err.message || t.errorMessage, variant: 'destructive' });
|
|
153
|
+
onError?.(err);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const handleReset = () => {
|
|
158
|
+
setIsSuccess(false);
|
|
159
|
+
form.reset();
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Success state
|
|
163
|
+
if (isSuccess) {
|
|
164
|
+
const successContent = (
|
|
165
|
+
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
166
|
+
<div className="rounded-full bg-green-100 p-4 mb-6">
|
|
167
|
+
<CheckCircle2 className="h-12 w-12 text-green-600" />
|
|
168
|
+
</div>
|
|
169
|
+
<h3 className="text-2xl font-semibold mb-2">{t.successTitle}</h3>
|
|
170
|
+
<p className="text-muted-foreground mb-6 max-w-sm">{t.successMessage}</p>
|
|
171
|
+
<Button variant="outline" onClick={handleReset}>
|
|
172
|
+
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
173
|
+
Send another message
|
|
174
|
+
</Button>
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (!showCard) return successContent;
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<Card>
|
|
182
|
+
<CardContent className="pt-6">{successContent}</CardContent>
|
|
183
|
+
</Card>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Form
|
|
188
|
+
const formContent = (
|
|
189
|
+
<Form {...form}>
|
|
190
|
+
<form onSubmit={form.handleSubmit(handleSubmit)} className={cn('space-y-4', className)}>
|
|
191
|
+
<FormField
|
|
192
|
+
control={form.control}
|
|
193
|
+
name="name"
|
|
194
|
+
render={({ field }) => (
|
|
195
|
+
<FormItem>
|
|
196
|
+
<FormLabel>Name *</FormLabel>
|
|
197
|
+
<FormControl>
|
|
198
|
+
<Input placeholder="Your name" {...field} />
|
|
199
|
+
</FormControl>
|
|
200
|
+
<FormMessage />
|
|
201
|
+
</FormItem>
|
|
202
|
+
)}
|
|
203
|
+
/>
|
|
204
|
+
|
|
205
|
+
<FormField
|
|
206
|
+
control={form.control}
|
|
207
|
+
name="email"
|
|
208
|
+
render={({ field }) => (
|
|
209
|
+
<FormItem>
|
|
210
|
+
<FormLabel>Email *</FormLabel>
|
|
211
|
+
<FormControl>
|
|
212
|
+
<Input type="email" placeholder="your@email.com" {...field} />
|
|
213
|
+
</FormControl>
|
|
214
|
+
<FormMessage />
|
|
215
|
+
</FormItem>
|
|
216
|
+
)}
|
|
217
|
+
/>
|
|
218
|
+
|
|
219
|
+
<FormField
|
|
220
|
+
control={form.control}
|
|
221
|
+
name="company"
|
|
222
|
+
render={({ field }) => (
|
|
223
|
+
<FormItem>
|
|
224
|
+
<FormLabel>Company</FormLabel>
|
|
225
|
+
<FormControl>
|
|
226
|
+
<Input placeholder="Your company (optional)" {...field} value={field.value ?? ''} />
|
|
227
|
+
</FormControl>
|
|
228
|
+
<FormMessage />
|
|
229
|
+
</FormItem>
|
|
230
|
+
)}
|
|
231
|
+
/>
|
|
232
|
+
|
|
233
|
+
<FormField
|
|
234
|
+
control={form.control}
|
|
235
|
+
name="subject"
|
|
236
|
+
render={({ field }) => (
|
|
237
|
+
<FormItem>
|
|
238
|
+
<FormLabel>Subject</FormLabel>
|
|
239
|
+
<FormControl>
|
|
240
|
+
<Input placeholder="What is this about?" {...field} value={field.value ?? ''} />
|
|
241
|
+
</FormControl>
|
|
242
|
+
<FormMessage />
|
|
243
|
+
</FormItem>
|
|
244
|
+
)}
|
|
245
|
+
/>
|
|
246
|
+
|
|
247
|
+
<FormField
|
|
248
|
+
control={form.control}
|
|
249
|
+
name="message"
|
|
250
|
+
render={({ field }) => (
|
|
251
|
+
<FormItem>
|
|
252
|
+
<FormLabel>Message *</FormLabel>
|
|
253
|
+
<FormControl>
|
|
254
|
+
<Textarea placeholder="Your message..." style={{ minHeight: '120px' }} {...field} />
|
|
255
|
+
</FormControl>
|
|
256
|
+
<FormMessage />
|
|
257
|
+
</FormItem>
|
|
258
|
+
)}
|
|
259
|
+
/>
|
|
260
|
+
|
|
261
|
+
<input type="hidden" {...form.register('site_url')} />
|
|
262
|
+
|
|
263
|
+
<Button type="submit" className="w-full" disabled={isSubmitting}>
|
|
264
|
+
{isSubmitting ? t.loadingText : t.submitText}
|
|
265
|
+
{!isSubmitting && (submitIcon || <Send className="ml-2 h-4 w-4" />)}
|
|
266
|
+
</Button>
|
|
267
|
+
</form>
|
|
268
|
+
</Form>
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
if (!showCard) return formContent;
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<Card>
|
|
275
|
+
<CardHeader>
|
|
276
|
+
<CardTitle>{t.title}</CardTitle>
|
|
277
|
+
<CardDescription>{t.description}</CardDescription>
|
|
278
|
+
</CardHeader>
|
|
279
|
+
<CardContent>{formContent}</CardContent>
|
|
280
|
+
</Card>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
|
|
4
|
+
import { configureAPI, createLeadsSubmitCreate, Enums } from '@djangocfg/api';
|
|
5
|
+
import type {
|
|
6
|
+
ContactFormContextValue,
|
|
7
|
+
ContactFormProviderProps,
|
|
8
|
+
LeadSubmissionData,
|
|
9
|
+
LeadSubmissionResult,
|
|
10
|
+
} from './types';
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Context
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
const ContactFormContext = createContext<ContactFormContextValue | undefined>(undefined);
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Helpers
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
const CONTACT_TYPE_MAP: Record<string, Enums.LeadSubmissionRequestContactType> = {
|
|
23
|
+
email: Enums.LeadSubmissionRequestContactType.EMAIL,
|
|
24
|
+
telegram: Enums.LeadSubmissionRequestContactType.TELEGRAM,
|
|
25
|
+
whatsapp: Enums.LeadSubmissionRequestContactType.WHATSAPP,
|
|
26
|
+
phone: Enums.LeadSubmissionRequestContactType.PHONE,
|
|
27
|
+
other: Enums.LeadSubmissionRequestContactType.OTHER,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// Provider
|
|
32
|
+
// ============================================================================
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* ContactFormProvider - Configures API and provides submit functionality
|
|
36
|
+
*
|
|
37
|
+
* Uses the generated Django-CFG API client for lead submission.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* import { ContactFormProvider, ContactForm } from '@djangocfg/layouts';
|
|
42
|
+
*
|
|
43
|
+
* <ContactFormProvider apiUrl="https://api.example.com">
|
|
44
|
+
* <ContactForm fields={fields} />
|
|
45
|
+
* </ContactFormProvider>
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function ContactFormProvider({ children, apiUrl }: ContactFormProviderProps) {
|
|
49
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
50
|
+
const [error, setError] = useState<Error | null>(null);
|
|
51
|
+
const [lastResponse, setLastResponse] = useState<LeadSubmissionResult | null>(null);
|
|
52
|
+
|
|
53
|
+
// Configure API on mount
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
configureAPI({ baseUrl: apiUrl });
|
|
56
|
+
}, [apiUrl]);
|
|
57
|
+
|
|
58
|
+
// Submit function using generated fetcher
|
|
59
|
+
const submit = useCallback(async (data: LeadSubmissionData): Promise<LeadSubmissionResult> => {
|
|
60
|
+
setIsSubmitting(true);
|
|
61
|
+
setError(null);
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
// Use the generated fetcher which handles Zod validation
|
|
65
|
+
const response = await createLeadsSubmitCreate({
|
|
66
|
+
name: data.name,
|
|
67
|
+
email: data.email,
|
|
68
|
+
message: data.message,
|
|
69
|
+
company: data.company ?? undefined,
|
|
70
|
+
company_site: data.company_site ?? undefined,
|
|
71
|
+
contact_type: data.contact_type ? CONTACT_TYPE_MAP[data.contact_type] : undefined,
|
|
72
|
+
contact_value: data.contact_value ?? undefined,
|
|
73
|
+
subject: data.subject ?? undefined,
|
|
74
|
+
extra: data.extra ?? undefined,
|
|
75
|
+
site_url: data.site_url,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const result: LeadSubmissionResult = {
|
|
79
|
+
success: response.success,
|
|
80
|
+
message: response.message,
|
|
81
|
+
lead_id: response.lead_id,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
setLastResponse(result);
|
|
85
|
+
return result;
|
|
86
|
+
} catch (err) {
|
|
87
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
88
|
+
setError(error);
|
|
89
|
+
throw error;
|
|
90
|
+
} finally {
|
|
91
|
+
setIsSubmitting(false);
|
|
92
|
+
}
|
|
93
|
+
}, []);
|
|
94
|
+
|
|
95
|
+
// Reset error state
|
|
96
|
+
const resetError = useCallback(() => {
|
|
97
|
+
setError(null);
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
const value: ContactFormContextValue = {
|
|
101
|
+
apiUrl,
|
|
102
|
+
submit,
|
|
103
|
+
isSubmitting,
|
|
104
|
+
error,
|
|
105
|
+
lastResponse,
|
|
106
|
+
resetError,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<ContactFormContext.Provider value={value}>
|
|
111
|
+
{children}
|
|
112
|
+
</ContactFormContext.Provider>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ============================================================================
|
|
117
|
+
// Hooks
|
|
118
|
+
// ============================================================================
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* useContactForm - Access contact form context
|
|
122
|
+
*
|
|
123
|
+
* Must be used within ContactFormProvider
|
|
124
|
+
*/
|
|
125
|
+
export function useContactForm(): ContactFormContextValue {
|
|
126
|
+
const context = useContext(ContactFormContext);
|
|
127
|
+
if (!context) {
|
|
128
|
+
throw new Error('useContactForm must be used within ContactFormProvider');
|
|
129
|
+
}
|
|
130
|
+
return context;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* useContactFormOptional - Access contact form context (optional)
|
|
135
|
+
*
|
|
136
|
+
* Returns null if not within ContactFormProvider
|
|
137
|
+
*/
|
|
138
|
+
export function useContactFormOptional(): ContactFormContextValue | null {
|
|
139
|
+
return useContext(ContactFormContext) ?? null;
|
|
140
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardHeader,
|
|
7
|
+
CardTitle,
|
|
8
|
+
CardContent,
|
|
9
|
+
Button,
|
|
10
|
+
cn,
|
|
11
|
+
} from '@djangocfg/ui';
|
|
12
|
+
import type { ContactInfoProps, ContactDetail } from './types';
|
|
13
|
+
import { DEFAULT_INFO_TITLE } from './types';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ContactInfo - Display contact information with optional action card
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* import { ContactInfo } from '@djangocfg/layouts';
|
|
21
|
+
* import { Mail, Phone, MapPin, Calendar } from 'lucide-react';
|
|
22
|
+
*
|
|
23
|
+
* const details = [
|
|
24
|
+
* { icon: <Mail />, label: 'Email', value: 'hello@example.com', href: 'mailto:hello@example.com' },
|
|
25
|
+
* { icon: <Phone />, label: 'Phone', value: '+1 234 567 890', href: 'tel:+1234567890' },
|
|
26
|
+
* { icon: <MapPin />, label: 'Address', value: '123 Main St, City' },
|
|
27
|
+
* ];
|
|
28
|
+
*
|
|
29
|
+
* <ContactInfo
|
|
30
|
+
* details={details}
|
|
31
|
+
* action={{
|
|
32
|
+
* title: 'Schedule a Meeting',
|
|
33
|
+
* description: 'Book a 30-minute call',
|
|
34
|
+
* button: {
|
|
35
|
+
* icon: <Calendar />,
|
|
36
|
+
* label: 'Book Now',
|
|
37
|
+
* href: 'https://calendly.com/...',
|
|
38
|
+
* },
|
|
39
|
+
* }}
|
|
40
|
+
* />
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function ContactInfo({
|
|
44
|
+
details,
|
|
45
|
+
title = DEFAULT_INFO_TITLE,
|
|
46
|
+
action,
|
|
47
|
+
className,
|
|
48
|
+
}: ContactInfoProps) {
|
|
49
|
+
const renderDetail = (detail: ContactDetail, index: number) => {
|
|
50
|
+
const content = (
|
|
51
|
+
<div className="flex items-start gap-3">
|
|
52
|
+
<div className="text-muted-foreground mt-0.5">{detail.icon}</div>
|
|
53
|
+
<div>
|
|
54
|
+
<p className="text-sm text-muted-foreground">{detail.label}</p>
|
|
55
|
+
<p className="font-medium">{detail.value}</p>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
if (detail.href) {
|
|
61
|
+
return (
|
|
62
|
+
<a
|
|
63
|
+
key={index}
|
|
64
|
+
href={detail.href}
|
|
65
|
+
target={detail.external !== false ? '_blank' : undefined}
|
|
66
|
+
rel={detail.external !== false ? 'noopener noreferrer' : undefined}
|
|
67
|
+
className="block hover:bg-muted/50 rounded-lg p-2 -m-2 transition-colors"
|
|
68
|
+
>
|
|
69
|
+
{content}
|
|
70
|
+
</a>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return <div key={index}>{content}</div>;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className={cn('space-y-6', className)}>
|
|
79
|
+
{/* Contact Details Card */}
|
|
80
|
+
<Card>
|
|
81
|
+
<CardHeader>
|
|
82
|
+
<CardTitle>{title}</CardTitle>
|
|
83
|
+
</CardHeader>
|
|
84
|
+
<CardContent className="space-y-4">
|
|
85
|
+
{details.map(renderDetail)}
|
|
86
|
+
</CardContent>
|
|
87
|
+
</Card>
|
|
88
|
+
|
|
89
|
+
{/* Action Card */}
|
|
90
|
+
{action && (
|
|
91
|
+
<Card className="bg-primary/5 border-primary/20">
|
|
92
|
+
<CardContent className="pt-6">
|
|
93
|
+
<h3 className="font-semibold mb-2">{action.title}</h3>
|
|
94
|
+
{action.description && (
|
|
95
|
+
<p className="text-sm text-muted-foreground mb-4">
|
|
96
|
+
{action.description}
|
|
97
|
+
</p>
|
|
98
|
+
)}
|
|
99
|
+
<Button asChild className="w-full">
|
|
100
|
+
<a
|
|
101
|
+
href={action.button.href}
|
|
102
|
+
target={action.button.external !== false ? '_blank' : undefined}
|
|
103
|
+
rel={action.button.external !== false ? 'noopener noreferrer' : undefined}
|
|
104
|
+
>
|
|
105
|
+
{action.button.icon}
|
|
106
|
+
<span className="ml-2">{action.button.label}</span>
|
|
107
|
+
</a>
|
|
108
|
+
</Button>
|
|
109
|
+
</CardContent>
|
|
110
|
+
</Card>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Mail, MapPin, Calendar } from 'lucide-react';
|
|
5
|
+
import { ContactForm } from './ContactForm';
|
|
6
|
+
import { ContactInfo } from './ContactInfo';
|
|
7
|
+
import type { ContactDetail, LeadSubmissionResult } from './types';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Config
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
const isDev = process.env.NODE_ENV === 'development';
|
|
14
|
+
|
|
15
|
+
const DEFAULT_CONFIG = {
|
|
16
|
+
apiUrl: isDev ? 'http://localhost:8000' : 'https://api.reforms.ai',
|
|
17
|
+
email: 'markolofsen@gmail.com',
|
|
18
|
+
calendly: 'https://calendly.com/markolofsen/meeting',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Props
|
|
23
|
+
// ============================================================================
|
|
24
|
+
|
|
25
|
+
export interface ContactPageProps {
|
|
26
|
+
/** Override API URL */
|
|
27
|
+
apiUrl?: string;
|
|
28
|
+
/** Override email */
|
|
29
|
+
email?: string;
|
|
30
|
+
/** Override calendly link */
|
|
31
|
+
calendlyUrl?: string;
|
|
32
|
+
/** Page title */
|
|
33
|
+
title?: React.ReactNode;
|
|
34
|
+
/** Page subtitle */
|
|
35
|
+
subtitle?: string;
|
|
36
|
+
/** Location text */
|
|
37
|
+
location?: string;
|
|
38
|
+
/** Hide calendly action card */
|
|
39
|
+
hideCalendly?: boolean;
|
|
40
|
+
/** Additional className */
|
|
41
|
+
className?: string;
|
|
42
|
+
/** Callback after successful submit */
|
|
43
|
+
onSuccess?: (result: LeadSubmissionResult) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// Component
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* ContactPage - Pre-configured contact page component
|
|
52
|
+
*
|
|
53
|
+
* Ready to use in any project with sensible defaults.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```tsx
|
|
57
|
+
* // Minimal usage - just drop it in
|
|
58
|
+
* <ContactPage />
|
|
59
|
+
*
|
|
60
|
+
* // With custom title
|
|
61
|
+
* <ContactPage
|
|
62
|
+
* title={<>Contact <span className="text-primary">Us</span></>}
|
|
63
|
+
* subtitle="We'd love to hear from you"
|
|
64
|
+
* />
|
|
65
|
+
*
|
|
66
|
+
* // Override defaults
|
|
67
|
+
* <ContactPage
|
|
68
|
+
* apiUrl="https://api.myproject.com"
|
|
69
|
+
* email="hello@myproject.com"
|
|
70
|
+
* />
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export function ContactPage({
|
|
74
|
+
apiUrl = DEFAULT_CONFIG.apiUrl,
|
|
75
|
+
email = DEFAULT_CONFIG.email,
|
|
76
|
+
calendlyUrl = DEFAULT_CONFIG.calendly,
|
|
77
|
+
title = 'Get in Touch',
|
|
78
|
+
subtitle = "Have a question or want to work together? We'd love to hear from you.",
|
|
79
|
+
location = 'Remote-first team',
|
|
80
|
+
hideCalendly = false,
|
|
81
|
+
className,
|
|
82
|
+
onSuccess,
|
|
83
|
+
}: ContactPageProps) {
|
|
84
|
+
const contactDetails: ContactDetail[] = [
|
|
85
|
+
{
|
|
86
|
+
icon: <Mail className="h-5 w-5" />,
|
|
87
|
+
label: 'Email',
|
|
88
|
+
value: email,
|
|
89
|
+
href: `mailto:${email}`,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
icon: <MapPin className="h-5 w-5" />,
|
|
93
|
+
label: 'Location',
|
|
94
|
+
value: location,
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div className={className}>
|
|
100
|
+
{/* Header */}
|
|
101
|
+
<div className="text-center mb-12">
|
|
102
|
+
<h1 className="text-4xl md:text-5xl font-bold mb-4">
|
|
103
|
+
{typeof title === 'string' ? (
|
|
104
|
+
title
|
|
105
|
+
) : (
|
|
106
|
+
title
|
|
107
|
+
)}
|
|
108
|
+
</h1>
|
|
109
|
+
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
|
110
|
+
{subtitle}
|
|
111
|
+
</p>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{/* Content Grid */}
|
|
115
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
116
|
+
{/* Contact Form */}
|
|
117
|
+
<div className="lg:col-span-2">
|
|
118
|
+
<ContactForm apiUrl={apiUrl} onSuccess={onSuccess} />
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* Contact Info */}
|
|
122
|
+
<div>
|
|
123
|
+
<ContactInfo
|
|
124
|
+
details={contactDetails}
|
|
125
|
+
action={hideCalendly ? undefined : {
|
|
126
|
+
title: 'Schedule a Meeting',
|
|
127
|
+
description: 'Book a time that works for you',
|
|
128
|
+
button: {
|
|
129
|
+
icon: <Calendar className="h-4 w-4" />,
|
|
130
|
+
label: 'Book a Call',
|
|
131
|
+
href: calendlyUrl,
|
|
132
|
+
},
|
|
133
|
+
}}
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ContactForm - Contact form using Django-CFG Lead API
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
// Components
|
|
6
|
+
export { ContactForm, type ContactFormProps } from './ContactForm';
|
|
7
|
+
export { ContactInfo } from './ContactInfo';
|
|
8
|
+
export { ContactPage, type ContactPageProps } from './ContactPage';
|
|
9
|
+
|
|
10
|
+
// Provider & Hooks
|
|
11
|
+
export {
|
|
12
|
+
ContactFormProvider,
|
|
13
|
+
useContactForm,
|
|
14
|
+
useContactFormOptional,
|
|
15
|
+
} from './ContactFormProvider';
|
|
16
|
+
|
|
17
|
+
// Types
|
|
18
|
+
export type {
|
|
19
|
+
ContactFormProviderProps,
|
|
20
|
+
ContactFormContextValue,
|
|
21
|
+
LeadSubmissionData,
|
|
22
|
+
LeadSubmissionResult,
|
|
23
|
+
UseContactFormReturn,
|
|
24
|
+
ContactFormTexts,
|
|
25
|
+
ContactDetail,
|
|
26
|
+
ContactAction,
|
|
27
|
+
ContactInfoProps,
|
|
28
|
+
} from './types';
|
|
29
|
+
|
|
30
|
+
// Constants
|
|
31
|
+
export { DEFAULT_FORM_TEXTS, DEFAULT_INFO_TITLE } from './types';
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { Schemas } from '@djangocfg/api';
|
|
3
|
+
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Re-export API Types
|
|
6
|
+
// ============================================================================
|
|
7
|
+
|
|
8
|
+
/** Lead submission request data - uses generated API type */
|
|
9
|
+
export type LeadSubmissionData = Schemas.LeadSubmissionRequest;
|
|
10
|
+
|
|
11
|
+
/** Lead submission response - uses generated API type */
|
|
12
|
+
export type LeadSubmissionResult = Schemas.LeadSubmissionResponse;
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Provider Types
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
export interface ContactFormProviderProps {
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
/**
|
|
21
|
+
* API base URL for lead submission
|
|
22
|
+
* @example "https://api.example.com"
|
|
23
|
+
*/
|
|
24
|
+
apiUrl: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Hook Types
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
export interface UseContactFormReturn {
|
|
32
|
+
/** Submit lead data */
|
|
33
|
+
submit: (data: LeadSubmissionData) => Promise<LeadSubmissionResult>;
|
|
34
|
+
/** Is currently submitting */
|
|
35
|
+
isSubmitting: boolean;
|
|
36
|
+
/** Last error */
|
|
37
|
+
error: Error | null;
|
|
38
|
+
/** Last successful response */
|
|
39
|
+
lastResponse: LeadSubmissionResult | null;
|
|
40
|
+
/** Reset error state */
|
|
41
|
+
resetError: () => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// Context Types
|
|
46
|
+
// ============================================================================
|
|
47
|
+
|
|
48
|
+
export interface ContactFormContextValue extends UseContactFormReturn {
|
|
49
|
+
/** API base URL */
|
|
50
|
+
apiUrl: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ============================================================================
|
|
54
|
+
// Form Texts
|
|
55
|
+
// ============================================================================
|
|
56
|
+
|
|
57
|
+
export interface ContactFormTexts {
|
|
58
|
+
title?: string;
|
|
59
|
+
description?: string;
|
|
60
|
+
submitText?: string;
|
|
61
|
+
loadingText?: string;
|
|
62
|
+
successTitle?: string;
|
|
63
|
+
successMessage?: string;
|
|
64
|
+
errorTitle?: string;
|
|
65
|
+
errorMessage?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const DEFAULT_FORM_TEXTS: Required<ContactFormTexts> = {
|
|
69
|
+
title: 'Send us a message',
|
|
70
|
+
description: "Fill out the form below and we'll get back to you as soon as possible",
|
|
71
|
+
submitText: 'Send Message',
|
|
72
|
+
loadingText: 'Sending...',
|
|
73
|
+
successTitle: 'Message Sent!',
|
|
74
|
+
successMessage: "We'll get back to you within 24 hours.",
|
|
75
|
+
errorTitle: 'Error',
|
|
76
|
+
errorMessage: 'Failed to send message. Please try again.',
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// ============================================================================
|
|
80
|
+
// Contact Info Types
|
|
81
|
+
// ============================================================================
|
|
82
|
+
|
|
83
|
+
export interface ContactDetail {
|
|
84
|
+
icon: ReactNode;
|
|
85
|
+
label: string;
|
|
86
|
+
value: string;
|
|
87
|
+
href?: string;
|
|
88
|
+
external?: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ContactAction {
|
|
92
|
+
icon: ReactNode;
|
|
93
|
+
label: string;
|
|
94
|
+
href: string;
|
|
95
|
+
external?: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ContactInfoProps {
|
|
99
|
+
details: ContactDetail[];
|
|
100
|
+
title?: string;
|
|
101
|
+
action?: {
|
|
102
|
+
title: string;
|
|
103
|
+
description?: string;
|
|
104
|
+
button: ContactAction;
|
|
105
|
+
};
|
|
106
|
+
className?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const DEFAULT_INFO_TITLE = 'Contact Information';
|
package/src/snippets/index.ts
CHANGED