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.
- package/README.md +1 -1
- package/bin/create-tractstack.js +2 -2
- package/dist/index.js +177 -18
- package/package.json +4 -2
- package/templates/custom/minimal/CodeHook.astro +22 -5
- package/templates/custom/shopify/Cart.tsx +372 -0
- package/templates/custom/shopify/CartIcon.tsx +47 -0
- package/templates/custom/shopify/CartModal.tsx +63 -0
- package/templates/custom/shopify/CheckoutModal.tsx +576 -0
- package/templates/custom/shopify/NativeBookingCalendar.tsx +375 -0
- package/templates/custom/shopify/ShopifyCartManager.tsx +200 -0
- package/templates/custom/shopify/ShopifyCheckout.tsx +167 -0
- package/templates/custom/shopify/ShopifyProductGrid.tsx +247 -0
- package/templates/custom/shopify/ShopifyServiceList.tsx +135 -0
- package/templates/custom/shopify/cart.astro +23 -0
- package/templates/custom/with-examples/CodeHook.astro +17 -1
- package/templates/custom/with-examples/ProductGrid.astro +1 -1
- package/templates/src/client/app.js +4 -2
- package/templates/src/components/Footer.astro +4 -4
- package/templates/src/components/Header.astro +44 -12
- package/templates/src/components/edit/pane/AddPanePanel_new.tsx +3 -3
- package/templates/src/components/edit/pane/AiRestylePaneModal.tsx +2 -2
- package/templates/src/components/edit/pane/steps/AiCreativeDesignStep.tsx +2 -2
- package/templates/src/components/edit/pane/steps/AiLibraryCopyStep.tsx +3 -3
- package/templates/src/components/edit/pane/steps/AiRefineDesignStep.tsx +2 -2
- package/templates/src/components/edit/pane/steps/AiStandardDesignStep.tsx +7 -7
- package/templates/src/components/form/advanced/APIConfigSection.tsx +407 -38
- package/templates/src/components/form/shopify/SchedulingSection.tsx +354 -0
- package/templates/src/components/storykeep/Dashboard.tsx +18 -4
- package/templates/src/components/storykeep/Dashboard_Advanced.tsx +1 -0
- package/templates/src/components/storykeep/Dashboard_Content.tsx +5 -96
- package/templates/src/components/storykeep/Dashboard_Shopify.tsx +668 -0
- package/templates/src/components/storykeep/StoryKeepBackdrop.astro +43 -23
- package/templates/src/components/storykeep/controls/content/BeliefTable.tsx +14 -5
- package/templates/src/components/storykeep/controls/content/ContentBrowser.tsx +0 -14
- package/templates/src/components/storykeep/controls/content/KnownResourceForm.tsx +36 -13
- package/templates/src/components/storykeep/controls/content/KnownResourceTable.tsx +5 -2
- package/templates/src/components/storykeep/controls/content/ManageContent.tsx +4 -11
- package/templates/src/components/storykeep/controls/content/MenuTable.tsx +14 -5
- package/templates/src/components/storykeep/controls/content/ProductTable.tsx +333 -0
- package/templates/src/components/storykeep/controls/content/ResourceBulkIngest.tsx +9 -5
- package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +108 -8
- package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +13 -4
- package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +14 -5
- package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +111 -0
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +393 -0
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Products.tsx +46 -0
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Schedule.tsx +78 -0
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx +55 -0
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Services.tsx +47 -0
- package/templates/src/lib/resources.ts +11 -21
- package/templates/src/pages/api/auth/lookup-lead.ts +72 -0
- package/templates/src/pages/api/booking/availability.ts +72 -0
- package/templates/src/pages/api/booking/cancel.ts +73 -0
- package/templates/src/pages/api/booking/confirm.ts +82 -0
- package/templates/src/pages/api/booking/hold.ts +75 -0
- package/templates/src/pages/api/booking/list.ts +66 -0
- package/templates/src/pages/api/booking/metrics.ts +60 -0
- package/templates/src/pages/api/booking/release.ts +76 -0
- package/templates/src/pages/api/sandbox.ts +2 -2
- package/templates/src/pages/api/shopify/createCart.ts +69 -0
- package/templates/src/pages/api/shopify/getProducts.ts +64 -0
- package/templates/src/pages/storykeep/login.astro +26 -24
- package/templates/src/pages/storykeep/logout.astro +1 -10
- package/templates/src/pages/storykeep/manage.astro +69 -0
- package/templates/src/pages/storykeep/{content.astro → pages.astro} +4 -8
- package/templates/src/pages/storykeep/shopify.astro +101 -0
- package/templates/src/stores/navigation.ts +3 -42
- package/templates/src/stores/nodes.ts +3 -1
- package/templates/src/stores/resources.ts +7 -10
- package/templates/src/stores/shopify.ts +266 -0
- package/templates/src/types/tractstack.ts +75 -0
- package/templates/src/utils/api/advancedConfig.ts +7 -1
- package/templates/src/utils/api/advancedHelpers.ts +87 -7
- package/templates/src/utils/api/bookingHelpers.ts +125 -0
- package/templates/src/utils/api/brandHelpers.ts +14 -0
- package/templates/src/utils/api/resourceConfig.ts +13 -5
- package/templates/src/utils/auth.ts +29 -9
- package/templates/src/utils/compositor/aiGeneration.ts +3 -3
- package/templates/src/utils/compositor/aiPaneParser.ts +2 -2
- package/templates/src/utils/customHelpers.ts +49 -0
- package/templates/src/utils/helpers.ts +59 -0
- package/templates/src/utils/profileStorage.ts +5 -0
- package/templates/src/utils/tenantResolver.ts +2 -1
- 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
|
|
35
|
-
const wordmark = getAssetPath(
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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 = '
|
|
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="
|
|
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
|
-
|
|
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 {
|
|
1
|
+
import { map } from 'nanostores';
|
|
2
2
|
import type { ResourceNode } from '@/types/compositorTypes';
|
|
3
3
|
|
|
4
|
-
export
|
|
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;
|