@djangocfg/layouts 1.4.19 → 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 CHANGED
@@ -1,140 +1,103 @@
1
1
  # @djangocfg/layouts
2
2
 
3
- > Pre-built dashboard layouts, authentication pages, and admin templates for Next.js applications with Tailwind CSS
3
+ Pre-built layouts, auth, and snippets for Next.js + Tailwind CSS.
4
4
 
5
- [![npm version](https://img.shields.io/npm/v/@djangocfg/layouts.svg)](https://www.npmjs.com/package/@djangocfg/layouts)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
+ **Part of [DjangoCFG](https://djangocfg.com)** — modern Django framework for production-ready SaaS applications.
7
6
 
8
- **Part of [DjangoCFG](https://djangocfg.com)** — a modern Django framework for building production-ready SaaS applications. All `@djangocfg/*` packages are designed to work together, providing type-safe configuration, real-time features, and beautiful admin interfaces out of the box.
7
+ ## Install
9
8
 
10
- ---
9
+ ```bash
10
+ pnpm add @djangocfg/layouts
11
+ ```
11
12
 
12
- ## Overview
13
+ ## Layouts
13
14
 
14
- `@djangocfg/layouts` provides production-ready layout components for building admin dashboards, authentication flows, and application shells. Built with Next.js App Router and Tailwind CSS.
15
+ ```tsx
16
+ import { AppLayout } from '@djangocfg/layouts/layouts';
15
17
 
16
- ## Features
18
+ <AppLayout config={config}>{children}</AppLayout>
19
+ ```
17
20
 
18
- - **Dashboard Layouts** - Sidebar, header, and content area components
19
- - **Auth Pages** - Login, register, forgot password templates
20
- - **Auto Route Detection** - Automatically applies correct layout
21
- - **Responsive Design** - Mobile-first responsive layouts
22
- - **Authentication Context** - Built-in auth state management
23
- - **Video Player** - Integrated Vidstack player components
24
- - **SEO Component** - OG image generation, meta tags
25
- - **TypeScript** - Full type safety
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
- ## Installation
30
+ ## Auth
28
31
 
29
- ```bash
30
- npm install @djangocfg/layouts
31
- # or
32
- pnpm add @djangocfg/layouts
33
- # or
34
- yarn add @djangocfg/layouts
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
- ## Quick Start
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
- ### Basic Setup
48
+ ## Snippets
40
49
 
41
50
  ```tsx
42
- // app/layout.tsx
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
- ### Configuration
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
- // core/appLayoutConfig.ts
59
- import type { AppLayoutConfig } from '@djangocfg/layouts';
60
-
61
- export const appLayoutConfig: AppLayoutConfig = {
62
- app: {
63
- name: 'Your App',
64
- description: 'App description',
65
- logoPath: '/logo.svg',
66
- siteUrl: process.env.NEXT_PUBLIC_SITE_URL,
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
- ### Authentication
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
- ```tsx
86
- import { AuthProvider, useAuth } from '@djangocfg/layouts/auth';
87
- import { LoginForm } from '@djangocfg/layouts/auth';
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 | Description |
101
- |------|-------------|
90
+ | Path | Content |
91
+ |------|---------|
102
92
  | `@djangocfg/layouts` | Main exports |
103
93
  | `@djangocfg/layouts/layouts` | Layout components |
104
- | `@djangocfg/layouts/auth` | Auth components and context |
105
- | `@djangocfg/layouts/auth/hooks` | Auth hooks |
106
- | `@djangocfg/layouts/snippets` | Reusable code snippets |
107
- | `@djangocfg/layouts/utils` | Utility functions |
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.4.4
127
- - React >= 19.1.0
128
- - Tailwind CSS >= 4.0.0
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.19",
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.19",
89
- "@djangocfg/og-image": "^1.4.19",
90
- "@djangocfg/ui": "^1.4.19",
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.19",
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",
@@ -48,7 +48,10 @@ export interface CoreProvidersProps {
48
48
  */
49
49
  export function CoreProviders({ children, config, validation, cors, network, onError }: CoreProvidersProps) {
50
50
  return (
51
- <ThemeProvider>
51
+ <ThemeProvider
52
+ defaultTheme={config.theme?.defaultTheme}
53
+ storageKey={config.theme?.storageKey}
54
+ >
52
55
  <AuthProvider
53
56
  config={{
54
57
  apiUrl: config.api.baseUrl,
@@ -24,6 +24,14 @@ export interface AppLayoutConfig {
24
24
  };
25
25
  };
26
26
 
27
+ /** Theme configuration */
28
+ theme?: {
29
+ /** Default theme (default: 'light') */
30
+ defaultTheme?: 'light' | 'dark';
31
+ /** Storage key for theme persistence (default: 'theme') */
32
+ storageKey?: string;
33
+ };
34
+
27
35
  /** API configuration */
28
36
  api: {
29
37
  baseUrl: string;
@@ -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';
@@ -8,3 +8,4 @@ export type { ChatWidgetProps, ChatUIState } from './Chat';
8
8
  export * from './Breadcrumbs';
9
9
  export * from './AuthDialog';
10
10
  export * from './VideoPlayer';
11
+ export * from './ContactForm';