@stevederico/skateboard-ui 1.2.10 → 1.2.12

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/AppSidebar.jsx CHANGED
@@ -50,14 +50,21 @@ export default function AppSidebar() {
50
50
  </SidebarHeader>
51
51
  )}
52
52
  <SidebarContent>
53
- <ul className={`flex flex-col gap-1 p-2 ${open ? "" : "items-center"}`}>
53
+ <ul
54
+ className={`flex flex-col gap-1 p-2 ${open ? "" : "items-center"}`}
55
+ role="navigation"
56
+ aria-label="Main navigation"
57
+ >
54
58
  {constants.pages.map((item) => {
55
59
  const isActive = currentPage === item.url.toLowerCase();
56
60
  return (
57
61
  <li key={item.title}>
58
- <div
62
+ <button
63
+ type="button"
59
64
  className={`cursor-pointer items-center flex w-full px-4 py-3 rounded-lg ${open ? "h-14" : "h-12 w-12"} ${isActive ? "bg-accent/80 text-accent-foreground" : "hover:bg-accent/50 hover:text-accent-foreground"}`}
60
65
  onClick={() => handleNavigation(`/app/${item.url.toLowerCase()}`)}
66
+ aria-label={item.title}
67
+ aria-current={isActive ? 'page' : undefined}
61
68
  >
62
69
  <span className="flex w-full items-center">
63
70
  <DynamicIconComponent
@@ -67,7 +74,7 @@ export default function AppSidebar() {
67
74
  />
68
75
  {open && <span className="ml-3">{item.title}</span>}
69
76
  </span>
70
- </div>
77
+ </button>
71
78
  </li>
72
79
  );
73
80
  })}
@@ -76,10 +83,13 @@ export default function AppSidebar() {
76
83
  <SidebarFooter>
77
84
  <ul className={`flex flex-col gap-1 ${open ? "" : "items-center"}`}>
78
85
  <li>
79
- <div
86
+ <button
87
+ type="button"
80
88
  className={`cursor-pointer items-center rounded-lg flex w-full px-4 py-3 ${open ? "h-14" : "h-12 w-12"}
81
89
  ${location.pathname.toLowerCase().includes("settings") ? "bg-accent/80 text-accent-foreground" : "hover:bg-accent/50 hover:text-accent-foreground"}`}
82
90
  onClick={() => handleNavigation("/app/settings")}
91
+ aria-label="Settings"
92
+ aria-current={location.pathname.toLowerCase().includes("settings") ? 'page' : undefined}
83
93
  >
84
94
  <span className="flex w-full items-center">
85
95
  <DynamicIconComponent
@@ -89,7 +99,7 @@ export default function AppSidebar() {
89
99
  />
90
100
  {open && <span className="ml-3">Settings</span>}
91
101
  </span>
92
- </div>
102
+ </button>
93
103
  </li>
94
104
  </ul>
95
105
  </SidebarFooter>
package/CHANGELOG.md CHANGED
@@ -1,4 +1,22 @@
1
1
  # CHANGELOG
2
+ 1.2.12
3
+
4
+ Remove unused embla-carousel-react
5
+ Remove unused sonner
6
+
7
+ 1.2.11
8
+
9
+ Add navigation aria labels
10
+ Add sidebar button semantics
11
+ Add TabBar accessibility
12
+ Add password validation
13
+ Add open redirect prevention
14
+ Add API request timeout
15
+ Fix useListData abort handling
16
+ Remove console.log statements
17
+ Optimize ThemeToggle storage
18
+ Remove ViteConfig export
19
+
2
20
  1.2.5
3
21
 
4
22
  Add sidebar visibility control
package/PaymentView.jsx CHANGED
@@ -1,13 +1,34 @@
1
- import React, { useEffect } from 'react';
1
+ import React, { useEffect, useCallback } from 'react';
2
2
  import { useNavigate, useSearchParams } from 'react-router-dom';
3
3
  import { getState } from './Context.jsx';
4
4
  import { getCurrentUser } from './Utilities.js'
5
5
  import constants from "@/constants.json";
6
6
 
7
+ // Whitelist of allowed redirect paths to prevent open redirect vulnerabilities
8
+ const ALLOWED_REDIRECT_PREFIXES = ['/app/', '/'];
9
+ const DEFAULT_REDIRECT = '/app/home';
10
+
11
+ function isAllowedRedirect(path) {
12
+ // Must start with allowed prefix and not contain protocol
13
+ if (path.includes('://') || path.includes('//')) return false;
14
+ return ALLOWED_REDIRECT_PREFIXES.some(prefix => path.startsWith(prefix));
15
+ }
16
+
7
17
  export default function PaymentView() {
8
18
  const navigate = useNavigate();
9
- const { state, dispatch } = getState();
10
- const [searchParams] = useSearchParams(); // Hook to access query params
19
+ const { dispatch } = getState();
20
+ const [searchParams] = useSearchParams();
21
+
22
+ const fetchUser = useCallback(async () => {
23
+ try {
24
+ const data = await getCurrentUser();
25
+ if (data) {
26
+ dispatch({ type: 'SET_USER', payload: data });
27
+ }
28
+ } catch (error) {
29
+ console.error('Failed to fetch user after payment:', error);
30
+ }
31
+ }, [dispatch]);
11
32
 
12
33
  useEffect(() => {
13
34
  // Get app-specific localStorage keys
@@ -20,30 +41,21 @@ export default function PaymentView() {
20
41
  const portal = searchParams.get('portal') === 'return';
21
42
 
22
43
  // Default redirect path
23
- let redirectPath = '/app/home'; // Fallback if no URL is stored
24
-
25
- async function getUser() {
26
- let data = await getCurrentUser();
27
- dispatch({ type: 'SET_USER', payload: data });
28
- }
44
+ let redirectPath = DEFAULT_REDIRECT;
29
45
 
30
46
  // Handle different cases
31
47
  switch (true) {
32
48
  case success:
33
- console.log("Checkout was successful!");
34
49
  redirectPath = localStorage.getItem(getAppKey('beforeCheckoutURL')) || redirectPath;
35
- getUser();
50
+ fetchUser();
36
51
  break;
37
52
  case canceled:
38
- console.log("Checkout was canceled!");
39
53
  redirectPath = localStorage.getItem(getAppKey('beforeCheckoutURL')) || redirectPath;
40
54
  break;
41
55
  case portal:
42
- console.log("Returned from billing portal!");
43
56
  redirectPath = localStorage.getItem(getAppKey('beforeManageURL')) || redirectPath;
44
57
  break;
45
58
  default:
46
- console.log("No specific query param detected, using default redirect.");
47
59
  redirectPath = localStorage.getItem(getAppKey('beforeCheckoutURL')) || redirectPath;
48
60
  break;
49
61
  }
@@ -52,10 +64,9 @@ export default function PaymentView() {
52
64
  if (redirectPath.startsWith('http://') || redirectPath.startsWith('https://')) {
53
65
  try {
54
66
  const url = new URL(redirectPath);
55
- redirectPath = url.pathname; // Extract just the path (e.g., '/app/settings')
67
+ redirectPath = url.pathname;
56
68
  } catch (e) {
57
- console.error('Invalid URL in redirectPath:', redirectPath);
58
- redirectPath = '/app/home'; // Fallback to default if parsing fails
69
+ redirectPath = DEFAULT_REDIRECT;
59
70
  }
60
71
  }
61
72
 
@@ -64,21 +75,23 @@ export default function PaymentView() {
64
75
  redirectPath = `/${redirectPath}`;
65
76
  }
66
77
 
67
- // Debug the redirectPath
68
- console.log('Redirecting to path:', redirectPath);
78
+ // Validate redirect path to prevent open redirect
79
+ if (!isAllowedRedirect(redirectPath)) {
80
+ redirectPath = DEFAULT_REDIRECT;
81
+ }
69
82
 
70
83
  // Clear the stored URLs
71
84
  localStorage.removeItem(getAppKey('beforeCheckoutURL'));
72
85
  localStorage.removeItem(getAppKey('beforeManageURL'));
73
-
86
+
74
87
  // Redirect after a delay
75
88
  const timeoutId = setTimeout(() => {
76
89
  navigate(redirectPath, { replace: true });
77
90
  }, 1500);
78
-
91
+
79
92
  // Cleanup timeout
80
93
  return () => clearTimeout(timeoutId);
81
- }, [navigate, searchParams]);
94
+ }, [navigate, searchParams, fetchUser]);
82
95
 
83
96
 
84
97
 
package/SignInView.jsx CHANGED
@@ -43,7 +43,6 @@ export default function LoginForm({
43
43
  setIsSubmitting(true);
44
44
  try {
45
45
  const uri = `${getBackendURL()}/signin`;
46
- console.log("URI ", uri)
47
46
  const response = await fetch(uri, {
48
47
  method: 'POST',
49
48
  credentials: 'include',
package/SignUpView.jsx CHANGED
@@ -35,9 +35,16 @@ export default function LoginForm({
35
35
  }, [])
36
36
 
37
37
  async function signUpClicked() {
38
+ // Client-side password validation (matches backend: 6-72 chars)
39
+ if (password.length < 6) {
40
+ setErrorMessage('Password must be at least 6 characters');
41
+ return;
42
+ }
43
+ if (password.length > 72) {
44
+ setErrorMessage('Password must be 72 characters or less');
45
+ return;
46
+ }
38
47
  try {
39
- console.log(`${getBackendURL()}/signup`);
40
- console.log(`name: ${name}`);
41
48
  const response = await fetch(`${getBackendURL()}/signup`, {
42
49
  method: 'POST',
43
50
  credentials: 'include',
@@ -52,7 +59,6 @@ export default function LoginForm({
52
59
  navigate('/app');
53
60
  } else {
54
61
  setErrorMessage('Invalid Credentials')
55
- console.log("error with /signup")
56
62
  }
57
63
  } catch (error) {
58
64
  console.error('Signup failed:', error);
@@ -105,16 +111,24 @@ export default function LoginForm({
105
111
  }}
106
112
  />
107
113
 
108
- <Input
109
- id="password"
110
- type="password"
111
- placeholder="Password"
112
- className="py-7 px-4 placeholder:text-gray-400 rounded-lg border-2 bg-secondary dark:bg-secondary dark:border-secondary"
113
- style={{ fontSize: '20px' }}
114
- required
115
- value={password}
116
- onChange={(e) => setPassword(e.target.value)}
117
- />
114
+ <div className="flex flex-col gap-1">
115
+ <Input
116
+ id="password"
117
+ type="password"
118
+ placeholder="Password"
119
+ className="py-7 px-4 placeholder:text-gray-400 rounded-lg border-2 bg-secondary dark:bg-secondary dark:border-secondary"
120
+ style={{ fontSize: '20px' }}
121
+ required
122
+ minLength={6}
123
+ maxLength={72}
124
+ value={password}
125
+ onChange={(e) => {
126
+ setPassword(e.target.value);
127
+ setErrorMessage('');
128
+ }}
129
+ />
130
+ <p className="text-xs text-gray-500 dark:text-gray-400 ml-1">Minimum 6 characters</p>
131
+ </div>
118
132
 
119
133
  <button
120
134
  onClick={(e) => { e.preventDefault(); signUpClicked() }}
package/TabBar.jsx CHANGED
@@ -7,43 +7,40 @@ export default function TabBar() {
7
7
  const location = useLocation();
8
8
 
9
9
  return (
10
- <div className="fixed flex md:hidden pt-2 pb-4 bottom-0 inset-x-0 justify-around text-center border-t shadow-lg bg-background">
11
- {constants?.pages?.map((item) => (
12
- <span className="px-3" key={item.title}>
13
- <Link to={`/app/${item.url.toLowerCase()}`} className="cursor-pointer">
14
- {
15
- location.pathname.includes(item.url.toLowerCase())
16
- ? (
17
- <span className="text-base">
18
- <DynamicIcon name={item.icon} size={32} strokeWidth={1.5} />
19
- </span>
20
- )
21
- : (
22
- <span className="text-gray-500">
23
- <DynamicIcon name={item.icon} size={32} strokeWidth={1.5} />
24
- </span>
25
- )
26
- }
27
- </Link>
28
- </span>
29
- ))}
10
+ <nav
11
+ className="fixed flex md:hidden pt-2 pb-4 bottom-0 inset-x-0 justify-around text-center border-t shadow-lg bg-background"
12
+ role="navigation"
13
+ aria-label="Main navigation"
14
+ >
15
+ {constants?.pages?.map((item) => {
16
+ const isActive = location.pathname.includes(item.url.toLowerCase());
17
+ return (
18
+ <span className="px-3" key={item.title}>
19
+ <Link
20
+ to={`/app/${item.url.toLowerCase()}`}
21
+ className="cursor-pointer"
22
+ aria-label={item.title}
23
+ aria-current={isActive ? 'page' : undefined}
24
+ >
25
+ <span className={isActive ? "text-base" : "text-gray-500"}>
26
+ <DynamicIcon name={item.icon} size={32} strokeWidth={1.5} />
27
+ </span>
28
+ </Link>
29
+ </span>
30
+ );
31
+ })}
30
32
  <span className="px-3">
31
- <Link to={`/app/settings`} className="cursor-pointer">
32
- {
33
- location.pathname.includes('Settings'.toLowerCase())
34
- ? (
35
-
36
- <span className="text-base">
37
- <DynamicIcon name={"settings"} size={32} strokeWidth={1.5} />
38
- </span>
39
- )
40
- : (
41
- <span className="text-gray-500">
42
- <DynamicIcon name={"settings"} size={32} strokeWidth={1.5} />
43
- </span>
44
- )
45
- }
46
- </Link></span>
47
- </div>
33
+ <Link
34
+ to={`/app/settings`}
35
+ className="cursor-pointer"
36
+ aria-label="Settings"
37
+ aria-current={location.pathname.includes('settings') ? 'page' : undefined}
38
+ >
39
+ <span className={location.pathname.includes('settings') ? "text-base" : "text-gray-500"}>
40
+ <DynamicIcon name={"settings"} size={32} strokeWidth={1.5} />
41
+ </span>
42
+ </Link>
43
+ </span>
44
+ </nav>
48
45
  );
49
46
  };
package/ThemeToggle.jsx CHANGED
@@ -24,12 +24,19 @@ export default function ThemeToggle({ className = "", iconSize = 24, variant = "
24
24
 
25
25
  useEffect(() => {
26
26
  const root = document.documentElement;
27
+ const newTheme = isDarkMode ? 'dark' : 'light';
28
+
27
29
  if (isDarkMode) {
28
30
  root.classList.add('dark');
29
31
  } else {
30
32
  root.classList.remove('dark');
31
33
  }
32
- localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
34
+
35
+ // Only write to localStorage if value changed
36
+ const currentTheme = localStorage.getItem('theme');
37
+ if (currentTheme !== newTheme) {
38
+ localStorage.setItem('theme', newTheme);
39
+ }
33
40
  }, [isDarkMode]);
34
41
 
35
42
  const toggleTheme = () => {
package/Utilities.js CHANGED
@@ -67,16 +67,13 @@ function safeRemoveItem(key) {
67
67
 
68
68
  export function initializeUtilities(constants) {
69
69
  if (!constants) {
70
- console.error('[Utilities] initializeUtilities called with null/undefined constants!');
71
70
  throw new Error('initializeUtilities called with null/undefined constants');
72
71
  }
73
- console.log('[Utilities] Initializing with app:', constants.appName);
74
72
  // Store in both module and window to handle module duplication
75
73
  _constants = constants;
76
74
  if (typeof window !== 'undefined') {
77
75
  window.__SKATEBOARD_CONSTANTS__ = constants;
78
76
  }
79
- console.log('[Utilities] Initialization complete. _constants set:', !!_constants);
80
77
  }
81
78
 
82
79
  function getConstants() {
@@ -86,8 +83,6 @@ function getConstants() {
86
83
  }
87
84
 
88
85
  if (!_constants) {
89
- console.error('[Utilities] getConstants called but _constants is null!');
90
- console.trace('[Utilities] Call stack:');
91
86
  throw new Error('Utilities not initialized. Call initializeUtilities(constants) first.');
92
87
  }
93
88
  return _constants;
@@ -220,7 +215,6 @@ export async function showManage(stripeID) {
220
215
 
221
216
  if (response.ok) {
222
217
  const data = await response.json();
223
- console.log("/portal response: ", data);
224
218
  if (data.url) {
225
219
  safeSetItem(getAppKey("beforeManageURL"), window.location.href);
226
220
  window.location.href = data.url; // Redirect to Stripe billing portal
@@ -497,11 +491,18 @@ export async function apiRequest(endpoint, options = {}) {
497
491
  (options.method || 'GET').toUpperCase()
498
492
  );
499
493
 
494
+ // Set up timeout (default 30 seconds)
495
+ const timeout = options.timeout || 30000;
496
+ const controller = options.signal ? null : new AbortController();
497
+ const signal = options.signal || controller?.signal;
498
+ const timeoutId = controller ? setTimeout(() => controller.abort(), timeout) : null;
499
+
500
500
  let response;
501
501
  try {
502
502
  response = await fetch(`${getBackendURL()}${endpoint}`, {
503
503
  ...options,
504
504
  credentials: 'include',
505
+ signal,
505
506
  headers: {
506
507
  'Content-Type': 'application/json',
507
508
  ...(needsCSRF && csrfToken && { 'X-CSRF-Token': csrfToken }),
@@ -509,7 +510,12 @@ export async function apiRequest(endpoint, options = {}) {
509
510
  }
510
511
  });
511
512
  } catch (error) {
513
+ if (error.name === 'AbortError') {
514
+ throw error; // Re-throw abort errors for useListData to handle
515
+ }
512
516
  throw new Error(`Network error: ${error.message}`);
517
+ } finally {
518
+ if (timeoutId) clearTimeout(timeoutId);
513
519
  }
514
520
 
515
521
  // Handle 401 (redirect to signout)
@@ -602,14 +608,16 @@ export function useListData(endpoint, sortFn = null) {
602
608
  const [loading, setLoading] = useState(true);
603
609
  const [error, setError] = useState(null);
604
610
 
605
- const fetchData = async () => {
611
+ const fetchData = async (signal) => {
606
612
  setLoading(true);
607
613
  try {
608
- const result = await apiRequest(endpoint);
614
+ const result = await apiRequest(endpoint, { signal });
609
615
  const sorted = sortFn ? result.sort(sortFn) : result;
610
616
  setData(sorted);
611
617
  setError(null);
612
618
  } catch (err) {
619
+ // Ignore abort errors
620
+ if (err.name === 'AbortError') return;
613
621
  setError(err.message);
614
622
  } finally {
615
623
  setLoading(false);
@@ -617,10 +625,12 @@ export function useListData(endpoint, sortFn = null) {
617
625
  };
618
626
 
619
627
  useEffect(() => {
620
- fetchData();
621
- }, [endpoint]);
628
+ const controller = new AbortController();
629
+ fetchData(controller.signal);
630
+ return () => controller.abort();
631
+ }, [endpoint, sortFn]);
622
632
 
623
- return { data, loading, error, refetch: fetchData };
633
+ return { data, loading, error, refetch: () => fetchData() };
624
634
  }
625
635
 
626
636
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stevederico/skateboard-ui",
3
3
  "private": false,
4
- "version": "1.2.10",
4
+ "version": "1.2.12",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  "./AppSidebar": {
@@ -80,10 +80,6 @@
80
80
  "import": "./Utilities.js",
81
81
  "default": "./Utilities.js"
82
82
  },
83
- "./ViteConfig": {
84
- "import": "./ViteConfig.js",
85
- "default": "./ViteConfig.js"
86
- },
87
83
  "./ProtectedRoute": {
88
84
  "import": "./ProtectedRoute.jsx",
89
85
  "default": "./ProtectedRoute.jsx"
@@ -152,12 +148,10 @@
152
148
  "clsx": "^2.1.1",
153
149
  "cmdk": "^1.1.1",
154
150
  "date-fns": "^4.1.0",
155
- "embla-carousel-react": "^8.6.0",
156
151
  "lucide-react": "^0.537.0",
157
152
  "next-themes": "^0.4.6",
158
153
  "react-day-picker": "^9.8.1",
159
154
  "react-resizable-panels": "^3.0.4",
160
- "sonner": "^2.0.7",
161
155
  "tailwind-merge": "^3.3.0",
162
156
  "tailwindcss-animate": "^1.0.7",
163
157
  "vaul": "^1.1.2"
package/ViteConfig.js DELETED
@@ -1,279 +0,0 @@
1
- /**
2
- * Vite configuration utilities for skateboard-ui
3
- * This file is for build-time use only (in vite.config.js)
4
- */
5
-
6
- /**
7
- * Custom logger plugin for Vite
8
- */
9
- export const customLoggerPlugin = () => {
10
- return {
11
- name: 'custom-logger',
12
- configureServer(server) {
13
- server.printUrls = () => {
14
- console.log(`🖥️ React is running on http://localhost:${server.config.server.port || 5173}`);
15
- };
16
- }
17
- };
18
- };
19
-
20
- /**
21
- * HTML replacement plugin
22
- * Replaces {{APP_NAME}}, {{TAGLINE}}, {{COMPANY_WEBSITE}} in index.html
23
- */
24
- export const htmlReplacePlugin = () => {
25
- return {
26
- name: 'html-replace',
27
- async transformIndexHtml(html) {
28
- const { readFileSync } = await import('node:fs');
29
- const constants = JSON.parse(readFileSync('src/constants.json', 'utf8'));
30
-
31
- return html
32
- .replace(/{{APP_NAME}}/g, constants.appName)
33
- .replace(/{{TAGLINE}}/g, constants.tagline)
34
- .replace(/{{COMPANY_WEBSITE}}/g, constants.companyWebsite);
35
- }
36
- };
37
- };
38
-
39
- /**
40
- * Dynamic robots.txt plugin
41
- */
42
- export const dynamicRobotsPlugin = () => {
43
- return {
44
- name: 'dynamic-robots',
45
- async generateBundle() {
46
- const { readFileSync } = await import('node:fs');
47
- const constants = JSON.parse(readFileSync('src/constants.json', 'utf8'));
48
- const website = constants.companyWebsite.startsWith('http')
49
- ? constants.companyWebsite
50
- : `https://${constants.companyWebsite}`;
51
-
52
- const robotsContent = `User-agent: Googlebot
53
- Disallow: /app/
54
- Disallow: /console/
55
- Disallow: /signin/
56
- Disallow: /signup/
57
-
58
- User-agent: Bingbot
59
- Disallow: /app/
60
- Disallow: /console/
61
- Disallow: /signin/
62
- Disallow: /signup/
63
-
64
- User-agent: Applebot
65
- Disallow: /app/
66
- Disallow: /console/
67
- Disallow: /signin/
68
- Disallow: /signup/
69
-
70
- User-agent: facebookexternalhit
71
- Disallow: /app/
72
- Disallow: /console/
73
- Disallow: /signin/
74
- Disallow: /signup/
75
-
76
- User-agent: Twitterbot
77
- Allow: /
78
-
79
- User-agent: LinkedInBot
80
- Allow: /
81
-
82
- User-agent: WhatsApp
83
- Allow: /
84
-
85
- User-agent: *
86
- Disallow: /app/
87
- Disallow: /console/
88
- Disallow: /signin/
89
- Disallow: /signup/
90
- Allow: /
91
-
92
- Sitemap: ${website}/sitemap.xml`;
93
-
94
- this.emitFile({
95
- type: 'asset',
96
- fileName: 'robots.txt',
97
- source: robotsContent
98
- });
99
- }
100
- };
101
- };
102
-
103
- /**
104
- * Dynamic sitemap.xml plugin
105
- */
106
- export const dynamicSitemapPlugin = () => {
107
- return {
108
- name: 'dynamic-sitemap',
109
- async generateBundle() {
110
- const { readFileSync } = await import('node:fs');
111
- const constants = JSON.parse(readFileSync('src/constants.json', 'utf8'));
112
- const website = constants.companyWebsite.startsWith('http')
113
- ? constants.companyWebsite
114
- : `https://${constants.companyWebsite}`;
115
-
116
- const currentDate = new Date().toISOString().split('T')[0];
117
-
118
- const sitemapContent = `<?xml version="1.0" encoding="UTF-8"?>
119
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
120
- <url>
121
- <loc>${website}/</loc>
122
- <lastmod>${currentDate}</lastmod>
123
- <changefreq>weekly</changefreq>
124
- <priority>1.0</priority>
125
- </url>
126
- <url>
127
- <loc>${website}/terms</loc>
128
- <lastmod>${currentDate}</lastmod>
129
- <changefreq>monthly</changefreq>
130
- <priority>0.8</priority>
131
- </url>
132
- <url>
133
- <loc>${website}/privacy</loc>
134
- <lastmod>${currentDate}</lastmod>
135
- <changefreq>monthly</changefreq>
136
- <priority>0.8</priority>
137
- </url>
138
- <url>
139
- <loc>${website}/signin</loc>
140
- <lastmod>${currentDate}</lastmod>
141
- <changefreq>weekly</changefreq>
142
- <priority>0.6</priority>
143
- </url>
144
- <url>
145
- <loc>${website}/signup</loc>
146
- <lastmod>${currentDate}</lastmod>
147
- <changefreq>weekly</changefreq>
148
- <priority>0.6</priority>
149
- </url>
150
- </urlset>`;
151
-
152
- this.emitFile({
153
- type: 'asset',
154
- fileName: 'sitemap.xml',
155
- source: sitemapContent
156
- });
157
- }
158
- };
159
- };
160
-
161
- /**
162
- * Dynamic manifest.json plugin
163
- */
164
- export const dynamicManifestPlugin = () => {
165
- return {
166
- name: 'dynamic-manifest',
167
- async generateBundle() {
168
- const { readFileSync } = await import('node:fs');
169
- const constants = JSON.parse(readFileSync('src/constants.json', 'utf8'));
170
-
171
- const manifestContent = {
172
- short_name: constants.appName,
173
- name: constants.appName,
174
- description: constants.tagline,
175
- icons: [
176
- {
177
- src: "/icons/icon.svg",
178
- sizes: "192x192",
179
- type: "image/svg+xml"
180
- }
181
- ],
182
- start_url: "./app",
183
- display: "standalone",
184
- theme_color: "#000000",
185
- background_color: "#ffffff"
186
- };
187
-
188
- this.emitFile({
189
- type: 'asset',
190
- fileName: 'manifest.json',
191
- source: JSON.stringify(manifestContent, null, 2)
192
- });
193
- }
194
- };
195
- };
196
-
197
- /**
198
- * Complete Vite config generator
199
- * Returns standard skateboard config with optional overrides
200
- */
201
- export async function getSkateboardViteConfig(customConfig = {}) {
202
- const [react, tailwindcss, path] = await Promise.all([
203
- import('@vitejs/plugin-react-swc').then(m => m.default),
204
- import('@tailwindcss/vite').then(m => m.default),
205
- import('node:path')
206
- ]);
207
- const { resolve } = path;
208
-
209
- return {
210
- plugins: [
211
- react(),
212
- tailwindcss(),
213
- customLoggerPlugin(),
214
- htmlReplacePlugin(),
215
- dynamicRobotsPlugin(),
216
- dynamicSitemapPlugin(),
217
- dynamicManifestPlugin(),
218
- ...(customConfig.plugins || [])
219
- ],
220
- esbuild: {
221
- drop: []
222
- },
223
- resolve: {
224
- alias: {
225
- '@': resolve(process.cwd(), './src'),
226
- '@package': path.resolve(process.cwd(), 'package.json'),
227
- '@root': path.resolve(process.cwd()),
228
- 'react/jsx-runtime': path.resolve(process.cwd(), 'node_modules/react/jsx-runtime.js'),
229
- ...(customConfig.resolve?.alias || {})
230
- }
231
- },
232
- optimizeDeps: {
233
- include: [
234
- 'react',
235
- 'react-dom',
236
- 'react-dom/client',
237
- '@radix-ui/react-slot',
238
- 'react-router-dom',
239
- 'react-router',
240
- 'cookie',
241
- 'set-cookie-parser',
242
- ...(customConfig.optimizeDeps?.include || [])
243
- ],
244
- force: true,
245
- exclude: [
246
- '@swc/core',
247
- '@swc/core-darwin-arm64',
248
- '@swc/wasm',
249
- 'lightningcss',
250
- 'fsevents',
251
- ...(customConfig.optimizeDeps?.exclude || [])
252
- ],
253
- esbuildOptions: {
254
- target: 'esnext',
255
- define: {
256
- global: 'globalThis'
257
- },
258
- ...(customConfig.optimizeDeps?.esbuildOptions || {})
259
- }
260
- },
261
- server: {
262
- host: 'localhost',
263
- open: false,
264
- port: 5173,
265
- strictPort: false,
266
- hmr: {
267
- port: 5173,
268
- overlay: false,
269
- },
270
- watch: {
271
- usePolling: false,
272
- ignored: ['**/node_modules/**', '**/.git/**']
273
- },
274
- ...(customConfig.server || {})
275
- },
276
- logLevel: 'error',
277
- ...customConfig
278
- };
279
- }