@djangocfg/layouts 1.4.26 → 1.4.27

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "1.4.26",
3
+ "version": "1.4.27",
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.26",
89
- "@djangocfg/og-image": "^1.4.26",
90
- "@djangocfg/ui": "^1.4.26",
88
+ "@djangocfg/api": "^1.4.27",
89
+ "@djangocfg/og-image": "^1.4.27",
90
+ "@djangocfg/ui": "^1.4.27",
91
91
  "@hookform/resolvers": "^5.2.0",
92
92
  "consola": "^3.4.2",
93
93
  "lucide-react": "^0.468.0",
@@ -109,7 +109,7 @@
109
109
  "vidstack": "0.6.15"
110
110
  },
111
111
  "devDependencies": {
112
- "@djangocfg/typescript-config": "^1.4.26",
112
+ "@djangocfg/typescript-config": "^1.4.27",
113
113
  "@types/node": "^24.7.2",
114
114
  "@types/react": "19.2.2",
115
115
  "@types/react-dom": "19.2.1",
@@ -58,17 +58,17 @@ export interface ContactFormProps {
58
58
  // ============================================================================
59
59
 
60
60
  /**
61
- * ContactForm - Contact form using Django-CFG Lead API
61
+ * ContactFormBase - Contact form using Django-CFG Lead API
62
+ *
63
+ * NOTE: Use ContactForm from index.ts which is dynamically imported (ssr: false)
62
64
  *
63
65
  * @example
64
66
  * ```tsx
65
- * <ContactForm
66
- * apiUrl="https://api.example.com"
67
- * onSuccess={(result) => console.log('Lead ID:', result.lead_id)}
68
- * />
67
+ * import { ContactForm } from '@djangocfg/layouts';
68
+ * <ContactForm apiUrl="https://api.example.com" />
69
69
  * ```
70
70
  */
71
- export function ContactForm({ apiUrl, ...props }: ContactFormProps) {
71
+ export function ContactFormBase({ apiUrl, ...props }: ContactFormProps) {
72
72
  return (
73
73
  <ContactFormProvider apiUrl={apiUrl}>
74
74
  <ContactFormInner {...props} />
@@ -112,20 +112,42 @@ function ContactFormInner({
112
112
  const t = { ...DEFAULT_FORM_TEXTS, ...texts };
113
113
  const [draft, setDraft, clearDraft] = useLocalStorage<FormDraft>(STORAGE_KEY, emptyDraft);
114
114
  const [isSuccess, setIsSuccess] = useState(false);
115
+ const [isHydrated, setIsHydrated] = useState(false);
115
116
 
116
117
  const form = useForm<FormData>({
117
118
  resolver: zodResolver(Schemas.LeadSubmissionRequestSchema),
119
+ // Start with empty values to match SSR
118
120
  defaultValues: {
119
121
  ...emptyDraft,
120
- ...draft,
121
- site_url: typeof window !== 'undefined' ? window.location.href : '',
122
+ site_url: '',
122
123
  },
123
124
  });
124
125
 
126
+ // Hydrate form with localStorage draft and site_url after mount
127
+ useEffect(() => {
128
+ if (isHydrated) return;
129
+ setIsHydrated(true);
130
+
131
+ // Apply draft from localStorage and set site_url
132
+ const currentValues = form.getValues();
133
+ const hasDraftData = draft.name || draft.email || draft.company || draft.subject || draft.message;
134
+
135
+ if (hasDraftData || !currentValues.site_url) {
136
+ form.reset({
137
+ ...emptyDraft,
138
+ ...draft,
139
+ site_url: window.location.href,
140
+ });
141
+ }
142
+ }, [draft, form, isHydrated]);
143
+
125
144
  // Watch form values and save to localStorage
126
145
  const watchedValues = useWatch({ control: form.control });
127
146
 
128
147
  useEffect(() => {
148
+ // Only save to localStorage after hydration to avoid unnecessary writes
149
+ if (!isHydrated) return;
150
+
129
151
  const { name, email, company, subject, message } = watchedValues;
130
152
  if (name || email || company || subject || message) {
131
153
  setDraft({
@@ -136,14 +158,30 @@ function ContactFormInner({
136
158
  message: message || '',
137
159
  });
138
160
  }
139
- }, [watchedValues, setDraft]);
161
+ }, [watchedValues, setDraft, isHydrated]);
140
162
 
141
163
  const handleSubmit = async (data: FormData) => {
142
164
  try {
143
165
  const result = await submit(data);
144
166
  if (resetOnSuccess) {
145
- form.reset();
146
- clearDraft();
167
+ // Keep contact info (name, email, company), clear only message content
168
+ const currentValues = form.getValues();
169
+ form.reset({
170
+ name: currentValues.name,
171
+ email: currentValues.email,
172
+ company: currentValues.company,
173
+ subject: '',
174
+ message: '',
175
+ site_url: currentValues.site_url,
176
+ });
177
+ // Update draft to keep contact info
178
+ setDraft({
179
+ name: currentValues.name || '',
180
+ email: currentValues.email || '',
181
+ company: currentValues.company || '',
182
+ subject: '',
183
+ message: '',
184
+ });
147
185
  }
148
186
  setIsSuccess(true);
149
187
  onSuccess?.(result);
@@ -156,7 +194,16 @@ function ContactFormInner({
156
194
 
157
195
  const handleReset = () => {
158
196
  setIsSuccess(false);
159
- form.reset();
197
+ // Keep contact info when returning to form
198
+ const currentDraft = draft;
199
+ form.reset({
200
+ name: currentDraft.name,
201
+ email: currentDraft.email,
202
+ company: currentDraft.company,
203
+ subject: '',
204
+ message: '',
205
+ site_url: typeof window !== 'undefined' ? window.location.href : '',
206
+ });
160
207
  };
161
208
 
162
209
  // Success state
@@ -2,7 +2,7 @@
2
2
 
3
3
  import React from 'react';
4
4
  import { Mail, MapPin, Calendar } from 'lucide-react';
5
- import { ContactForm } from './ContactForm';
5
+ import { ContactFormBase as ContactForm } from './ContactForm';
6
6
  import { ContactInfo } from './ContactInfo';
7
7
  import type { ContactDetail, LeadSubmissionResult } from './types';
8
8
 
@@ -48,29 +48,17 @@ export interface ContactPageProps {
48
48
  // ============================================================================
49
49
 
50
50
  /**
51
- * ContactPage - Pre-configured contact page component
51
+ * ContactPageBase - Pre-configured contact page component
52
52
  *
53
- * Ready to use in any project with sensible defaults.
53
+ * NOTE: Use ContactPage from index.ts which is dynamically imported (ssr: false)
54
54
  *
55
55
  * @example
56
56
  * ```tsx
57
- * // Minimal usage - just drop it in
57
+ * import { ContactPage } from '@djangocfg/layouts';
58
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
59
  * ```
72
60
  */
73
- export function ContactPage({
61
+ export function ContactPageBase({
74
62
  apiUrl = DEFAULT_CONFIG.apiUrl,
75
63
  email = DEFAULT_CONFIG.email,
76
64
  calendlyUrl = DEFAULT_CONFIG.calendly,
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Dynamic ContactForm components (ssr: false)
3
+ *
4
+ * Avoids hydration mismatch when using localStorage for form drafts.
5
+ * Components are loaded client-side only.
6
+ */
7
+
8
+ 'use client';
9
+
10
+ import dynamic from 'next/dynamic';
11
+ import { Skeleton } from '@djangocfg/ui';
12
+ import type { ContactFormProps } from './ContactForm';
13
+ import type { ContactPageProps } from './ContactPage';
14
+
15
+ function ContactFormSkeleton() {
16
+ return (
17
+ <div className="space-y-4">
18
+ <Skeleton className="w-full h-10" />
19
+ <Skeleton className="w-full h-10" />
20
+ <Skeleton className="w-full h-10" />
21
+ <Skeleton className="w-full h-24" />
22
+ <Skeleton className="w-full h-10" />
23
+ </div>
24
+ );
25
+ }
26
+
27
+ function ContactPageSkeleton() {
28
+ return (
29
+ <div>
30
+ <div className="text-center mb-8 md:mb-12">
31
+ <Skeleton className="w-64 h-12 mx-auto mb-4" />
32
+ <Skeleton className="w-96 h-6 mx-auto" />
33
+ </div>
34
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
35
+ <div className="lg:col-span-2">
36
+ <ContactFormSkeleton />
37
+ </div>
38
+ <div className="space-y-4">
39
+ <Skeleton className="w-full h-32" />
40
+ <Skeleton className="w-full h-24" />
41
+ </div>
42
+ </div>
43
+ </div>
44
+ );
45
+ }
46
+
47
+ export const ContactForm = dynamic<ContactFormProps>(
48
+ () => import('./ContactForm').then((mod) => mod.ContactFormBase),
49
+ { ssr: false, loading: () => <ContactFormSkeleton /> }
50
+ );
51
+
52
+ export const ContactPage = dynamic<ContactPageProps>(
53
+ () => import('./ContactPage').then((mod) => mod.ContactPageBase),
54
+ { ssr: false, loading: () => <ContactPageSkeleton /> }
55
+ );
@@ -2,10 +2,13 @@
2
2
  // ContactForm - Contact form using Django-CFG Lead API
3
3
  // ============================================================================
4
4
 
5
- // Components
6
- export { ContactForm, type ContactFormProps } from './ContactForm';
5
+ // Components (dynamic import, ssr: false - no hydration issues)
6
+ export { ContactForm, ContactPage } from './dynamic';
7
7
  export { ContactInfo } from './ContactInfo';
8
- export { ContactPage, type ContactPageProps } from './ContactPage';
8
+
9
+ // Types
10
+ export type { ContactFormProps } from './ContactForm';
11
+ export type { ContactPageProps } from './ContactPage';
9
12
 
10
13
  // Provider & Hooks
11
14
  export {