Ashraf Al-Kassem commited on
Commit
7d627c0
Β·
1 Parent(s): 49d2391

fix(website): Inject HTML shell and globals.css into root layout

Browse files
Website/next-env.d.ts CHANGED
@@ -1,6 +1,6 @@
1
  /// <reference types="next" />
2
  /// <reference types="next/image-types/global" />
3
- import "./.next/dev/types/routes.d.ts";
4
 
5
  // NOTE: This file should not be edited
6
  // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
 
1
  /// <reference types="next" />
2
  /// <reference types="next/image-types/global" />
3
+ import "./.next/types/routes.d.ts";
4
 
5
  // NOTE: This file should not be edited
6
  // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
Website/package-lock.json CHANGED
@@ -9,6 +9,7 @@
9
  "version": "0.1.0",
10
  "dependencies": {
11
  "clsx": "^2.1.1",
 
12
  "lucide-react": "^0.574.0",
13
  "next": "16.1.6",
14
  "react": "19.2.3",
@@ -17,6 +18,7 @@
17
  },
18
  "devDependencies": {
19
  "@tailwindcss/postcss": "^4",
 
20
  "@types/node": "^20",
21
  "@types/react": "^19",
22
  "@types/react-dom": "^19",
@@ -1588,6 +1590,13 @@
1588
  "dev": true,
1589
  "license": "MIT"
1590
  },
 
 
 
 
 
 
 
1591
  "node_modules/@types/json-schema": {
1592
  "version": "7.0.15",
1593
  "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -3349,6 +3358,7 @@
3349
  "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
3350
  "dev": true,
3351
  "license": "MIT",
 
3352
  "dependencies": {
3353
  "@rtsao/scc": "^1.1.0",
3354
  "array-includes": "^3.1.9",
@@ -4543,6 +4553,15 @@
4543
  "jiti": "lib/jiti-cli.mjs"
4544
  }
4545
  },
 
 
 
 
 
 
 
 
 
4546
  "node_modules/js-tokens": {
4547
  "version": "4.0.0",
4548
  "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
 
9
  "version": "0.1.0",
10
  "dependencies": {
11
  "clsx": "^2.1.1",
12
+ "js-cookie": "^3.0.5",
13
  "lucide-react": "^0.574.0",
14
  "next": "16.1.6",
15
  "react": "19.2.3",
 
18
  },
19
  "devDependencies": {
20
  "@tailwindcss/postcss": "^4",
21
+ "@types/js-cookie": "^3.0.6",
22
  "@types/node": "^20",
23
  "@types/react": "^19",
24
  "@types/react-dom": "^19",
 
1590
  "dev": true,
1591
  "license": "MIT"
1592
  },
1593
+ "node_modules/@types/js-cookie": {
1594
+ "version": "3.0.6",
1595
+ "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz",
1596
+ "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==",
1597
+ "dev": true,
1598
+ "license": "MIT"
1599
+ },
1600
  "node_modules/@types/json-schema": {
1601
  "version": "7.0.15",
1602
  "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
 
3358
  "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
3359
  "dev": true,
3360
  "license": "MIT",
3361
+ "peer": true,
3362
  "dependencies": {
3363
  "@rtsao/scc": "^1.1.0",
3364
  "array-includes": "^3.1.9",
 
4553
  "jiti": "lib/jiti-cli.mjs"
4554
  }
4555
  },
4556
+ "node_modules/js-cookie": {
4557
+ "version": "3.0.5",
4558
+ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
4559
+ "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
4560
+ "license": "MIT",
4561
+ "engines": {
4562
+ "node": ">=14"
4563
+ }
4564
+ },
4565
  "node_modules/js-tokens": {
4566
  "version": "4.0.0",
4567
  "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
Website/package.json CHANGED
@@ -11,6 +11,7 @@
11
  },
12
  "dependencies": {
13
  "clsx": "^2.1.1",
 
14
  "lucide-react": "^0.574.0",
15
  "next": "16.1.6",
16
  "react": "19.2.3",
@@ -19,6 +20,7 @@
19
  },
20
  "devDependencies": {
21
  "@tailwindcss/postcss": "^4",
 
22
  "@types/node": "^20",
23
  "@types/react": "^19",
24
  "@types/react-dom": "^19",
 
11
  },
12
  "dependencies": {
13
  "clsx": "^2.1.1",
14
+ "js-cookie": "^3.0.5",
15
  "lucide-react": "^0.574.0",
16
  "next": "16.1.6",
17
  "react": "19.2.3",
 
20
  },
21
  "devDependencies": {
22
  "@tailwindcss/postcss": "^4",
23
+ "@types/js-cookie": "^3.0.6",
24
  "@types/node": "^20",
25
  "@types/react": "^19",
26
  "@types/react-dom": "^19",
Website/src/app/layout.tsx CHANGED
@@ -1,12 +1,35 @@
 
 
 
1
  import MarketingHeader from "@/components/Header";
2
  import MarketingFooter from "@/components/Footer";
3
 
4
- export default function MarketingLayout({ children }: { children: React.ReactNode }) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  return (
6
- <div className="flex flex-col min-h-screen">
7
- <MarketingHeader />
8
- <main className="flex-1">{children}</main>
9
- <MarketingFooter />
10
- </div>
 
 
 
 
11
  );
12
  }
 
1
+ import type { Metadata } from "next";
2
+ import { Inter } from "next/font/google";
3
+ import "./globals.css";
4
  import MarketingHeader from "@/components/Header";
5
  import MarketingFooter from "@/components/Footer";
6
 
7
+ const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
8
+
9
+ export const metadata: Metadata = {
10
+ title: {
11
+ default: "LeadPilot β€” AI-Native Lead Capture, Qualification & Routing",
12
+ template: "%s | LeadPilot",
13
+ },
14
+ description:
15
+ "LeadPilot is the AI-native platform that captures, qualifies, and routes leads to the right rep automatically. Close more deals without growing headcount.",
16
+ openGraph: {
17
+ type: "website",
18
+ locale: "en_US",
19
+ siteName: "LeadPilot",
20
+ },
21
+ };
22
+
23
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
24
  return (
25
+ <html lang="en">
26
+ <body className={`${inter.variable} font-inter antialiased`}>
27
+ <div className="flex flex-col min-h-screen">
28
+ <MarketingHeader />
29
+ <main className="flex-1">{children}</main>
30
+ <MarketingFooter />
31
+ </div>
32
+ </body>
33
+ </html>
34
  );
35
  }
Website/src/lib/api.ts CHANGED
@@ -1,105 +1,266 @@
1
  /**
2
- * Catalog API client β€” fetches public data from the LeadPilot backend.
3
- * Used by Server Components with ISR (revalidate every 60s).
 
 
 
4
  */
5
 
6
- const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000";
7
 
8
- /** Base URL for the product app (login, signup, dashboard). */
9
- export const APP_URL = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000";
 
10
 
11
- // ── Types ────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
12
 
13
- export interface CatalogPlan {
14
- id: string;
15
- name: string;
16
- display_name: string;
17
- description: string | null;
18
- sort_order: number;
19
- entitlements: CatalogEntitlement[];
20
- }
21
 
22
- export interface CatalogEntitlement {
23
- module_key: string;
24
- hard_limit: number | null; // null = unlimited
25
  }
26
 
27
- export interface CatalogModule {
28
- key: string;
29
- label: string;
30
- is_enabled: boolean;
31
  }
32
 
33
- export interface CatalogProvider {
34
- key: string;
35
- label: string;
36
- description: string;
37
- icon_hint: string;
38
- fields: { name: string; label: string; type: string }[];
39
- }
40
 
41
- export interface CatalogTemplate {
42
- id: string;
43
- slug: string;
44
- name: string;
45
- description: string;
46
- category: string;
47
- industry_tags: string[];
48
- platforms: string[];
49
- required_integrations: string[];
50
- is_featured: boolean;
51
- clone_count: number;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  }
53
 
54
- export interface CatalogEnum {
55
- key: string;
56
- label: string;
 
 
 
 
 
 
 
 
57
  }
58
 
59
- // ── Generic fetcher ──────────────────────────────────────────────────
 
 
 
 
60
 
61
- interface Envelope<T> {
62
- success: boolean;
63
- data: T;
64
- error: string | null;
 
 
65
  }
66
 
67
- async function fetchCatalog<T>(path: string): Promise<T> {
68
- try {
69
- const res = await fetch(`${API_BASE}/api/v1/catalog${path}`, {
70
- next: { revalidate: 60 },
71
- });
72
- if (!res.ok) return [] as unknown as T;
73
- const json: Envelope<T> = await res.json();
74
- return json.success ? json.data : ([] as unknown as T);
75
- } catch {
76
- return [] as unknown as T;
77
- }
78
  }
79
 
80
- // ── Typed fetchers ───────────────────────────────────────────────────
81
 
82
  export async function getPlans(): Promise<CatalogPlan[]> {
83
- return fetchCatalog<CatalogPlan[]>("/plans");
 
 
 
 
84
  }
85
 
86
  export async function getModules(): Promise<CatalogModule[]> {
87
- return fetchCatalog<CatalogModule[]>("/modules");
 
 
 
 
88
  }
89
 
90
  export async function getIntegrationProviders(): Promise<CatalogProvider[]> {
91
- return fetchCatalog<CatalogProvider[]>("/integration-providers");
 
 
 
 
92
  }
93
 
94
  export async function getPublicTemplates(category?: string): Promise<CatalogTemplate[]> {
95
- const params = category ? `?category=${encodeURIComponent(category)}` : "";
96
- return fetchCatalog<CatalogTemplate[]>(`/templates${params}`);
 
 
 
 
 
 
 
 
 
97
  }
98
 
99
  export async function getTemplateCategories(): Promise<CatalogEnum[]> {
100
- return fetchCatalog<CatalogEnum[]>("/template-categories");
 
 
 
 
101
  }
102
 
103
  export async function getTemplatePlatforms(): Promise<CatalogEnum[]> {
104
- return fetchCatalog<CatalogEnum[]>("/template-platforms");
 
 
 
 
105
  }
 
1
  /**
2
+ * LeadPilot Robust API Client
3
+ * - Handles JSON vs Text response detection
4
+ * - Automatic Authorization header injection
5
+ * - Workspace context management
6
+ * - Consistent error handling
7
  */
8
 
9
+ import { auth } from "./auth";
10
 
11
+ const getBaseUrl = () => {
12
+ if (process.env.NEXT_PUBLIC_API_BASE_URL) return process.env.NEXT_PUBLIC_API_BASE_URL;
13
+ if (process.env.NEXT_PUBLIC_API_URL) return process.env.NEXT_PUBLIC_API_URL;
14
 
15
+ if (typeof window !== "undefined") {
16
+ // Browser context: localhost dev hits backend directly; production uses
17
+ // relative paths so nginx can proxy /api/* to the backend.
18
+ if (window.location.hostname === "localhost") {
19
+ return "http://localhost:8000";
20
+ }
21
+ return ""; // relative URLs β€” works in any production deployment (HF, Docker, etc.)
22
+ }
23
+ // Server-side (SSR inside Docker): backend is accessible at 127.0.0.1:8000 internally.
24
+ return "http://127.0.0.1:8000";
25
+ };
26
 
27
+ const API_BASE_URL = getBaseUrl();
 
 
 
 
 
 
 
28
 
29
+ if (!API_BASE_URL && typeof window !== "undefined" && window.location.hostname === "localhost") {
30
+ console.warn("WARNING: NEXT_PUBLIC_API_BASE_URL not defined. Falling back to http://localhost:8000");
 
31
  }
32
 
33
+ export interface ApiResponse<T = any> {
34
+ success: boolean;
35
+ data?: T;
36
+ error?: string;
37
  }
38
 
39
+ class ApiClient {
40
+ private getToken(): string | null {
41
+ return auth.getToken();
42
+ }
 
 
 
43
 
44
+ private getWorkspaceId(): string | null {
45
+ return auth.getWorkspaceId();
46
+ }
47
+
48
+ async request<T = any>(
49
+ path: string,
50
+ options: RequestInit = {}
51
+ ): Promise<ApiResponse<T>> {
52
+ // If API_BASE_URL is empty, it defaults to relative paths (e.g., /api/v1/...)
53
+
54
+ // Safe base URL with /api/v1 prefix - ALWAYS absolute
55
+ const base = API_BASE_URL.replace(/\/$/, "");
56
+ const cleanPath = path.startsWith("/") ? path : `/${path}`;
57
+ const url = `${base}/api/v1${cleanPath}`;
58
+
59
+ const method = options.method || "GET";
60
+ console.log(`[API] ${method} ${url}`);
61
+
62
+ const token = this.getToken();
63
+ const workspaceId = this.getWorkspaceId();
64
+
65
+ const headers = new Headers(options.headers || {});
66
+
67
+ // Always inject Authorization if token exists
68
+ if (token) {
69
+ headers.set("Authorization", `Bearer ${token}`);
70
+ }
71
+
72
+ // Always inject X-Workspace-ID
73
+ if (workspaceId) {
74
+ headers.set("X-Workspace-ID", workspaceId);
75
+ }
76
+
77
+ if (!headers.has("Content-Type") && !(options.body instanceof FormData)) {
78
+ headers.set("Content-Type", "application/json");
79
+ }
80
+
81
+ try {
82
+ const response = await fetch(url, {
83
+ ...options,
84
+ headers,
85
+ });
86
+
87
+ // Handle 401 (expired/invalid session) β†’ logout
88
+ if (response.status === 401) {
89
+ if (typeof window !== "undefined") {
90
+ auth.logout();
91
+ }
92
+ return { success: false, error: "Session expired. Please login again." };
93
+ }
94
+
95
+ // Handle 403 (forbidden / module disabled / insufficient role) β†’ surface error, don't logout
96
+ if (response.status === 403) {
97
+ const contentType = response.headers.get("content-type");
98
+ if (contentType && contentType.includes("application/json")) {
99
+ const json = await response.json();
100
+ return { success: false, error: json.error || json.detail || "Access denied (403)." };
101
+ }
102
+ return { success: false, error: "Access denied (403)." };
103
+ }
104
+
105
+ const contentType = response.headers.get("content-type");
106
+
107
+ if (contentType && contentType.includes("application/json")) {
108
+ const json = await response.json();
109
+ if (!response.ok) {
110
+ return {
111
+ success: false,
112
+ error: json.error || json.detail || `API Error (${response.status})`
113
+ };
114
+ }
115
+ if (json.hasOwnProperty("success")) {
116
+ return json as ApiResponse<T>;
117
+ }
118
+ return { success: true, data: json as T };
119
+ } else {
120
+ const text = await response.text();
121
+ if (!response.ok) {
122
+ console.error("Non-JSON Error Response:", text.substring(0, 200));
123
+ return {
124
+ success: false,
125
+ error: `Backend Error (${response.status}): ${text.substring(0, 50)}...`
126
+ };
127
+ }
128
+ return { success: true, data: text as any };
129
+ }
130
+ } catch (error: any) {
131
+ console.error("API Connectivity Error:", error);
132
+ return {
133
+ success: false,
134
+ error: `Backend unreachable (${error.message || 'Network Error'}). Check API base URL or server status.`
135
+ };
136
+ }
137
+ }
138
+
139
+ get<T = any>(path: string, options?: RequestInit) {
140
+ return this.request<T>(path, { ...options, method: "GET" });
141
+ }
142
+
143
+ post<T = any>(path: string, body?: any, options?: RequestInit) {
144
+ return this.request<T>(path, {
145
+ ...options,
146
+ method: "POST",
147
+ body: body instanceof FormData ? body : JSON.stringify(body),
148
+ });
149
+ }
150
+
151
+ put<T = any>(path: string, body?: any, options?: RequestInit) {
152
+ return this.request<T>(path, {
153
+ ...options,
154
+ method: "PUT",
155
+ body: body instanceof FormData ? body : JSON.stringify(body),
156
+ });
157
+ }
158
+
159
+ patch<T = any>(path: string, body?: any, options?: RequestInit) {
160
+ return this.request<T>(path, {
161
+ ...options,
162
+ method: "PATCH",
163
+ body: body instanceof FormData ? body : JSON.stringify(body),
164
+ });
165
+ }
166
+
167
+ delete<T = any>(path: string, options?: RequestInit) {
168
+ return this.request<T>(path, { ...options, method: "DELETE" });
169
+ }
170
  }
171
 
172
+ export const apiClient = new ApiClient();
173
+
174
+ // ── Marketing Website Functions ──
175
+
176
+ export interface CatalogPlan {
177
+ id: string;
178
+ name: string;
179
+ display_name: string;
180
+ description: string | null;
181
+ sort_order: number;
182
+ entitlements: any[];
183
  }
184
 
185
+ export interface CatalogModule {
186
+ key: string;
187
+ label: string;
188
+ is_enabled: boolean;
189
+ }
190
 
191
+ export interface CatalogProvider {
192
+ key: string;
193
+ label: string;
194
+ description: string;
195
+ icon_hint: string;
196
+ fields: { name: string; label: string; type: string }[];
197
  }
198
 
199
+ export interface CatalogTemplate {
200
+ id: string;
201
+ slug: string;
202
+ name: string;
203
+ description: string;
204
+ category: string;
205
+ industry_tags: string[];
206
+ platforms: string[];
207
+ required_integrations: string[];
208
+ is_featured: boolean;
209
+ clone_count: number;
210
  }
211
 
212
+ export const APP_URL = "http://localhost:3000";
213
 
214
  export async function getPlans(): Promise<CatalogPlan[]> {
215
+ try {
216
+ const res = await fetch(`${API_BASE_URL}/api/v1/catalog/plans`, { next: { revalidate: 60 } });
217
+ const json = await res.json();
218
+ return json.success ? json.data : [];
219
+ } catch { return []; }
220
  }
221
 
222
  export async function getModules(): Promise<CatalogModule[]> {
223
+ try {
224
+ const res = await fetch(`${API_BASE_URL}/api/v1/catalog/modules`, { next: { revalidate: 60 } });
225
+ const json = await res.json();
226
+ return json.success ? json.data : [];
227
+ } catch { return []; }
228
  }
229
 
230
  export async function getIntegrationProviders(): Promise<CatalogProvider[]> {
231
+ try {
232
+ const res = await fetch(`${API_BASE_URL}/api/v1/catalog/integration-providers`, { next: { revalidate: 60 } });
233
+ const json = await res.json();
234
+ return json.success ? json.data : [];
235
+ } catch { return []; }
236
  }
237
 
238
  export async function getPublicTemplates(category?: string): Promise<CatalogTemplate[]> {
239
+ const params = category ? `?category=${encodeURIComponent(category)}` : "";
240
+ try {
241
+ const res = await fetch(`${API_BASE_URL}/api/v1/catalog/templates${params}`, { next: { revalidate: 60 } });
242
+ const json = await res.json();
243
+ return json.success ? json.data : [];
244
+ } catch { return []; }
245
+ }
246
+
247
+ export interface CatalogEnum {
248
+ key: string;
249
+ label: string;
250
  }
251
 
252
  export async function getTemplateCategories(): Promise<CatalogEnum[]> {
253
+ try {
254
+ const res = await fetch(`${API_BASE_URL}/api/v1/catalog/template-categories`, { next: { revalidate: 60 } });
255
+ const json = await res.json();
256
+ return json.success ? json.data : [];
257
+ } catch { return []; }
258
  }
259
 
260
  export async function getTemplatePlatforms(): Promise<CatalogEnum[]> {
261
+ try {
262
+ const res = await fetch(`${API_BASE_URL}/api/v1/catalog/template-platforms`, { next: { revalidate: 60 } });
263
+ const json = await res.json();
264
+ return json.success ? json.data : [];
265
+ } catch { return []; }
266
  }