@stevederico/skateboard-ui 0.9.8 → 1.0.2
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/App.jsx +75 -0
- package/CHANGELOG.md +18 -0
- package/Context.jsx +57 -0
- package/LandingView.jsx +4 -4
- package/Utilities.js +442 -15
- package/package.json +13 -1
- package/styles.css +174 -0
package/App.jsx
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createRoot } from 'react-dom/client';
|
|
2
|
+
import {
|
|
3
|
+
BrowserRouter as Router,
|
|
4
|
+
Routes,
|
|
5
|
+
Route,
|
|
6
|
+
Navigate,
|
|
7
|
+
useNavigate,
|
|
8
|
+
useLocation,
|
|
9
|
+
} from 'react-router-dom';
|
|
10
|
+
import { useEffect } from 'react';
|
|
11
|
+
import Layout from './Layout.jsx';
|
|
12
|
+
import LandingView from './LandingView.jsx';
|
|
13
|
+
import TextView from './TextView.jsx';
|
|
14
|
+
import SignUpView from './SignUpView.jsx';
|
|
15
|
+
import SignInView from './SignInView.jsx';
|
|
16
|
+
import SignOutView from './SignOutView.jsx';
|
|
17
|
+
import PaymentView from './PaymentView.jsx';
|
|
18
|
+
import SettingsView from './SettingsView.jsx';
|
|
19
|
+
import NotFound from './NotFound.jsx';
|
|
20
|
+
import ProtectedRoute from './ProtectedRoute.jsx';
|
|
21
|
+
import { useAppSetup, initializeUtilities } from './Utilities.js';
|
|
22
|
+
import { ContextProvider, getState } from './Context.jsx';
|
|
23
|
+
|
|
24
|
+
function App({ constants, appRoutes, defaultRoute }) {
|
|
25
|
+
const location = useLocation();
|
|
26
|
+
const navigate = useNavigate();
|
|
27
|
+
const { dispatch } = getState();
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
document.title = constants.appName;
|
|
31
|
+
}, [constants.appName]);
|
|
32
|
+
|
|
33
|
+
useAppSetup(location, navigate, dispatch);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Routes>
|
|
37
|
+
<Route element={<Layout />}>
|
|
38
|
+
<Route path="/console" element={<Navigate to="/app" replace />} />
|
|
39
|
+
<Route path="/app" element={<ProtectedRoute />}>
|
|
40
|
+
<Route index element={<Navigate to={defaultRoute} replace />} />
|
|
41
|
+
{appRoutes.map(({ path, element }) => (
|
|
42
|
+
<Route key={path} path={path} element={element} />
|
|
43
|
+
))}
|
|
44
|
+
<Route path="settings" element={<SettingsView />} />
|
|
45
|
+
<Route path="payment" element={<PaymentView />} />
|
|
46
|
+
</Route>
|
|
47
|
+
</Route>
|
|
48
|
+
<Route path="/" element={<LandingView />} />
|
|
49
|
+
<Route path="/signin" element={<SignInView />} />
|
|
50
|
+
<Route path="/signup" element={<SignUpView />} />
|
|
51
|
+
<Route path="/signout" element={<SignOutView />} />
|
|
52
|
+
<Route path="/terms" element={<TextView details={constants.termsOfService} />} />
|
|
53
|
+
<Route path="/privacy" element={<TextView details={constants.privacyPolicy} />} />
|
|
54
|
+
<Route path="/eula" element={<TextView details={constants.EULA} />} />
|
|
55
|
+
<Route path="/subs" element={<TextView details={constants.subscriptionDetails} />} />
|
|
56
|
+
<Route path="*" element={<NotFound />} />
|
|
57
|
+
</Routes>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function createSkateboardApp({ constants, appRoutes, defaultRoute = appRoutes[0]?.path || 'home' }) {
|
|
62
|
+
// Initialize utilities with constants
|
|
63
|
+
initializeUtilities(constants);
|
|
64
|
+
|
|
65
|
+
const container = document.getElementById('root');
|
|
66
|
+
const root = createRoot(container);
|
|
67
|
+
|
|
68
|
+
root.render(
|
|
69
|
+
<ContextProvider constants={constants}>
|
|
70
|
+
<Router>
|
|
71
|
+
<App constants={constants} appRoutes={appRoutes} defaultRoute={defaultRoute} />
|
|
72
|
+
</Router>
|
|
73
|
+
</ContextProvider>
|
|
74
|
+
);
|
|
75
|
+
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
|
+
1.0.1
|
|
3
|
+
|
|
4
|
+
Remove Vite build plugins
|
|
5
|
+
|
|
6
|
+
1.0.0
|
|
7
|
+
|
|
8
|
+
Add API request utilities
|
|
9
|
+
Add constants validation
|
|
10
|
+
Add React hooks
|
|
11
|
+
Export App component
|
|
12
|
+
Export Context component
|
|
13
|
+
Export styles.css
|
|
14
|
+
|
|
15
|
+
0.9.9
|
|
16
|
+
|
|
17
|
+
Add optional chaining safeguards
|
|
18
|
+
Fix features rendering logic
|
|
19
|
+
|
|
2
20
|
0.9.8
|
|
3
21
|
|
|
4
22
|
Add ProtectedRoute component
|
package/Context.jsx
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React, { createContext, useContext, useReducer } from 'react';
|
|
2
|
+
|
|
3
|
+
const context = createContext();
|
|
4
|
+
|
|
5
|
+
export function ContextProvider({ children, constants }) {
|
|
6
|
+
const getStorageKey = () => {
|
|
7
|
+
const appName = constants.appName || 'skateboard';
|
|
8
|
+
return `${appName.toLowerCase().replace(/\s+/g, '-')}_user`;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const getInitialUser = () => {
|
|
12
|
+
try {
|
|
13
|
+
const storageKey = getStorageKey();
|
|
14
|
+
const storedUser = localStorage.getItem(storageKey);
|
|
15
|
+
if (!storedUser || storedUser === "undefined") return null;
|
|
16
|
+
return JSON.parse(storedUser);
|
|
17
|
+
} catch (e) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const initialState = { user: getInitialUser() };
|
|
23
|
+
|
|
24
|
+
function reducer(state, action) {
|
|
25
|
+
try {
|
|
26
|
+
const storageKey = getStorageKey();
|
|
27
|
+
const appName = constants.appName || 'skateboard';
|
|
28
|
+
const csrfKey = `${appName.toLowerCase().replace(/\s+/g, '-')}_csrf`;
|
|
29
|
+
|
|
30
|
+
switch (action.type) {
|
|
31
|
+
case 'SET_USER':
|
|
32
|
+
localStorage.setItem(storageKey, JSON.stringify(action.payload));
|
|
33
|
+
return { ...state, user: action.payload };
|
|
34
|
+
case 'CLEAR_USER':
|
|
35
|
+
localStorage.removeItem(storageKey);
|
|
36
|
+
localStorage.removeItem(csrfKey);
|
|
37
|
+
return { ...state, user: null };
|
|
38
|
+
default:
|
|
39
|
+
return state;
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return state;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const [state, dispatch] = useReducer(reducer, initialState);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<context.Provider value={{ state, dispatch }}>
|
|
50
|
+
{children}
|
|
51
|
+
</context.Provider>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getState() {
|
|
56
|
+
return useContext(context);
|
|
57
|
+
}
|
package/LandingView.jsx
CHANGED
|
@@ -252,11 +252,11 @@ export default function LandingView() {
|
|
|
252
252
|
{/* Features Section */}
|
|
253
253
|
<section id="features" className="bg-slate-100 dark:bg-gray-800 py-12 md:py-20 transition-colors duration-300">
|
|
254
254
|
<div className="max-w-7xl mx-auto px-6">
|
|
255
|
-
<h2 className="text-center text-4xl md:text-5xl font-bold mb-16 text-gray-900 dark:text-white">{constants.features
|
|
256
|
-
|
|
255
|
+
<h2 className="text-center text-4xl md:text-5xl font-bold mb-16 text-gray-900 dark:text-white">{constants.features?.title || 'Features'}</h2>
|
|
256
|
+
|
|
257
257
|
{/* Feature Grid */}
|
|
258
258
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-5xl mx-auto">
|
|
259
|
-
{constants.features
|
|
259
|
+
{(constants.features?.items || []).map((feature, index) => (
|
|
260
260
|
<div key={index} className="bg-white dark:bg-gray-700 rounded-2xl p-8 shadow-lg text-center transition-colors duration-300">
|
|
261
261
|
<div className="text-4xl mb-6">{feature.icon}</div>
|
|
262
262
|
<h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">{feature.title}</h3>
|
|
@@ -296,7 +296,7 @@ export default function LandingView() {
|
|
|
296
296
|
>{constants.stripeProducts[0]?.price || '$5.00'}</div>
|
|
297
297
|
<p className="text-gray-600 dark:text-gray-300 mb-8">per month</p>
|
|
298
298
|
<ul className="text-left space-y-4 mb-8">
|
|
299
|
-
{constants.features
|
|
299
|
+
{(constants.features?.items || []).map((feature, index) => (
|
|
300
300
|
<li key={index} className="flex items-center">
|
|
301
301
|
✅ {feature.title}
|
|
302
302
|
</li>
|
package/Utilities.js
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
|
-
import { useEffect } from 'react';
|
|
2
|
-
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
// Constants will be initialized by the app shell
|
|
4
|
+
let _constants = null;
|
|
5
|
+
|
|
6
|
+
export function initializeUtilities(constants) {
|
|
7
|
+
_constants = constants;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function getConstants() {
|
|
11
|
+
if (!_constants) {
|
|
12
|
+
throw new Error('Utilities not initialized. Call initializeUtilities(constants) first.');
|
|
13
|
+
}
|
|
14
|
+
return _constants;
|
|
15
|
+
}
|
|
3
16
|
|
|
4
17
|
export function getCookie(name) {
|
|
5
18
|
// For token cookies, use app-specific name
|
|
6
19
|
let cookieName = name;
|
|
7
20
|
if (name === 'token') {
|
|
8
|
-
const appName =
|
|
21
|
+
const appName = getConstants().appName || 'skateboard';
|
|
9
22
|
cookieName = `${appName.toLowerCase().replace(/\s+/g, '-')}_token`;
|
|
10
23
|
}
|
|
11
24
|
|
|
@@ -16,18 +29,18 @@ export function getCookie(name) {
|
|
|
16
29
|
}
|
|
17
30
|
|
|
18
31
|
export function getCSRFToken() {
|
|
19
|
-
const appName =
|
|
32
|
+
const appName = getConstants().appName || 'skateboard';
|
|
20
33
|
const csrfKey = `${appName.toLowerCase().replace(/\s+/g, '-')}_csrf`;
|
|
21
34
|
return localStorage.getItem(csrfKey);
|
|
22
35
|
}
|
|
23
36
|
|
|
24
37
|
export function getAppKey(suffix) {
|
|
25
|
-
const appName =
|
|
38
|
+
const appName = getConstants().appName || 'skateboard';
|
|
26
39
|
return `${appName.toLowerCase().replace(/\s+/g, '-')}_${suffix}`;
|
|
27
40
|
}
|
|
28
41
|
|
|
29
42
|
export function isAuthenticated() {
|
|
30
|
-
if (
|
|
43
|
+
if (getConstants().noLogin === true) {
|
|
31
44
|
return true;
|
|
32
45
|
}
|
|
33
46
|
const csrfKey = getAppKey('csrf');
|
|
@@ -35,7 +48,7 @@ export function isAuthenticated() {
|
|
|
35
48
|
}
|
|
36
49
|
|
|
37
50
|
export function getBackendURL() {
|
|
38
|
-
let result = import.meta.env.DEV ?
|
|
51
|
+
let result = import.meta.env.DEV ? getConstants().devBackendURL : getConstants().backendURL;
|
|
39
52
|
return result
|
|
40
53
|
}
|
|
41
54
|
|
|
@@ -50,7 +63,7 @@ export function isAppMode() {
|
|
|
50
63
|
|
|
51
64
|
export async function getCurrentUser() {
|
|
52
65
|
|
|
53
|
-
if (
|
|
66
|
+
if (getConstants().noLogin == true) {
|
|
54
67
|
return {}
|
|
55
68
|
}
|
|
56
69
|
|
|
@@ -78,11 +91,11 @@ export async function getCurrentUser() {
|
|
|
78
91
|
}
|
|
79
92
|
|
|
80
93
|
export async function isSubscriber() {
|
|
81
|
-
if (
|
|
94
|
+
if (getConstants().noLogin == true) {
|
|
82
95
|
return false
|
|
83
96
|
}
|
|
84
97
|
|
|
85
|
-
try
|
|
98
|
+
try{
|
|
86
99
|
const csrfToken = getCSRFToken();
|
|
87
100
|
const response = await fetch(`${getBackendURL()}/isSubscriber`, {
|
|
88
101
|
method: 'GET',
|
|
@@ -150,7 +163,7 @@ export async function showCheckout(email, productIndex = 0) {
|
|
|
150
163
|
const csrfToken = getCSRFToken();
|
|
151
164
|
|
|
152
165
|
const params = {
|
|
153
|
-
lookup_key:
|
|
166
|
+
lookup_key: getConstants().stripeProducts[productIndex].lookup_key,
|
|
154
167
|
email: email
|
|
155
168
|
};
|
|
156
169
|
|
|
@@ -194,7 +207,7 @@ const FREE_LIMITS = {
|
|
|
194
207
|
};
|
|
195
208
|
|
|
196
209
|
export async function getRemainingUsage(action) {
|
|
197
|
-
if (
|
|
210
|
+
if (getConstants().noLogin === true) {
|
|
198
211
|
return { remaining: -1, total: -1, isSubscriber: true };
|
|
199
212
|
}
|
|
200
213
|
|
|
@@ -223,7 +236,7 @@ export async function getRemainingUsage(action) {
|
|
|
223
236
|
}
|
|
224
237
|
|
|
225
238
|
export async function trackUsage(action) {
|
|
226
|
-
if (
|
|
239
|
+
if (getConstants().noLogin === true) {
|
|
227
240
|
return { remaining: -1, total: -1, isSubscriber: true };
|
|
228
241
|
}
|
|
229
242
|
|
|
@@ -259,7 +272,7 @@ export async function trackUsage(action) {
|
|
|
259
272
|
|
|
260
273
|
export async function showUpgradeSheet(upgradeSheetRef) {
|
|
261
274
|
// Check subscription from user data in localStorage instead of API call
|
|
262
|
-
const appName =
|
|
275
|
+
const appName = getConstants().appName || 'skateboard';
|
|
263
276
|
const storageKey = `${appName.toLowerCase().replace(/\s+/g, '-')}_user`;
|
|
264
277
|
const storedUser = localStorage.getItem(storageKey);
|
|
265
278
|
|
|
@@ -384,7 +397,7 @@ export function timestampToString(input, format = "DOB") {
|
|
|
384
397
|
|
|
385
398
|
export function useAppSetup(location) {
|
|
386
399
|
useEffect(() => {
|
|
387
|
-
document.title =
|
|
400
|
+
document.title = getConstants().appName;
|
|
388
401
|
if (!location.pathname.toLowerCase().includes('app')) {
|
|
389
402
|
document.documentElement.classList.remove('dark');
|
|
390
403
|
document.body.classList.remove('dark');
|
|
@@ -392,3 +405,417 @@ export function useAppSetup(location) {
|
|
|
392
405
|
}, [location.pathname]);
|
|
393
406
|
}
|
|
394
407
|
|
|
408
|
+
// ===== API REQUEST UTILITIES =====
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Unified API request utility
|
|
412
|
+
* Handles credentials, CSRF tokens, 401 redirects, and error handling
|
|
413
|
+
*
|
|
414
|
+
* @param {string} endpoint - API endpoint (e.g., '/deals')
|
|
415
|
+
* @param {RequestInit} options - Fetch options (method, body, headers, etc.)
|
|
416
|
+
* @returns {Promise<any>} - Parsed JSON response
|
|
417
|
+
*/
|
|
418
|
+
export async function apiRequest(endpoint, options = {}) {
|
|
419
|
+
const csrfToken = getCSRFToken();
|
|
420
|
+
const needsCSRF = ['POST', 'PUT', 'DELETE', 'PATCH'].includes(
|
|
421
|
+
(options.method || 'GET').toUpperCase()
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
const response = await fetch(`${getBackendURL()}${endpoint}`, {
|
|
425
|
+
...options,
|
|
426
|
+
credentials: 'include',
|
|
427
|
+
headers: {
|
|
428
|
+
'Content-Type': 'application/json',
|
|
429
|
+
...(needsCSRF && csrfToken && { 'X-CSRF-Token': csrfToken }),
|
|
430
|
+
...options.headers
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// Handle 401 (redirect to signout)
|
|
435
|
+
if (response.status === 401) {
|
|
436
|
+
window.location.href = '/signout';
|
|
437
|
+
throw new Error('Unauthorized - Redirecting to Sign Out');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Handle other errors
|
|
441
|
+
if (!response.ok) {
|
|
442
|
+
throw new Error(`Request failed: ${response.status} ${response.statusText}`);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return response.json();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* API request with query parameters
|
|
450
|
+
*
|
|
451
|
+
* @param {string} endpoint - API endpoint
|
|
452
|
+
* @param {object} params - Query parameters
|
|
453
|
+
* @param {RequestInit} options - Fetch options
|
|
454
|
+
* @returns {Promise<any>}
|
|
455
|
+
*/
|
|
456
|
+
export async function apiRequestWithParams(endpoint, params = {}, options = {}) {
|
|
457
|
+
const queryString = new URLSearchParams(params).toString();
|
|
458
|
+
const url = queryString ? `${endpoint}?${queryString}` : endpoint;
|
|
459
|
+
return apiRequest(url, options);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ===== CONSTANTS VALIDATION =====
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Validate constants.json has all required fields
|
|
466
|
+
* @param {object} constants - Constants object to validate
|
|
467
|
+
* @returns {object} - Same constants if valid
|
|
468
|
+
* @throws {Error} - If required fields are missing
|
|
469
|
+
*/
|
|
470
|
+
export function validateConstants(constants) {
|
|
471
|
+
const required = [
|
|
472
|
+
'appName',
|
|
473
|
+
'appIcon',
|
|
474
|
+
'tagline',
|
|
475
|
+
'cta',
|
|
476
|
+
'backendURL',
|
|
477
|
+
'devBackendURL',
|
|
478
|
+
'features.title',
|
|
479
|
+
'features.items',
|
|
480
|
+
'companyName',
|
|
481
|
+
'companyWebsite',
|
|
482
|
+
'companyEmail',
|
|
483
|
+
];
|
|
484
|
+
|
|
485
|
+
const missing = required.filter(key => {
|
|
486
|
+
const value = key.split('.').reduce((obj, k) => obj?.[k], constants);
|
|
487
|
+
return !value;
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
if (missing.length > 0) {
|
|
491
|
+
throw new Error(`Missing required constants: ${missing.join(', ')}`);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return constants;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
// ===== REACT HOOKS =====
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Standard list data fetcher with optional sorting
|
|
502
|
+
* @param {string} endpoint - API endpoint to fetch from
|
|
503
|
+
* @param {function} sortFn - Optional sort function for results
|
|
504
|
+
* @returns {object} - { data, loading, error, refetch }
|
|
505
|
+
*/
|
|
506
|
+
export function useListData(endpoint, sortFn = null) {
|
|
507
|
+
const [data, setData] = useState([]);
|
|
508
|
+
const [loading, setLoading] = useState(true);
|
|
509
|
+
const [error, setError] = useState(null);
|
|
510
|
+
|
|
511
|
+
const fetchData = async () => {
|
|
512
|
+
setLoading(true);
|
|
513
|
+
try {
|
|
514
|
+
const result = await apiRequest(endpoint);
|
|
515
|
+
const sorted = sortFn ? result.sort(sortFn) : result;
|
|
516
|
+
setData(sorted);
|
|
517
|
+
setError(null);
|
|
518
|
+
} catch (err) {
|
|
519
|
+
setError(err.message);
|
|
520
|
+
} finally {
|
|
521
|
+
setLoading(false);
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
useEffect(() => {
|
|
526
|
+
fetchData();
|
|
527
|
+
}, [endpoint]);
|
|
528
|
+
|
|
529
|
+
return { data, loading, error, refetch: fetchData };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Standard form state management
|
|
534
|
+
* @param {object} initialValues - Initial form values
|
|
535
|
+
* @param {function} onSubmit - Submit handler function
|
|
536
|
+
* @returns {object} - { values, handleChange, handleSubmit, reset, submitting, error }
|
|
537
|
+
*/
|
|
538
|
+
export function useForm(initialValues, onSubmit) {
|
|
539
|
+
const [values, setValues] = useState(initialValues);
|
|
540
|
+
const [submitting, setSubmitting] = useState(false);
|
|
541
|
+
const [error, setError] = useState(null);
|
|
542
|
+
|
|
543
|
+
const handleChange = (field) => (e) => {
|
|
544
|
+
setValues(prev => ({ ...prev, [field]: e.target.value }));
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
const handleSubmit = async (e) => {
|
|
548
|
+
e?.preventDefault();
|
|
549
|
+
setSubmitting(true);
|
|
550
|
+
setError(null);
|
|
551
|
+
try {
|
|
552
|
+
await onSubmit(values);
|
|
553
|
+
setValues(initialValues);
|
|
554
|
+
} catch (err) {
|
|
555
|
+
setError(err.message);
|
|
556
|
+
} finally {
|
|
557
|
+
setSubmitting(false);
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
const reset = () => setValues(initialValues);
|
|
562
|
+
|
|
563
|
+
return { values, handleChange, handleSubmit, reset, submitting, error };
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// ===== VITE BUILD CONFIG UTILITIES =====
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Custom logger plugin for Vite
|
|
570
|
+
*/
|
|
571
|
+
export const customLoggerPlugin = () => {
|
|
572
|
+
return {
|
|
573
|
+
name: 'custom-logger',
|
|
574
|
+
configureServer(server) {
|
|
575
|
+
server.printUrls = () => {
|
|
576
|
+
console.log(`🖥️ React is running on http://localhost:${server.config.server.port || 5173}`);
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* HTML replacement plugin
|
|
584
|
+
* Replaces {{APP_NAME}}, {{TAGLINE}}, {{COMPANY_WEBSITE}} in index.html
|
|
585
|
+
*/
|
|
586
|
+
export const htmlReplacePlugin = () => {
|
|
587
|
+
return {
|
|
588
|
+
name: 'html-replace',
|
|
589
|
+
async transformIndexHtml(html) {
|
|
590
|
+
const { readFileSync } = await import('node:fs');
|
|
591
|
+
const constants = JSON.parse(readFileSync('src/constants.json', 'utf8'));
|
|
592
|
+
|
|
593
|
+
return html
|
|
594
|
+
.replace(/{{APP_NAME}}/g, constants.appName)
|
|
595
|
+
.replace(/{{TAGLINE}}/g, constants.tagline)
|
|
596
|
+
.replace(/{{COMPANY_WEBSITE}}/g, constants.companyWebsite);
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Dynamic robots.txt plugin
|
|
603
|
+
*/
|
|
604
|
+
export const dynamicRobotsPlugin = () => {
|
|
605
|
+
return {
|
|
606
|
+
name: 'dynamic-robots',
|
|
607
|
+
async generateBundle() {
|
|
608
|
+
const { readFileSync } = await import('node:fs');
|
|
609
|
+
const constants = JSON.parse(readFileSync('src/constants.json', 'utf8'));
|
|
610
|
+
const website = constants.companyWebsite.startsWith('http')
|
|
611
|
+
? constants.companyWebsite
|
|
612
|
+
: `https://${constants.companyWebsite}`;
|
|
613
|
+
|
|
614
|
+
const robotsContent = `User-agent: Googlebot
|
|
615
|
+
Disallow: /app/
|
|
616
|
+
Disallow: /console/
|
|
617
|
+
Disallow: /signin/
|
|
618
|
+
Disallow: /signup/
|
|
619
|
+
|
|
620
|
+
User-agent: Bingbot
|
|
621
|
+
Disallow: /app/
|
|
622
|
+
Disallow: /console/
|
|
623
|
+
Disallow: /signin/
|
|
624
|
+
Disallow: /signup/
|
|
625
|
+
|
|
626
|
+
User-agent: Applebot
|
|
627
|
+
Disallow: /app/
|
|
628
|
+
Disallow: /console/
|
|
629
|
+
Disallow: /signin/
|
|
630
|
+
Disallow: /signup/
|
|
631
|
+
|
|
632
|
+
User-agent: facebookexternalhit
|
|
633
|
+
Disallow: /app/
|
|
634
|
+
Disallow: /console/
|
|
635
|
+
Disallow: /signin/
|
|
636
|
+
Disallow: /signup/
|
|
637
|
+
|
|
638
|
+
User-agent: Facebot
|
|
639
|
+
Disallow: /app/
|
|
640
|
+
Disallow: /console/
|
|
641
|
+
Disallow: /signin/
|
|
642
|
+
Disallow: /signup/
|
|
643
|
+
|
|
644
|
+
User-agent: Twitterbot
|
|
645
|
+
Disallow: /app/
|
|
646
|
+
Disallow: /console/
|
|
647
|
+
Disallow: /signin/
|
|
648
|
+
Disallow: /signup/
|
|
649
|
+
|
|
650
|
+
User-agent: *
|
|
651
|
+
Disallow: /
|
|
652
|
+
|
|
653
|
+
Sitemap: ${website}/sitemap.xml
|
|
654
|
+
`;
|
|
655
|
+
|
|
656
|
+
this.emitFile({
|
|
657
|
+
type: 'asset',
|
|
658
|
+
fileName: 'robots.txt',
|
|
659
|
+
source: robotsContent
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Dynamic sitemap.xml plugin
|
|
667
|
+
*/
|
|
668
|
+
export const dynamicSitemapPlugin = () => {
|
|
669
|
+
return {
|
|
670
|
+
name: 'dynamic-sitemap',
|
|
671
|
+
async generateBundle() {
|
|
672
|
+
const { readFileSync } = await import('node:fs');
|
|
673
|
+
const constants = JSON.parse(readFileSync('src/constants.json', 'utf8'));
|
|
674
|
+
const website = constants.companyWebsite.startsWith('http')
|
|
675
|
+
? constants.companyWebsite
|
|
676
|
+
: `https://${constants.companyWebsite}`;
|
|
677
|
+
|
|
678
|
+
const currentDate = new Date().toISOString().split('T')[0];
|
|
679
|
+
|
|
680
|
+
const sitemapContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
681
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
682
|
+
<url>
|
|
683
|
+
<loc>${website}/</loc>
|
|
684
|
+
<lastmod>${currentDate}</lastmod>
|
|
685
|
+
<changefreq>weekly</changefreq>
|
|
686
|
+
<priority>1.0</priority>
|
|
687
|
+
</url>
|
|
688
|
+
<url>
|
|
689
|
+
<loc>${website}/terms</loc>
|
|
690
|
+
<lastmod>${currentDate}</lastmod>
|
|
691
|
+
<changefreq>monthly</changefreq>
|
|
692
|
+
<priority>0.8</priority>
|
|
693
|
+
</url>
|
|
694
|
+
<url>
|
|
695
|
+
<loc>${website}/privacy</loc>
|
|
696
|
+
<lastmod>${currentDate}</lastmod>
|
|
697
|
+
<changefreq>monthly</changefreq>
|
|
698
|
+
<priority>0.8</priority>
|
|
699
|
+
</url>
|
|
700
|
+
<url>
|
|
701
|
+
<loc>${website}/subs</loc>
|
|
702
|
+
<lastmod>${currentDate}</lastmod>
|
|
703
|
+
<changefreq>monthly</changefreq>
|
|
704
|
+
<priority>0.7</priority>
|
|
705
|
+
</url>
|
|
706
|
+
<url>
|
|
707
|
+
<loc>${website}/eula</loc>
|
|
708
|
+
<lastmod>${currentDate}</lastmod>
|
|
709
|
+
<changefreq>monthly</changefreq>
|
|
710
|
+
<priority>0.7</priority>
|
|
711
|
+
</url>
|
|
712
|
+
</urlset>`;
|
|
713
|
+
|
|
714
|
+
this.emitFile({
|
|
715
|
+
type: 'asset',
|
|
716
|
+
fileName: 'sitemap.xml',
|
|
717
|
+
source: sitemapContent
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Dynamic manifest.json plugin
|
|
725
|
+
*/
|
|
726
|
+
export const dynamicManifestPlugin = () => {
|
|
727
|
+
return {
|
|
728
|
+
name: 'dynamic-manifest',
|
|
729
|
+
async generateBundle() {
|
|
730
|
+
const { readFileSync } = await import('node:fs');
|
|
731
|
+
const constants = JSON.parse(readFileSync('src/constants.json', 'utf8'));
|
|
732
|
+
|
|
733
|
+
const manifestContent = {
|
|
734
|
+
short_name: constants.appName,
|
|
735
|
+
name: constants.appName,
|
|
736
|
+
description: constants.tagline,
|
|
737
|
+
icons: [
|
|
738
|
+
{
|
|
739
|
+
src: "/icons/icon.svg",
|
|
740
|
+
sizes: "192x192",
|
|
741
|
+
type: "image/svg+xml"
|
|
742
|
+
}
|
|
743
|
+
],
|
|
744
|
+
start_url: "./app",
|
|
745
|
+
display: "standalone",
|
|
746
|
+
theme_color: "#000000",
|
|
747
|
+
background_color: "#ffffff"
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
this.emitFile({
|
|
751
|
+
type: 'asset',
|
|
752
|
+
fileName: 'manifest.json',
|
|
753
|
+
source: JSON.stringify(manifestContent, null, 2)
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Complete Vite config generator
|
|
761
|
+
* Returns standard skateboard config with optional overrides
|
|
762
|
+
*/
|
|
763
|
+
export async function getSkateboardViteConfig(customConfig = {}) {
|
|
764
|
+
const [react, tailwindcss, { resolve }, path] = await Promise.all([
|
|
765
|
+
import('@vitejs/plugin-react-swc').then(m => m.default),
|
|
766
|
+
import('@tailwindcss/vite').then(m => m.default),
|
|
767
|
+
import('node:path'),
|
|
768
|
+
import('node:path')
|
|
769
|
+
]);
|
|
770
|
+
|
|
771
|
+
return {
|
|
772
|
+
plugins: [
|
|
773
|
+
react(),
|
|
774
|
+
tailwindcss(),
|
|
775
|
+
customLoggerPlugin(),
|
|
776
|
+
htmlReplacePlugin(),
|
|
777
|
+
dynamicRobotsPlugin(),
|
|
778
|
+
dynamicSitemapPlugin(),
|
|
779
|
+
dynamicManifestPlugin(),
|
|
780
|
+
...(customConfig.plugins || [])
|
|
781
|
+
],
|
|
782
|
+
esbuild: {
|
|
783
|
+
drop: []
|
|
784
|
+
},
|
|
785
|
+
resolve: {
|
|
786
|
+
alias: {
|
|
787
|
+
'@': resolve(process.cwd(), './src'),
|
|
788
|
+
'@package': path.resolve(process.cwd(), 'package.json'),
|
|
789
|
+
'@root': path.resolve(process.cwd()),
|
|
790
|
+
'react/jsx-runtime': path.resolve(process.cwd(), 'node_modules/react/jsx-runtime.js'),
|
|
791
|
+
...(customConfig.resolve?.alias || {})
|
|
792
|
+
}
|
|
793
|
+
},
|
|
794
|
+
optimizeDeps: {
|
|
795
|
+
include: ['react-dom', '@radix-ui/react-slot'],
|
|
796
|
+
esbuildOptions: {
|
|
797
|
+
define: {
|
|
798
|
+
global: 'globalThis',
|
|
799
|
+
},
|
|
800
|
+
},
|
|
801
|
+
},
|
|
802
|
+
server: {
|
|
803
|
+
host: 'localhost',
|
|
804
|
+
open: false,
|
|
805
|
+
port: 5173,
|
|
806
|
+
strictPort: false,
|
|
807
|
+
hmr: {
|
|
808
|
+
port: 5173,
|
|
809
|
+
overlay: false,
|
|
810
|
+
},
|
|
811
|
+
watch: {
|
|
812
|
+
usePolling: false,
|
|
813
|
+
ignored: ['**/node_modules/**', '**/.git/**']
|
|
814
|
+
},
|
|
815
|
+
...(customConfig.server || {})
|
|
816
|
+
},
|
|
817
|
+
logLevel: 'error',
|
|
818
|
+
...customConfig
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stevederico/skateboard-ui",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "1.0.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
"./AppSidebar": {
|
|
@@ -76,6 +76,18 @@
|
|
|
76
76
|
"import": "./ProtectedRoute.jsx",
|
|
77
77
|
"default": "./ProtectedRoute.jsx"
|
|
78
78
|
},
|
|
79
|
+
"./styles.css": {
|
|
80
|
+
"import": "./styles.css",
|
|
81
|
+
"default": "./styles.css"
|
|
82
|
+
},
|
|
83
|
+
"./Context": {
|
|
84
|
+
"import": "./Context.jsx",
|
|
85
|
+
"default": "./Context.jsx"
|
|
86
|
+
},
|
|
87
|
+
"./App": {
|
|
88
|
+
"import": "./App.jsx",
|
|
89
|
+
"default": "./App.jsx"
|
|
90
|
+
},
|
|
79
91
|
"./shadcn/ui/*": "./shadcn/ui/*.jsx",
|
|
80
92
|
"./shadcn/lib/utils": {
|
|
81
93
|
"import": "./shadcn/lib/utils.js",
|
package/styles.css
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
@source '../';
|
|
4
|
+
|
|
5
|
+
@plugin 'tailwindcss-animate';
|
|
6
|
+
|
|
7
|
+
@custom-variant dark (&:is(.dark *));
|
|
8
|
+
|
|
9
|
+
@theme {
|
|
10
|
+
--color-app: var(--color-blue-500);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
:root {
|
|
14
|
+
--background: oklch(0.985 0 0);
|
|
15
|
+
--foreground: oklch(0.145 0 0);
|
|
16
|
+
--card: oklch(0.985 0 0);
|
|
17
|
+
--card-foreground: oklch(0.145 0 0);
|
|
18
|
+
--popover: oklch(1 0 0);
|
|
19
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
20
|
+
--primary: oklch(0.205 0 0);
|
|
21
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
22
|
+
--secondary: oklch(0.97 0 0);
|
|
23
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
24
|
+
--muted: oklch(0.97 0 0);
|
|
25
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
26
|
+
--accent: oklch(0.96 0 0);
|
|
27
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
28
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
29
|
+
--destructive-foreground: oklch(0.577 0.245 27.325);
|
|
30
|
+
--border: oklch(0.922 0 0);
|
|
31
|
+
--input: oklch(0.922 0 0);
|
|
32
|
+
--ring: oklch(0.708 0 0);
|
|
33
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
34
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
35
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
36
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
37
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
38
|
+
--radius: 0.625rem;
|
|
39
|
+
--sidebar: oklch(0.985 0 0);
|
|
40
|
+
--sidebar-foreground: oklch(0.145 0 0);
|
|
41
|
+
--sidebar-primary: oklch(0.205 0 0);
|
|
42
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
43
|
+
--sidebar-accent: oklch(0.96 0 0);
|
|
44
|
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
45
|
+
--sidebar-border: oklch(0.922 0 0);
|
|
46
|
+
--sidebar-ring: oklch(0.708 0 0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.dark {
|
|
50
|
+
--background: oklch(0.205 0 0);
|
|
51
|
+
--foreground: oklch(0.985 0 0);
|
|
52
|
+
--card: oklch(0.205 0 0);
|
|
53
|
+
--card-foreground: oklch(0.985 0 0);
|
|
54
|
+
--popover: oklch(0.145 0 0);
|
|
55
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
56
|
+
--primary: oklch(0.985 0 0);
|
|
57
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
58
|
+
--secondary: oklch(0.269 0 0);
|
|
59
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
60
|
+
--muted: oklch(0.269 0 0);
|
|
61
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
62
|
+
--accent: oklch(0.28 0 0);
|
|
63
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
64
|
+
--destructive: oklch(0.396 0.141 25.723);
|
|
65
|
+
--destructive-foreground: oklch(0.637 0.237 25.331);
|
|
66
|
+
--border: oklch(0.269 0 0);
|
|
67
|
+
--input: oklch(0.269 0 0);
|
|
68
|
+
--ring: oklch(0.439 0 0);
|
|
69
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
70
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
71
|
+
--chart-3: oklch(0.769 0.188 70.08);
|
|
72
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
73
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
74
|
+
--sidebar: oklch(0.205 0 0);
|
|
75
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
76
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
77
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
78
|
+
--sidebar-accent: var(--accent);
|
|
79
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
80
|
+
--sidebar-border: oklch(0.269 0 0);
|
|
81
|
+
--sidebar-ring: oklch(0.439 0 0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@theme inline {
|
|
85
|
+
--color-background: var(--background);
|
|
86
|
+
--color-foreground: var(--foreground);
|
|
87
|
+
--color-card: var(--card);
|
|
88
|
+
--color-card-foreground: var(--card-foreground);
|
|
89
|
+
--color-popover: var(--popover);
|
|
90
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
91
|
+
--color-primary: var(--primary);
|
|
92
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
93
|
+
--color-secondary: var(--secondary);
|
|
94
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
95
|
+
--color-muted: var(--muted);
|
|
96
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
97
|
+
--color-accent: var(--accent);
|
|
98
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
99
|
+
--color-destructive: var(--destructive);
|
|
100
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
101
|
+
--color-border: var(--border);
|
|
102
|
+
--color-input: var(--input);
|
|
103
|
+
--color-ring: var(--ring);
|
|
104
|
+
--color-chart-1: var(--chart-1);
|
|
105
|
+
--color-chart-2: var(--chart-2);
|
|
106
|
+
--color-chart-3: var(--chart-3);
|
|
107
|
+
--color-chart-4: var(--chart-4);
|
|
108
|
+
--color-chart-5: var(--chart-5);
|
|
109
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
110
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
111
|
+
--radius-lg: var(--radius);
|
|
112
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
113
|
+
--color-sidebar: var(--sidebar);
|
|
114
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
115
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
116
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
117
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
118
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
119
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
120
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
121
|
+
--animate-accordion-down: accordion-down 0.2s ease-out;
|
|
122
|
+
--animate-accordion-up: accordion-up 0.2s ease-out;
|
|
123
|
+
|
|
124
|
+
@keyframes accordion-down {
|
|
125
|
+
from {
|
|
126
|
+
height: 0;
|
|
127
|
+
}
|
|
128
|
+
to {
|
|
129
|
+
height: var(--radix-accordion-content-height);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@keyframes accordion-up {
|
|
134
|
+
from {
|
|
135
|
+
height: var(--radix-accordion-content-height);
|
|
136
|
+
}
|
|
137
|
+
to {
|
|
138
|
+
height: 0;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@layer base {
|
|
144
|
+
* {
|
|
145
|
+
@apply border-border outline-ring/50;
|
|
146
|
+
}
|
|
147
|
+
body {
|
|
148
|
+
@apply bg-white dark:bg-black text-foreground;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@layer base {
|
|
153
|
+
:root {
|
|
154
|
+
--sidebar-background: 0 0% 98%;
|
|
155
|
+
--sidebar-foreground: 240 5.3% 26.1%;
|
|
156
|
+
--sidebar-primary: 240 5.9% 10%;
|
|
157
|
+
--sidebar-primary-foreground: 0 0% 98%;
|
|
158
|
+
--sidebar-accent: 240 4.8% 95.9%;
|
|
159
|
+
--sidebar-accent-foreground: 240 5.9% 10%;
|
|
160
|
+
--sidebar-border: 220 13% 91%;
|
|
161
|
+
--sidebar-ring: 217.2 91.2% 59.8%;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.dark {
|
|
165
|
+
--sidebar-background: 240 5.9% 10%;
|
|
166
|
+
--sidebar-foreground: 240 4.8% 95.9%;
|
|
167
|
+
--sidebar-primary: 224.3 76.3% 48%;
|
|
168
|
+
--sidebar-primary-foreground: 0 0% 100%;
|
|
169
|
+
--sidebar-accent: 240 3.7% 15.9%;
|
|
170
|
+
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
|
171
|
+
--sidebar-border: 240 3.7% 15.9%;
|
|
172
|
+
--sidebar-ring: 217.2 91.2% 59.8%;
|
|
173
|
+
}
|
|
174
|
+
}
|