astro-tractstack 2.2.10 → 2.3.1

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.
Files changed (85) hide show
  1. package/README.md +1 -1
  2. package/bin/create-tractstack.js +2 -2
  3. package/dist/index.js +177 -18
  4. package/package.json +4 -2
  5. package/templates/custom/minimal/CodeHook.astro +22 -5
  6. package/templates/custom/shopify/Cart.tsx +372 -0
  7. package/templates/custom/shopify/CartIcon.tsx +47 -0
  8. package/templates/custom/shopify/CartModal.tsx +63 -0
  9. package/templates/custom/shopify/CheckoutModal.tsx +576 -0
  10. package/templates/custom/shopify/NativeBookingCalendar.tsx +375 -0
  11. package/templates/custom/shopify/ShopifyCartManager.tsx +200 -0
  12. package/templates/custom/shopify/ShopifyCheckout.tsx +167 -0
  13. package/templates/custom/shopify/ShopifyProductGrid.tsx +247 -0
  14. package/templates/custom/shopify/ShopifyServiceList.tsx +135 -0
  15. package/templates/custom/shopify/cart.astro +23 -0
  16. package/templates/custom/with-examples/CodeHook.astro +17 -1
  17. package/templates/custom/with-examples/ProductGrid.astro +1 -1
  18. package/templates/src/client/app.js +4 -2
  19. package/templates/src/components/Footer.astro +4 -4
  20. package/templates/src/components/Header.astro +44 -12
  21. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +3 -3
  22. package/templates/src/components/edit/pane/AiRestylePaneModal.tsx +2 -2
  23. package/templates/src/components/edit/pane/steps/AiCreativeDesignStep.tsx +2 -2
  24. package/templates/src/components/edit/pane/steps/AiLibraryCopyStep.tsx +3 -3
  25. package/templates/src/components/edit/pane/steps/AiRefineDesignStep.tsx +2 -2
  26. package/templates/src/components/edit/pane/steps/AiStandardDesignStep.tsx +7 -7
  27. package/templates/src/components/form/advanced/APIConfigSection.tsx +407 -38
  28. package/templates/src/components/form/shopify/SchedulingSection.tsx +354 -0
  29. package/templates/src/components/storykeep/Dashboard.tsx +18 -4
  30. package/templates/src/components/storykeep/Dashboard_Advanced.tsx +1 -0
  31. package/templates/src/components/storykeep/Dashboard_Content.tsx +5 -96
  32. package/templates/src/components/storykeep/Dashboard_Shopify.tsx +668 -0
  33. package/templates/src/components/storykeep/StoryKeepBackdrop.astro +43 -23
  34. package/templates/src/components/storykeep/controls/content/BeliefTable.tsx +14 -5
  35. package/templates/src/components/storykeep/controls/content/ContentBrowser.tsx +0 -14
  36. package/templates/src/components/storykeep/controls/content/KnownResourceForm.tsx +36 -13
  37. package/templates/src/components/storykeep/controls/content/KnownResourceTable.tsx +5 -2
  38. package/templates/src/components/storykeep/controls/content/ManageContent.tsx +4 -11
  39. package/templates/src/components/storykeep/controls/content/MenuTable.tsx +14 -5
  40. package/templates/src/components/storykeep/controls/content/ProductTable.tsx +333 -0
  41. package/templates/src/components/storykeep/controls/content/ResourceBulkIngest.tsx +9 -5
  42. package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +108 -8
  43. package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +13 -4
  44. package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +14 -5
  45. package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +111 -0
  46. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +393 -0
  47. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Products.tsx +46 -0
  48. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Schedule.tsx +78 -0
  49. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx +55 -0
  50. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Services.tsx +47 -0
  51. package/templates/src/lib/resources.ts +11 -21
  52. package/templates/src/pages/api/auth/lookup-lead.ts +72 -0
  53. package/templates/src/pages/api/booking/availability.ts +72 -0
  54. package/templates/src/pages/api/booking/cancel.ts +73 -0
  55. package/templates/src/pages/api/booking/confirm.ts +82 -0
  56. package/templates/src/pages/api/booking/hold.ts +75 -0
  57. package/templates/src/pages/api/booking/list.ts +66 -0
  58. package/templates/src/pages/api/booking/metrics.ts +60 -0
  59. package/templates/src/pages/api/booking/release.ts +76 -0
  60. package/templates/src/pages/api/sandbox.ts +2 -2
  61. package/templates/src/pages/api/shopify/createCart.ts +69 -0
  62. package/templates/src/pages/api/shopify/getProducts.ts +64 -0
  63. package/templates/src/pages/storykeep/login.astro +26 -24
  64. package/templates/src/pages/storykeep/logout.astro +1 -10
  65. package/templates/src/pages/storykeep/manage.astro +69 -0
  66. package/templates/src/pages/storykeep/{content.astro → pages.astro} +4 -8
  67. package/templates/src/pages/storykeep/shopify.astro +101 -0
  68. package/templates/src/stores/navigation.ts +3 -42
  69. package/templates/src/stores/nodes.ts +3 -1
  70. package/templates/src/stores/resources.ts +7 -10
  71. package/templates/src/stores/shopify.ts +266 -0
  72. package/templates/src/types/tractstack.ts +75 -0
  73. package/templates/src/utils/api/advancedConfig.ts +7 -1
  74. package/templates/src/utils/api/advancedHelpers.ts +87 -7
  75. package/templates/src/utils/api/bookingHelpers.ts +125 -0
  76. package/templates/src/utils/api/brandHelpers.ts +14 -0
  77. package/templates/src/utils/api/resourceConfig.ts +13 -5
  78. package/templates/src/utils/auth.ts +29 -9
  79. package/templates/src/utils/compositor/aiGeneration.ts +3 -3
  80. package/templates/src/utils/compositor/aiPaneParser.ts +2 -2
  81. package/templates/src/utils/customHelpers.ts +49 -0
  82. package/templates/src/utils/helpers.ts +59 -0
  83. package/templates/src/utils/profileStorage.ts +5 -0
  84. package/templates/src/utils/tenantResolver.ts +2 -1
  85. package/utils/inject-files.ts +161 -2
@@ -0,0 +1,69 @@
1
+ import type { APIRoute } from '@/types/astro';
2
+ import { resolveTenantId } from '@/utils/tenantResolver';
3
+
4
+ export const prerender = false;
5
+
6
+ interface CreateCartPayload {
7
+ lines: Array<{
8
+ merchandiseId: string;
9
+ quantity: number;
10
+ attributes?: Array<{ key: string; value: string }>;
11
+ }>;
12
+ attributes?: Array<{
13
+ key: string;
14
+ value: string;
15
+ }>;
16
+ email?: string;
17
+ traceId?: string;
18
+ }
19
+
20
+ const getBackendUrl = () => {
21
+ return import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
22
+ };
23
+
24
+ export const POST: APIRoute = async ({ request }) => {
25
+ const resolution = await resolveTenantId(request);
26
+ const tenantId = resolution.id;
27
+
28
+ const backendEndpoint = `${getBackendUrl()}/api/v1/shopify/checkout`;
29
+ const cookieHeader = request.headers.get('cookie') || '';
30
+
31
+ try {
32
+ const body = (await request.json()) as CreateCartPayload;
33
+
34
+ const backendResponse = await fetch(backendEndpoint, {
35
+ method: 'POST',
36
+ headers: {
37
+ 'Content-Type': 'application/json',
38
+ 'X-Tenant-ID': tenantId,
39
+ Cookie: cookieHeader,
40
+ },
41
+ body: JSON.stringify(body),
42
+ });
43
+
44
+ if (!backendResponse.ok) {
45
+ const errorText = await backendResponse.text();
46
+ throw new Error(
47
+ `Backend Proxy Error: ${backendResponse.status} - ${errorText}`
48
+ );
49
+ }
50
+
51
+ const result = await backendResponse.json();
52
+
53
+ return new Response(JSON.stringify(result), {
54
+ status: 200,
55
+ headers: { 'Content-Type': 'application/json' },
56
+ });
57
+ } catch (error) {
58
+ console.error('Shopify proxy create cart failed:', error);
59
+ return new Response(
60
+ JSON.stringify({
61
+ error: error instanceof Error ? error.message : 'Failed to create cart',
62
+ }),
63
+ {
64
+ status: 500,
65
+ headers: { 'Content-Type': 'application/json' },
66
+ }
67
+ );
68
+ }
69
+ };
@@ -0,0 +1,64 @@
1
+ import type { APIRoute } from '@/types/astro';
2
+ import { resolveTenantId } from '@/utils/tenantResolver';
3
+
4
+ export const prerender = false;
5
+
6
+ const getBackendUrl = () => {
7
+ return import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
8
+ };
9
+
10
+ export const GET: APIRoute = async ({ request }) => {
11
+ const resolution = await resolveTenantId(request);
12
+ const tenantId = resolution.id;
13
+
14
+ const url = new URL(request.url);
15
+ const q = url.searchParams.get('q') || '';
16
+ const cursor = url.searchParams.get('cursor') || '';
17
+
18
+ const backendUrl = new URL(`${getBackendUrl()}/api/v1/shopify/products`);
19
+ if (q) {
20
+ backendUrl.searchParams.set('q', q);
21
+ }
22
+ if (cursor) {
23
+ backendUrl.searchParams.set('cursor', cursor);
24
+ }
25
+
26
+ const cookieHeader = request.headers.get('cookie') || '';
27
+
28
+ try {
29
+ const backendResponse = await fetch(backendUrl.toString(), {
30
+ method: 'GET',
31
+ headers: {
32
+ 'Content-Type': 'application/json',
33
+ 'X-Tenant-ID': tenantId,
34
+ Cookie: cookieHeader,
35
+ },
36
+ });
37
+
38
+ if (!backendResponse.ok) {
39
+ const errorText = await backendResponse.text();
40
+ throw new Error(
41
+ `Backend Proxy Error: ${backendResponse.status} - ${errorText}`
42
+ );
43
+ }
44
+
45
+ const result = await backendResponse.json();
46
+
47
+ return new Response(JSON.stringify(result), {
48
+ status: 200,
49
+ headers: { 'Content-Type': 'application/json' },
50
+ });
51
+ } catch (error) {
52
+ console.error('Shopify proxy fetch failed:', error);
53
+ return new Response(
54
+ JSON.stringify({
55
+ error:
56
+ error instanceof Error ? error.message : 'Failed to fetch products',
57
+ }),
58
+ {
59
+ status: 500,
60
+ headers: { 'Content-Type': 'application/json' },
61
+ }
62
+ );
63
+ }
64
+ };
@@ -31,8 +31,11 @@ const getAssetPath = (configPath: string, fallback: string) => {
31
31
  }
32
32
  return fallback;
33
33
  };
34
- const logo = getAssetPath(brandConfig?.LOGO, '/brand/logo.svg');
35
- const wordmark = getAssetPath(brandConfig?.WORDMARK, '/brand/wordmark.svg');
34
+ const logo = getAssetPath(brandConfig?.LOGO || ``, '/brand/logo.svg');
35
+ const wordmark = getAssetPath(
36
+ brandConfig?.WORDMARK || ``,
37
+ '/brand/wordmark.svg'
38
+ );
36
39
  const mainStylesUrl = isDev
37
40
  ? `${cssBasePath}/storykeep.css`
38
41
  : `${cssBasePath}/frontend.css`;
@@ -52,20 +55,27 @@ const mainStylesUrl = isDev
52
55
  <div class="mx-auto pb-6">
53
56
  <!-- Logo and Wordmark -->
54
57
  <div class="flex flex-col items-center justify-center gap-4">
55
- <div class="h-16 w-auto">
56
- <img
57
- src={logo}
58
- class="pointer-events-none h-full w-auto"
59
- alt="Logo"
60
- />
61
- </div>
62
- <div class="h-16 w-auto">
63
- <img
64
- src={wordmark}
65
- class="pointer-events-none h-full w-auto max-w-48 md:max-w-72"
66
- alt="Wordmark"
67
- />
68
- </div>
58
+ {
59
+ brandConfig?.WORDMARK_MODE !== 'wordmark' && (
60
+ <img
61
+ id="t8k-logo"
62
+ src={logo}
63
+ class="pointer-events-none h-16 w-auto"
64
+ alt="Logo"
65
+ />
66
+ )
67
+ }
68
+
69
+ {
70
+ brandConfig?.WORDMARK_MODE !== 'logo' && (
71
+ <img
72
+ id="t8k-wordmark"
73
+ src={wordmark}
74
+ class="pointer-events-none h-16 w-auto max-w-48 md:max-w-72"
75
+ alt="Wordmark"
76
+ />
77
+ )
78
+ }
69
79
  </div>
70
80
 
71
81
  <h2
@@ -193,10 +203,8 @@ const mainStylesUrl = isDev
193
203
  const buttonText = submitButton.textContent.trim();
194
204
 
195
205
  submitButton.addEventListener('click', async function () {
196
- // Hide error message
197
206
  if (loginErrorDiv) loginErrorDiv.style.display = 'none';
198
207
 
199
- // Show loading state
200
208
  submitButton.disabled = true;
201
209
  submitButton.textContent = 'Signing in...';
202
210
 
@@ -226,17 +234,11 @@ const mainStylesUrl = isDev
226
234
  const result = await response.json();
227
235
 
228
236
  if (result.success) {
229
- // Show success message with role
230
237
  if (loginFormDiv) loginFormDiv.style.display = 'none';
231
238
  if (loginSuccessDiv) loginSuccessDiv.style.display = 'block';
232
239
  if (userRoleSpan)
233
240
  userRoleSpan.textContent = result.role || 'authenticated';
234
241
 
235
- // Console log for debugging
236
- console.log('Login successful:', result);
237
- console.log('User role:', result.role);
238
-
239
- // Redirect after short delay
240
242
  setTimeout(() => {
241
243
  window.location.href = result.redirect || redirectPath;
242
244
  }, 1500);
@@ -35,10 +35,8 @@
35
35
  </div>
36
36
 
37
37
  <script>
38
- // Clear authentication data
39
38
  async function logout() {
40
39
  try {
41
- // Call server-side logout API to clear HTTP-only cookies
42
40
  const response = await fetch('/api/auth/logout', {
43
41
  method: 'POST',
44
42
  headers: {
@@ -48,13 +46,10 @@
48
46
 
49
47
  const result = await response.json();
50
48
 
51
- if (result.success) {
52
- console.log('StoryKeep: Admin logout successful');
53
- } else {
49
+ if (!result.success) {
54
50
  console.error('StoryKeep: Admin logout failed:', result.error);
55
51
  }
56
52
 
57
- // Also clear any remaining TractStack user profile data
58
53
  const tractStackKeys = [];
59
54
  for (let i = 0; i < localStorage.length; i++) {
60
55
  const key = localStorage.key(i);
@@ -64,15 +59,11 @@
64
59
  }
65
60
  tractStackKeys.forEach((key) => localStorage.removeItem(key));
66
61
 
67
- console.log('TractStack: Complete logout finished');
68
-
69
- // Redirect to home page
70
62
  setTimeout(() => {
71
63
  window.location.href = '/';
72
64
  }, 1000);
73
65
  } catch (error) {
74
66
  console.error('Logout error:', error);
75
- // Still redirect even if logout API fails
76
67
  window.location.href = '/';
77
68
  }
78
69
  }
@@ -0,0 +1,69 @@
1
+ ---
2
+ import Layout from '@/layouts/Layout.astro';
3
+ import StoryKeepBackdrop from '@/components/storykeep/StoryKeepBackdrop.astro';
4
+ import StoryKeepDashboard from '@/components/storykeep/Dashboard';
5
+ import ManageContent from '@/components/storykeep/controls/content/ManageContent';
6
+ import { requireAdminOrEditor, isAuthenticated, isAdmin } from '@/utils/auth';
7
+ import { getFullContentMap } from '@/stores/analytics';
8
+ import { getBrandConfig } from '@/utils/api/brandConfig';
9
+ import { preHealthCheck } from '@/utils/backend';
10
+
11
+ const tenantId =
12
+ Astro.locals.tenant?.id || import.meta.env.PUBLIC_TENANTID || 'default';
13
+
14
+ const healthCheckRedirect = await preHealthCheck(tenantId);
15
+ if (healthCheckRedirect !== undefined) {
16
+ return healthCheckRedirect;
17
+ }
18
+
19
+ const authCheck = requireAdminOrEditor(Astro);
20
+ if (authCheck) {
21
+ return authCheck;
22
+ }
23
+
24
+ const userIsAuthenticated = isAuthenticated(Astro);
25
+ const userIsAdmin = isAdmin(Astro);
26
+ const role = userIsAdmin ? `admin` : userIsAuthenticated ? `editor` : null;
27
+ const brandConfig = await getBrandConfig(tenantId);
28
+ const initializing = !brandConfig.SITE_INIT;
29
+
30
+ if (initializing) {
31
+ return Astro.redirect('/storykeep/branding');
32
+ }
33
+
34
+ const title = 'Manage Content | StoryKeep';
35
+ const createMenu = Astro.url.searchParams.has('create-menu');
36
+ const homeSlug = brandConfig.HOME_SLUG || 'hello';
37
+ let fullContentMap;
38
+
39
+ try {
40
+ fullContentMap = await getFullContentMap(tenantId);
41
+ } catch (error) {
42
+ return Astro.redirect(
43
+ `/maint?from=${encodeURIComponent(Astro.url.pathname)}`
44
+ );
45
+ }
46
+ ---
47
+
48
+ <Layout title={title} slug="storykeep" isStoryKeep={true}>
49
+ <main id="main-content" class="relative min-h-screen w-full">
50
+ <StoryKeepBackdrop brandConfig={brandConfig} />
51
+ <div class="max-w-5xl p-3.5 md:p-8">
52
+ <StoryKeepDashboard
53
+ client:only="react"
54
+ fullContentMap={fullContentMap}
55
+ homeSlug={homeSlug}
56
+ activeTab="manage"
57
+ role={role}
58
+ initializing={initializing}
59
+ />
60
+
61
+ <ManageContent
62
+ client:only="react"
63
+ fullContentMap={fullContentMap}
64
+ homeSlug={homeSlug}
65
+ createMenu={createMenu}
66
+ />
67
+ </div>
68
+ </main>
69
+ </Layout>
@@ -7,6 +7,7 @@ import { requireAdminOrEditor, isAuthenticated, isAdmin } from '@/utils/auth';
7
7
  import { getFullContentMap } from '@/stores/analytics';
8
8
  import { getBrandConfig } from '@/utils/api/brandConfig';
9
9
  import { preHealthCheck } from '@/utils/backend';
10
+ import type { FullContentMapItem } from '@/types/compositorTypes';
10
11
 
11
12
  const tenantId =
12
13
  Astro.locals.tenant?.id || import.meta.env.PUBLIC_TENANTID || 'default';
@@ -24,20 +25,16 @@ if (authCheck) {
24
25
  const userIsAuthenticated = isAuthenticated(Astro);
25
26
  const userIsAdmin = isAdmin(Astro);
26
27
  const role = userIsAdmin ? `admin` : userIsAuthenticated ? `editor` : null;
27
-
28
28
  const brandConfig = await getBrandConfig(tenantId);
29
29
  const initializing = !brandConfig.SITE_INIT;
30
30
 
31
- // Redirect to branding page during initial setup
32
31
  if (initializing) {
33
32
  return Astro.redirect('/storykeep/branding');
34
33
  }
35
34
 
36
- const title = 'Content | StoryKeep';
37
- const createMenu = Astro.url.searchParams.has('create-menu');
38
-
39
- let fullContentMap;
35
+ const title = 'Web Pages | StoryKeep';
40
36
  const homeSlug = brandConfig.HOME_SLUG || 'hello';
37
+ let fullContentMap: FullContentMapItem[];
41
38
 
42
39
  try {
43
40
  fullContentMap = await getFullContentMap(tenantId);
@@ -56,7 +53,7 @@ try {
56
53
  client:only="react"
57
54
  fullContentMap={fullContentMap}
58
55
  homeSlug={homeSlug}
59
- activeTab="content"
56
+ activeTab="pages"
60
57
  role={role}
61
58
  initializing={initializing}
62
59
  />
@@ -65,7 +62,6 @@ try {
65
62
  client:only="react"
66
63
  fullContentMap={fullContentMap}
67
64
  homeSlug={homeSlug}
68
- createMenu={createMenu}
69
65
  />
70
66
  </div>
71
67
  </main>
@@ -0,0 +1,101 @@
1
+ ---
2
+ import Layout from '@/layouts/Layout.astro';
3
+ import StoryKeepBackdrop from '@/components/storykeep/StoryKeepBackdrop.astro';
4
+ import StoryKeepDashboard from '@/components/storykeep/Dashboard';
5
+ import StoryKeepDashboard_Shopify from '@/components/storykeep/Dashboard_Shopify';
6
+ import { requireAdminOrEditor, isAuthenticated, isAdmin } from '@/utils/auth';
7
+ import { getFullContentMap } from '@/stores/analytics';
8
+ import { getBrandConfig } from '@/utils/api/brandConfig';
9
+ import { getResourcesByIds } from '@/utils/api/resourceConfig';
10
+ import { preHealthCheck } from '@/utils/backend';
11
+ import type { ResourceNode } from '@/types/compositorTypes';
12
+
13
+ const tenantId =
14
+ Astro.locals.tenant?.id || import.meta.env.PUBLIC_TENANTID || 'default';
15
+
16
+ const healthCheckRedirect = await preHealthCheck(tenantId);
17
+ if (healthCheckRedirect !== undefined) {
18
+ return healthCheckRedirect;
19
+ }
20
+
21
+ const authCheck = requireAdminOrEditor(Astro);
22
+ if (authCheck) {
23
+ return authCheck;
24
+ }
25
+
26
+ // Fetch configuration early to check enablement
27
+ const brandConfig = await getBrandConfig(tenantId);
28
+
29
+ if (!brandConfig.HAS_SHOPIFY) {
30
+ return Astro.redirect('/storykeep');
31
+ }
32
+
33
+ const userIsAuthenticated = isAuthenticated(Astro);
34
+ const userIsAdmin = isAdmin(Astro);
35
+ const role = userIsAdmin ? 'admin' : userIsAuthenticated ? 'editor' : null;
36
+
37
+ const initializing = !brandConfig.SITE_INIT;
38
+
39
+ if (initializing) {
40
+ return Astro.redirect('/storykeep/branding');
41
+ }
42
+
43
+ const title = 'Shopify | StoryKeep';
44
+ let fullContentMap = [];
45
+ let relevantResources: ResourceNode[] = [];
46
+ const homeSlug = brandConfig.HOME_SLUG || 'hello';
47
+
48
+ try {
49
+ fullContentMap = await getFullContentMap(tenantId);
50
+ const knownResources = brandConfig.KNOWN_RESOURCES || {};
51
+ const activeCategories = Object.keys(knownResources);
52
+
53
+ if (Array.isArray(fullContentMap)) {
54
+ const slimResources = fullContentMap.filter(
55
+ (node: any) =>
56
+ node.type === 'Resource' &&
57
+ node.categorySlug &&
58
+ activeCategories.includes(node.categorySlug)
59
+ ) as ResourceNode[];
60
+
61
+ if (slimResources.length > 0) {
62
+ const ids = slimResources.map((r) => r.id);
63
+ const configs = await getResourcesByIds(tenantId, ids);
64
+ relevantResources = configs.map((config) => ({
65
+ ...config,
66
+ nodeType: 'Resource',
67
+ parentId: null,
68
+ })) as ResourceNode[];
69
+ } else {
70
+ relevantResources = [];
71
+ }
72
+ }
73
+ } catch (error) {
74
+ console.error('Failed to load shopify resources:', error);
75
+ return Astro.redirect(
76
+ `/maint?from=${encodeURIComponent(Astro.url.pathname)}`
77
+ );
78
+ }
79
+ ---
80
+
81
+ <Layout title={title} slug="storykeep" isStoryKeep={true}>
82
+ <main id="main-content" class="relative min-h-screen w-full">
83
+ <StoryKeepBackdrop brandConfig={brandConfig} />
84
+ <div class="max-w-5xl p-3.5 md:p-8">
85
+ <StoryKeepDashboard
86
+ client:only="react"
87
+ fullContentMap={fullContentMap}
88
+ homeSlug={homeSlug}
89
+ activeTab="shopify"
90
+ role={role}
91
+ initializing={initializing}
92
+ brandConfig={brandConfig}
93
+ />
94
+ <StoryKeepDashboard_Shopify
95
+ client:only="react"
96
+ brandConfig={brandConfig}
97
+ existingResources={relevantResources}
98
+ />
99
+ </div>
100
+ </main>
101
+ </Layout>
@@ -1,11 +1,8 @@
1
1
  import { atom } from 'nanostores';
2
2
 
3
- // Used by StoryKeepWizard
4
3
  export const skipWizard = atom<boolean>(false);
5
4
 
6
- // Navigation state structure for Content sub-navigation only
7
5
  export interface ContentNavigationState {
8
- subtab: 'webpages' | 'manage';
9
6
  manageSubtab:
10
7
  | 'summary'
11
8
  | 'storyfragments'
@@ -17,24 +14,22 @@ export interface ContentNavigationState {
17
14
  | 'files';
18
15
  }
19
16
 
20
- // Default content navigation state
21
17
  const defaultContentNavigationState: ContentNavigationState = {
22
- subtab: 'webpages',
23
18
  manageSubtab: 'summary',
24
19
  };
25
20
 
26
- // Storage key for localStorage persistence
27
21
  const CONTENT_NAVIGATION_STORAGE_KEY = 'tractstack_content_navigation_state';
28
22
 
29
- // Helper functions for localStorage
30
23
  function loadContentNavigationFromStorage(): ContentNavigationState {
31
24
  try {
32
25
  const stored = localStorage.getItem(CONTENT_NAVIGATION_STORAGE_KEY);
33
26
  if (stored) {
34
27
  const parsed = JSON.parse(stored);
28
+ // Ensure we only merge valid keys, ignoring old 'subtab' if present
35
29
  return {
36
30
  ...defaultContentNavigationState,
37
- ...parsed,
31
+ manageSubtab:
32
+ parsed.manageSubtab || defaultContentNavigationState.manageSubtab,
38
33
  };
39
34
  }
40
35
  } catch (error) {
@@ -57,29 +52,15 @@ function saveContentNavigationToStorage(state: ContentNavigationState): void {
57
52
  }
58
53
  }
59
54
 
60
- // Create the persistent nanostore for content navigation
61
55
  export const contentNavigationStore = atom<ContentNavigationState>(
62
56
  loadContentNavigationFromStorage()
63
57
  );
64
58
 
65
- // Subscribe to changes and persist to localStorage
66
59
  contentNavigationStore.subscribe((state) => {
67
60
  saveContentNavigationToStorage(state);
68
61
  });
69
62
 
70
- // Action creators for content navigation
71
63
  export const contentNavigationActions = {
72
- /**
73
- * Set content subtab (webpages vs manage)
74
- */
75
- setContentSubtab: (subtab: ContentNavigationState['subtab']) => {
76
- const currentState = contentNavigationStore.get();
77
- contentNavigationStore.set({
78
- ...currentState,
79
- subtab,
80
- });
81
- },
82
-
83
64
  /**
84
65
  * Set manage content sub-subtab (summary, storyfragments, etc.)
85
66
  */
@@ -106,22 +87,6 @@ export const contentNavigationActions = {
106
87
  },
107
88
  };
108
89
 
109
- // Navigation helper functions (moved from navigationHelpers.ts)
110
-
111
- /**
112
- * Handle content subtab change with navigation tracking
113
- */
114
- export function handleContentSubtabChange(
115
- newSubtab: ContentNavigationState['subtab'],
116
- setActiveContentTab: (tab: string) => void
117
- ) {
118
- // Update the active subtab in the component
119
- setActiveContentTab(newSubtab);
120
-
121
- // Update navigation store
122
- contentNavigationActions.setContentSubtab(newSubtab);
123
- }
124
-
125
90
  /**
126
91
  * Handle manage content sub-subtab change with navigation tracking
127
92
  */
@@ -129,10 +94,7 @@ export function handleManageSubtabChange(
129
94
  newSubtab: ContentNavigationState['manageSubtab'],
130
95
  setActiveTab: (tab: string) => void
131
96
  ) {
132
- // Update the active sub-subtab in the component
133
97
  setActiveTab(newSubtab);
134
-
135
- // Update navigation store
136
98
  contentNavigationActions.setManageSubtab(newSubtab);
137
99
  }
138
100
 
@@ -143,7 +105,6 @@ export function handleManageSubtabChange(
143
105
  export function restoreTabNavigation() {
144
106
  const state = contentNavigationStore.get();
145
107
  return {
146
- subtab: state.subtab,
147
108
  manageSubtab: state.manageSubtab,
148
109
  };
149
110
  }
@@ -1893,6 +1893,7 @@ export class NodesContext {
1893
1893
  delete duplicatedPane.gridLayout;
1894
1894
  }
1895
1895
 
1896
+ duplicatedPane.isChanged = true;
1896
1897
  this.addNode(duplicatedPane as PaneNode);
1897
1898
  this.addNodes(allNodes);
1898
1899
  this.notifyNode(ownerId);
@@ -1953,7 +1954,6 @@ export class NodesContext {
1953
1954
  const storyFragmentNode = ownerNode as StoryFragmentNode;
1954
1955
  let specificIdx = -1;
1955
1956
  let elIdx = -1;
1956
- let storyFragmentWasChanged: boolean = false;
1957
1957
 
1958
1958
  if (
1959
1959
  insertPaneId &&
@@ -1976,8 +1976,10 @@ export class NodesContext {
1976
1976
  );
1977
1977
  }
1978
1978
  }
1979
+ storyFragmentNode.isChanged = true;
1979
1980
  }
1980
1981
 
1982
+ duplicatedPane.isChanged = true;
1981
1983
  this.addNode(duplicatedPane as PaneNode);
1982
1984
  this.linkChildToParent(
1983
1985
  duplicatedPane.id,
@@ -1,17 +1,14 @@
1
- import { atom } from 'nanostores';
1
+ import { map } from 'nanostores';
2
2
  import type { ResourceNode } from '@/types/compositorTypes';
3
3
 
4
- export interface ResourcesCache {
4
+ export const HEADER_RESOURCES_TTL = 5 * 60 * 1000;
5
+
6
+ export const headerResourcesStore = map<{
5
7
  data: ResourceNode[];
6
8
  lastFetched: number;
7
- }
8
-
9
- // Initialize with an empty state. This atom will live on the server and persist
10
- // across page requests for the lifetime of the server instance.
11
- export const headerResourcesStore = atom<ResourcesCache>({
9
+ key?: string;
10
+ }>({
12
11
  data: [],
13
12
  lastFetched: 0,
13
+ key: '',
14
14
  });
15
-
16
- // Default Time-To-Live for the cache: 5 minutes in milliseconds.
17
- export const HEADER_RESOURCES_TTL = 5 * 60 * 1000;