@foxpixel/react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,216 @@
1
+ # @foxpixel/react
2
+
3
+ React SDK for FoxPixel API - Headless integration for custom sites and portals.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @foxpixel/react
9
+ # or
10
+ yarn add @foxpixel/react
11
+ # or
12
+ pnpm add @foxpixel/react
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ### 1. Wrap your app with FoxPixelProvider
18
+
19
+ ```tsx
20
+ // app.tsx or _app.tsx
21
+ import { FoxPixelProvider } from '@foxpixel/react';
22
+
23
+ function App() {
24
+ return (
25
+ <FoxPixelProvider>
26
+ <YourApp />
27
+ </FoxPixelProvider>
28
+ );
29
+ }
30
+ ```
31
+
32
+ ### 2. Use hooks in your components
33
+
34
+ ```tsx
35
+ import { useServices } from '@foxpixel/react';
36
+
37
+ function ServicesPage() {
38
+ const { services, isLoading, error } = useServices({ active: true });
39
+
40
+ if (isLoading) return <div>Loading...</div>;
41
+ if (error) return <div>Error: {error.message}</div>;
42
+
43
+ return (
44
+ <div>
45
+ {services?.map(service => (
46
+ <div key={service.id}>
47
+ <h3>{service.name}</h3>
48
+ <p>{service.description}</p>
49
+ </div>
50
+ ))}
51
+ </div>
52
+ );
53
+ }
54
+ ```
55
+
56
+ ## Configuration
57
+
58
+ ### Environment Variables
59
+
60
+ ```env
61
+ # Client-side (public)
62
+ NEXT_PUBLIC_FOXPIXEL_API_URL=https://api.foxpixel.com
63
+
64
+ # Server-side only (NEVER expose in client)
65
+ FOXPIXEL_API_KEY=sk_live_your_key_here
66
+ FOXPIXEL_TENANT_ID=your_tenant_id_here
67
+ ```
68
+
69
+ ### Provider Configuration
70
+
71
+ ```tsx
72
+ <FoxPixelProvider
73
+ config={{
74
+ apiUrl: 'https://api.foxpixel.com',
75
+ // apiKey: Only use server-side (via proxy)
76
+ // tenantId: Optional, can be set per request
77
+ }}
78
+ >
79
+ <App />
80
+ </FoxPixelProvider>
81
+ ```
82
+
83
+ ## Security: Server-Side Proxy
84
+
85
+ **IMPORTANT:** API Keys should NEVER be exposed in client-side code.
86
+
87
+ Use Next.js API routes to proxy requests:
88
+
89
+ ```tsx
90
+ // pages/api/foxpixel/[...path].ts
91
+ import type { NextApiRequest, NextApiResponse } from 'next';
92
+
93
+ export default async function handler(
94
+ req: NextApiRequest,
95
+ res: NextApiResponse
96
+ ) {
97
+ const apiKey = process.env.FOXPIXEL_API_KEY;
98
+ const apiUrl = process.env.NEXT_PUBLIC_FOXPIXEL_API_URL;
99
+
100
+ // Proxy request with API Key
101
+ const response = await fetch(`${apiUrl}${req.url}`, {
102
+ method: req.method,
103
+ headers: {
104
+ 'Authorization': `Bearer ${apiKey}`,
105
+ 'Content-Type': 'application/json',
106
+ },
107
+ body: req.method !== 'GET' ? JSON.stringify(req.body) : undefined,
108
+ });
109
+
110
+ const data = await response.json();
111
+ res.status(response.status).json(data);
112
+ }
113
+ ```
114
+
115
+ Then configure SDK to use proxy:
116
+
117
+ ```tsx
118
+ <FoxPixelProvider
119
+ config={{
120
+ apiUrl: '/api/foxpixel', // Use proxy instead of direct API
121
+ }}
122
+ >
123
+ <App />
124
+ </FoxPixelProvider>
125
+ ```
126
+
127
+ ## Hooks
128
+
129
+ ### `useServices(options?)`
130
+
131
+ Fetch services from Projects module.
132
+
133
+ ```tsx
134
+ const { services, isLoading, error, refetch } = useServices({
135
+ category: 'pools',
136
+ active: true,
137
+ });
138
+ ```
139
+
140
+ ### `useLeadCapture()`
141
+
142
+ Capture leads (create lead in CRM).
143
+
144
+ ```tsx
145
+ const { captureLead, isLoading, error } = useLeadCapture();
146
+
147
+ await captureLead({
148
+ fullName: 'John Doe',
149
+ email: 'john@example.com',
150
+ phone: '+1234567890',
151
+ source: 'website',
152
+ });
153
+ ```
154
+
155
+ ### `useAuth()`
156
+
157
+ Authenticate End Users (clients of your tenant).
158
+
159
+ ```tsx
160
+ const { user, isAuthenticated, login, logout, isLoading } = useAuth();
161
+
162
+ // Login
163
+ await login({
164
+ email: 'user@example.com',
165
+ password: 'password123',
166
+ });
167
+
168
+ // Logout
169
+ await logout();
170
+ ```
171
+
172
+ ## End User Authentication
173
+
174
+ End Users are authenticated via httpOnly cookies (secure, XSS-resistant).
175
+
176
+ ```tsx
177
+ function LoginPage() {
178
+ const { login, isLoading, error } = useAuth();
179
+
180
+ const handleSubmit = async (e: React.FormEvent) => {
181
+ e.preventDefault();
182
+ try {
183
+ await login({
184
+ email: e.target.email.value,
185
+ password: e.target.password.value,
186
+ });
187
+ router.push('/dashboard');
188
+ } catch (err) {
189
+ console.error('Login failed:', err);
190
+ }
191
+ };
192
+
193
+ return (
194
+ <form onSubmit={handleSubmit}>
195
+ <input name="email" type="email" required />
196
+ <input name="password" type="password" required />
197
+ <button type="submit" disabled={isLoading}>
198
+ {isLoading ? 'Logging in...' : 'Login'}
199
+ </button>
200
+ {error && <div>{error.message}</div>}
201
+ </form>
202
+ );
203
+ }
204
+ ```
205
+
206
+ ## TypeScript
207
+
208
+ Full TypeScript support with exported types:
209
+
210
+ ```tsx
211
+ import type { Service, Lead, EndUser } from '@foxpixel/react';
212
+ ```
213
+
214
+ ## License
215
+
216
+ MIT
@@ -0,0 +1,408 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode, ComponentType } from 'react';
3
+ import { AxiosInstance, AxiosRequestConfig } from 'axios';
4
+
5
+ /**
6
+ * Core types for FoxPixel React SDK
7
+ */
8
+ interface FoxPixelConfig {
9
+ /**
10
+ * Base URL of the FoxPixel API
11
+ * Default: process.env.NEXT_PUBLIC_FOXPIXEL_API_URL
12
+ */
13
+ apiUrl?: string;
14
+ /**
15
+ * API Key for authentication (server-side only)
16
+ * NEVER expose this in client-side code
17
+ * Default: process.env.FOXPIXEL_API_KEY
18
+ */
19
+ apiKey?: string;
20
+ /**
21
+ * Tenant ID (optional, can be set per request)
22
+ */
23
+ tenantId?: string;
24
+ }
25
+ interface Service {
26
+ id: string;
27
+ name: string;
28
+ description?: string;
29
+ category?: string;
30
+ price?: number;
31
+ active: boolean;
32
+ createdAt: string;
33
+ updatedAt: string;
34
+ }
35
+ interface ServiceCatalogResponse {
36
+ id: string;
37
+ tenantId: string;
38
+ name: string;
39
+ description?: string;
40
+ unitPrice: number;
41
+ unitType: string;
42
+ currency: string;
43
+ category?: string;
44
+ isActive: boolean;
45
+ version: number;
46
+ createdAt: string;
47
+ updatedAt: string;
48
+ }
49
+ interface Lead {
50
+ id: string;
51
+ fullName: string;
52
+ email: string;
53
+ phone?: string;
54
+ source?: string;
55
+ status: string;
56
+ createdAt: string;
57
+ }
58
+ interface CreateLeadRequest {
59
+ fullName: string;
60
+ email?: string;
61
+ phone?: string;
62
+ source?: string;
63
+ notes?: string;
64
+ }
65
+ interface EndUser {
66
+ id: string;
67
+ email: string;
68
+ fullName?: string;
69
+ phone?: string;
70
+ tenantId: string;
71
+ subscriptionStatus: 'trial' | 'active' | 'inactive' | 'cancelled';
72
+ trialEndsAt?: string;
73
+ hasAccess: boolean;
74
+ [key: string]: any;
75
+ }
76
+ interface EndUserLoginRequest {
77
+ email: string;
78
+ password: string;
79
+ }
80
+ interface ApiError$1 {
81
+ message: string;
82
+ code?: string;
83
+ status?: number;
84
+ expected?: boolean;
85
+ }
86
+
87
+ /**
88
+ * HTTP client for FoxPixel API
89
+ * Handles authentication and request/response transformation
90
+ */
91
+
92
+ declare class FoxPixelHttpClient {
93
+ private client;
94
+ private apiKey?;
95
+ private tenantId?;
96
+ private endUserToken?;
97
+ constructor(config: FoxPixelConfig);
98
+ /**
99
+ * Set API Key (server-side only)
100
+ */
101
+ setApiKey(apiKey: string): void;
102
+ /**
103
+ * Set tenant ID
104
+ */
105
+ setTenantId(tenantId: string): void;
106
+ /**
107
+ * Set end user token (for client-side auth)
108
+ * Note: In practice, end user auth uses httpOnly cookies
109
+ */
110
+ setEndUserToken(token: string): void;
111
+ /**
112
+ * Clear end user token (logout)
113
+ */
114
+ clearEndUserToken(): void;
115
+ /**
116
+ * Get underlying axios instance
117
+ */
118
+ getInstance(): AxiosInstance;
119
+ /**
120
+ * Make GET request
121
+ */
122
+ get<T>(url: string, config?: AxiosRequestConfig): Promise<T>;
123
+ /**
124
+ * Make POST request
125
+ */
126
+ post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
127
+ /**
128
+ * Make PUT request
129
+ */
130
+ put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
131
+ /**
132
+ * Make DELETE request
133
+ */
134
+ delete<T>(url: string, config?: AxiosRequestConfig): Promise<T>;
135
+ }
136
+
137
+ interface FoxPixelContextValue {
138
+ client: FoxPixelHttpClient;
139
+ config: FoxPixelConfig;
140
+ }
141
+ interface FoxPixelProviderProps {
142
+ children: ReactNode;
143
+ config?: FoxPixelConfig;
144
+ }
145
+ /**
146
+ * FoxPixelProvider - Wraps your app and provides SDK functionality
147
+ *
148
+ * IMPORTANT: API Key should NEVER be passed from client-side.
149
+ * Use server-side proxy (Next.js API routes) for API Key authentication.
150
+ *
151
+ * @example
152
+ * ```tsx
153
+ * // Client-side (no API Key)
154
+ * <FoxPixelProvider>
155
+ * <App />
156
+ * </FoxPixelProvider>
157
+ *
158
+ * // Server-side proxy (Next.js API route)
159
+ * // pages/api/foxpixel/[...path].ts
160
+ * export default async function handler(req, res) {
161
+ * const apiKey = process.env.FOXPIXEL_API_KEY;
162
+ * // Proxy request with API Key
163
+ * }
164
+ * ```
165
+ */
166
+ declare function FoxPixelProvider({ children, config }: FoxPixelProviderProps): react_jsx_runtime.JSX.Element;
167
+ /**
168
+ * Hook to access FoxPixel context
169
+ * @throws Error if used outside FoxPixelProvider
170
+ */
171
+ declare function useFoxPixelContext(): FoxPixelContextValue;
172
+
173
+ interface AuthContextValue {
174
+ user: EndUser | null;
175
+ isLoading: boolean;
176
+ isAuthenticated: boolean;
177
+ error: ApiError$1 | null;
178
+ login: (credentials: EndUserLoginRequest) => Promise<void>;
179
+ logout: () => Promise<void>;
180
+ register: (data: {
181
+ email: string;
182
+ password: string;
183
+ fullName: string;
184
+ phone?: string;
185
+ }) => Promise<void>;
186
+ updateProfile: (data: {
187
+ fullName?: string;
188
+ phone?: string;
189
+ }) => Promise<void>;
190
+ refetch: () => Promise<void>;
191
+ }
192
+ interface AuthProviderProps {
193
+ children: ReactNode;
194
+ /**
195
+ * Redirect path when user is not authenticated
196
+ * Default: '/login'
197
+ */
198
+ loginPath?: string;
199
+ /**
200
+ * Redirect path after successful login
201
+ * Default: '/account'
202
+ */
203
+ accountPath?: string;
204
+ }
205
+ /**
206
+ * AuthProvider - Manages authentication state globally
207
+ *
208
+ * Automatically:
209
+ * - Restores session on mount (checks httpOnly cookie)
210
+ * - Manages user state across the app
211
+ * - Handles login/logout transparently
212
+ *
213
+ * @example
214
+ * ```tsx
215
+ * // In _app.tsx
216
+ * <FoxPixelProvider>
217
+ * <AuthProvider>
218
+ * <Component {...pageProps} />
219
+ * </AuthProvider>
220
+ * </FoxPixelProvider>
221
+ * ```
222
+ */
223
+ declare function AuthProvider({ children, loginPath, accountPath }: AuthProviderProps): react_jsx_runtime.JSX.Element;
224
+ /**
225
+ * Hook to access authentication context
226
+ * @throws Error if used outside AuthProvider
227
+ */
228
+ declare function useAuth(): AuthContextValue;
229
+
230
+ interface ProtectedRouteProps {
231
+ children: ReactNode;
232
+ /**
233
+ * Redirect path when not authenticated
234
+ * Default: '/login'
235
+ */
236
+ loginPath?: string;
237
+ /**
238
+ * Show loading spinner while checking auth
239
+ */
240
+ loadingComponent?: ReactNode;
241
+ }
242
+ declare function ProtectedRoute({ children, loginPath, loadingComponent, }: ProtectedRouteProps): string | number | true | Iterable<ReactNode> | react_jsx_runtime.JSX.Element | null;
243
+
244
+ interface GuestOnlyRouteProps {
245
+ children: ReactNode;
246
+ /**
247
+ * Where to redirect when already authenticated.
248
+ * Default: '/account'
249
+ */
250
+ redirectTo?: string;
251
+ /**
252
+ * Show this while checking auth or redirecting.
253
+ */
254
+ loadingComponent?: ReactNode;
255
+ }
256
+ declare function GuestOnlyRoute({ children, redirectTo, loadingComponent, }: GuestOnlyRouteProps): string | number | true | Iterable<ReactNode> | react_jsx_runtime.JSX.Element;
257
+
258
+ interface WithAuthOptions {
259
+ loginPath?: string;
260
+ loadingComponent?: ReactNode;
261
+ }
262
+ declare function withAuth<P extends object>(Component: ComponentType<P>, options?: WithAuthOptions): (props: P) => string | number | true | Iterable<ReactNode> | react_jsx_runtime.JSX.Element | null;
263
+
264
+ /**
265
+ * Hook to fetch and manage services (Projects module)
266
+ */
267
+
268
+ interface UseServicesOptions {
269
+ category?: string;
270
+ active?: boolean;
271
+ }
272
+ interface UseServicesReturn {
273
+ services: ServiceCatalogResponse[] | null;
274
+ isLoading: boolean;
275
+ error: ApiError$1 | null;
276
+ refetch: () => Promise<void>;
277
+ }
278
+ /**
279
+ * Fetch services from Projects module
280
+ *
281
+ * @example
282
+ * ```tsx
283
+ * function ServicesPage() {
284
+ * const { services, isLoading, error } = useServices({ active: true });
285
+ *
286
+ * if (isLoading) return <div>Loading...</div>;
287
+ * if (error) return <div>Error: {error.message}</div>;
288
+ *
289
+ * return (
290
+ * <div>
291
+ * {services?.map(service => (
292
+ * <ServiceCard key={service.id} service={service} />
293
+ * ))}
294
+ * </div>
295
+ * );
296
+ * }
297
+ * ```
298
+ */
299
+ declare function useServices(options?: UseServicesOptions): UseServicesReturn;
300
+
301
+ /**
302
+ * Hook to capture leads (CRM module)
303
+ */
304
+
305
+ interface UseLeadCaptureReturn {
306
+ captureLead: (data: CreateLeadRequest) => Promise<Lead>;
307
+ isLoading: boolean;
308
+ error: ApiError$1 | null;
309
+ }
310
+ /**
311
+ * Capture a lead (create lead in CRM)
312
+ *
313
+ * @example
314
+ * ```tsx
315
+ * function ContactForm() {
316
+ * const { captureLead, isLoading, error } = useLeadCapture();
317
+ *
318
+ * const handleSubmit = async (e) => {
319
+ * e.preventDefault();
320
+ * try {
321
+ * const lead = await captureLead({
322
+ * fullName: 'John Doe',
323
+ * email: 'john@example.com',
324
+ * phone: '+1234567890',
325
+ * source: 'website',
326
+ * });
327
+ * alert('Lead created!');
328
+ * } catch (err) {
329
+ * console.error('Error:', err);
330
+ * }
331
+ * };
332
+ *
333
+ * return <form onSubmit={handleSubmit}>...</form>;
334
+ * }
335
+ * ```
336
+ */
337
+ declare function useLeadCapture(): UseLeadCaptureReturn;
338
+
339
+ /**
340
+ * Hook to capture contacts (CRM module)
341
+ */
342
+ interface Contact {
343
+ id: string;
344
+ fullName: string;
345
+ email?: string;
346
+ phone?: string;
347
+ company?: string;
348
+ notes?: string;
349
+ postalCode?: string;
350
+ address?: string;
351
+ city?: string;
352
+ createdAt: string;
353
+ }
354
+ interface CreateContactRequest {
355
+ fullName: string;
356
+ email?: string;
357
+ phone?: string;
358
+ company?: string;
359
+ notes?: string;
360
+ nif?: string;
361
+ address?: string;
362
+ postalCode?: string;
363
+ city?: string;
364
+ }
365
+ interface ApiError {
366
+ message: string;
367
+ status?: number;
368
+ code?: string;
369
+ }
370
+ interface UseContactCaptureReturn {
371
+ captureContact: (data: CreateContactRequest) => Promise<Contact>;
372
+ isLoading: boolean;
373
+ error: ApiError | null;
374
+ }
375
+ /**
376
+ * Capture a contact (create contact in CRM)
377
+ *
378
+ * The frontend/SDK decides which fields to fill.
379
+ * This hook accepts any fields from CreateContactRequest.
380
+ *
381
+ * @example
382
+ * ```tsx
383
+ * function ContactForm() {
384
+ * const { captureContact, isLoading, error } = useContactCapture();
385
+ *
386
+ * const handleSubmit = async (e) => {
387
+ * e.preventDefault();
388
+ * try {
389
+ * const contact = await captureContact({
390
+ * fullName: 'John Doe',
391
+ * email: 'john@example.com',
392
+ * phone: '+1234567890',
393
+ * postalCode: '75001',
394
+ * notes: 'Interested in garden maintenance',
395
+ * });
396
+ * alert('Contact created!');
397
+ * } catch (err) {
398
+ * console.error('Error:', err);
399
+ * }
400
+ * };
401
+ *
402
+ * return <form onSubmit={handleSubmit}>...</form>;
403
+ * }
404
+ * ```
405
+ */
406
+ declare function useContactCapture(): UseContactCaptureReturn;
407
+
408
+ export { type ApiError$1 as ApiError, AuthProvider, type CreateLeadRequest, type EndUser, type EndUserLoginRequest, type FoxPixelConfig, FoxPixelHttpClient, FoxPixelProvider, GuestOnlyRoute, type Lead, ProtectedRoute, type Service, type ServiceCatalogResponse, useAuth, useContactCapture, useFoxPixelContext, useLeadCapture, useServices, withAuth };