@brightsideoy/kama 1.0.0-alpha.2 → 1.0.0-alpha.20
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/bin/cli.js +604 -364
- package/content/definitions/block-kama-forms.yaml +5 -0
- package/content/definitions/blocks/button.yaml +15 -5
- package/content/definitions/blocks/node-list.yaml +75 -0
- package/content/definitions/field-sets/channel-base.yaml +7 -0
- package/content/definitions/nodes/article.yaml +17 -1
- package/content/definitions/taxonomies/categories.yaml +5 -2
- package/content/definitions/taxonomies/tags.yaml +5 -2
- package/content/forms/contact.yaml +44 -0
- package/content/forms/subscription.yaml +32 -0
- package/dist/assets/AssetEditor-D5j7pwcK.css +1 -0
- package/dist/assets/AssetEditor-Dt1zCR7_.js +1 -0
- package/dist/assets/CmsBadge-BNsgxyyB.css +1 -0
- package/dist/assets/CmsBadge-D0Ob9qPU.js +1 -0
- package/dist/assets/CmsButton-BQ85-YXn.css +1 -0
- package/dist/assets/CmsButton-CbOfxvjr.js +1 -0
- package/dist/assets/CmsDataRow--H4iI_u_.js +1 -0
- package/dist/assets/CmsDataRow-CYNxfVaZ.css +1 -0
- package/dist/assets/CmsHelp-BO_vWABq.css +1 -0
- package/dist/assets/CmsHelp-DyPJLGGi.js +1 -0
- package/dist/assets/CmsImagePreview-CujwG8r_.css +1 -0
- package/dist/assets/CmsImagePreview-DuHPBzml.js +1 -0
- package/dist/assets/CmsPaneResizer-B62ZqsnE.js +1 -0
- package/dist/assets/CmsPaneResizer-Dkidd4pI.css +1 -0
- package/dist/assets/CmsPublisher-CugEqTD8.css +1 -0
- package/dist/assets/CmsPublisher-DNLwjirk.js +3 -0
- package/dist/assets/CmsUserCenter-Ac0GywlR.css +1 -0
- package/dist/assets/CmsUserCenter-D3XDzKDC.js +1 -0
- package/dist/assets/EditorSplitPreview-BzKh4wFB.css +1 -0
- package/dist/assets/EditorSplitPreview-Cp2x0O_P.js +1 -0
- package/dist/assets/NodeContextMenu-BYeWzmQ5.css +1 -0
- package/dist/assets/NodeContextMenu-C_HWBGeb.js +1 -0
- package/dist/assets/NodeEditor-CiXQKXK1.js +147 -0
- package/dist/assets/NodeEditor-pZI3gu4a.css +1 -0
- package/dist/assets/StatusPill-CbeW5SMT.js +1 -0
- package/dist/assets/StatusPill-Dlm6uvGe.css +1 -0
- package/dist/assets/TrashWorkspace-DgIg1v6t.js +1 -0
- package/dist/assets/TrashWorkspace-DtMgxg13.css +1 -0
- package/dist/assets/_plugin-vue_export-helper-CQxWhVtr.js +5 -0
- package/dist/assets/circle-check-A-QeFqt3.js +1 -0
- package/dist/assets/database-hg5SqKLG.js +1 -0
- package/dist/assets/folder-tree-BWYot72H.js +1 -0
- package/dist/assets/globe-BraoljaN.js +1 -0
- package/dist/assets/history-pqIbKK9u.js +1 -0
- package/dist/assets/index-DQ6_RmCI.js +14 -0
- package/dist/assets/index-DcKiaRjI.css +1 -0
- package/dist/assets/layers-B6RgfpPR.js +1 -0
- package/dist/assets/loader-circle-BoUkhDjq.js +1 -0
- package/dist/assets/monitor-CrpvInqG.js +1 -0
- package/dist/assets/panel-left-close-CVFakVaA.js +1 -0
- package/dist/assets/plus-FQRsZBoJ.js +1 -0
- package/dist/assets/rotate-ccw-CWE4pOjs.js +1 -0
- package/dist/assets/save-Ctv8vW9H.js +1 -0
- package/dist/assets/schema-helpers-DvficKko.js +1 -0
- package/dist/assets/send-F-mJeL54.js +1 -0
- package/dist/assets/smartphone-EPce6VdP.js +1 -0
- package/dist/assets/trash-2-CAUF1KtI.js +1 -0
- package/dist/assets/useLocalStorage-C805uplI.js +1 -0
- package/dist/assets/useNodes-D2-VxsxR.js +1 -0
- package/dist/assets/user-IFlT6mYq.js +1 -0
- package/dist/build-seed.js +495 -0
- package/dist/{dist-Dy7-HUEG.js → dist-D_Q1y4Kh.js} +2314 -1180
- package/dist/index.html +34 -2
- package/dist/index.js +2298 -1158
- package/dist/schema-compiler.js +5 -5
- package/documentation/EXTENSIONS.md +204 -0
- package/extension-registry.js +5 -0
- package/package.json +9 -2
- package/schema.sql +34 -1
- package/templates/astro-starter/old_src/components/blocks/BlockRenderer.astro +143 -0
- package/templates/astro-starter/old_src/components/blocks/ColumnLayout.astro +67 -0
- package/templates/astro-starter/old_src/components/blocks/HeaderImage.astro +126 -0
- package/templates/astro-starter/old_src/components/blocks/Hero.astro +218 -0
- package/templates/astro-starter/old_src/components/blocks/HtmlBlock.astro +25 -0
- package/templates/astro-starter/old_src/components/blocks/ImageBlock.astro +91 -0
- package/templates/astro-starter/old_src/components/blocks/NodeList.astro +274 -0
- package/templates/astro-starter/old_src/components/blocks/TextBlock.astro +15 -0
- package/templates/astro-starter/old_src/components/global/Breadcrumb.astro +108 -0
- package/templates/astro-starter/old_src/components/global/Footer.astro +50 -0
- package/templates/astro-starter/old_src/components/global/Header.astro +171 -0
- package/templates/astro-starter/old_src/components/global/MetaHead.astro +266 -0
- package/templates/astro-starter/old_src/components/global/NavigationMenu.astro +413 -0
- package/templates/astro-starter/old_src/components/ui/KamaImage.astro +180 -0
- package/templates/astro-starter/old_src/layouts/SkeletonLayout.astro +56 -0
- package/templates/astro-starter/old_src/layouts/StandardLayout.astro +63 -0
- package/templates/astro-starter/old_src/lib/cms.ts +219 -0
- package/templates/astro-starter/old_src/lib/media/kama-image-service.ts +67 -0
- package/templates/astro-starter/old_src/lib/media/media-proxy.ts +148 -0
- package/templates/astro-starter/old_src/lib/media/media-url.ts +112 -0
- package/templates/astro-starter/old_src/middleware.ts +536 -0
- package/templates/astro-starter/old_src/pages/404.astro +46 -0
- package/templates/astro-starter/old_src/pages/[...slug].astro +311 -0
- package/templates/astro-starter/old_src/pages/api/load-nodes.ts +68 -0
- package/templates/astro-starter/old_src/pages/api/partials/node-cards.astro +101 -0
- package/templates/astro-starter/old_src/pages/llms-full.txt.ts +171 -0
- package/templates/astro-starter/old_src/pages/llms.txt.ts +113 -0
- package/templates/astro-starter/old_src/pages/media/[...path].ts +14 -0
- package/templates/astro-starter/old_src/pages/partials/footer.astro +69 -0
- package/templates/astro-starter/old_src/pages/partials/nav.astro +141 -0
- package/templates/astro-starter/old_src/styles/standard-theme.css +157 -0
- package/templates/astro-starter/old_src/utils/tiptap.ts +113 -0
- package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +9 -0
- package/templates/astro-starter/src/components/blocks/Form.astro +143 -0
- package/templates/astro-starter/src/components/blocks/HeaderImage.astro +6 -12
- package/templates/astro-starter/src/components/blocks/ImageBlock.astro +5 -6
- package/templates/astro-starter/src/components/blocks/NodeList.astro +274 -0
- package/templates/astro-starter/src/components/global/Breadcrumb.astro +50 -9
- package/templates/astro-starter/src/components/global/Footer.astro +7 -46
- package/templates/astro-starter/src/components/global/Header.astro +10 -16
- package/templates/astro-starter/src/components/global/NavigationMenu.astro +57 -46
- package/templates/astro-starter/src/components/ui/KamaImage.astro +13 -5
- package/templates/astro-starter/src/layouts/StandardLayout.astro +4 -3
- package/templates/astro-starter/src/lib/cms.ts +33 -11
- package/templates/astro-starter/src/lib/media/kama-image-service.ts +15 -4
- package/templates/astro-starter/src/lib/media/media-proxy.ts +22 -3
- package/templates/astro-starter/src/lib/media/media-url.ts +17 -3
- package/templates/astro-starter/src/middleware.ts +158 -100
- package/templates/astro-starter/src/pages/[...slug].astro +77 -3
- package/templates/astro-starter/src/pages/api/load-nodes.ts +68 -0
- package/templates/astro-starter/src/pages/api/partials/node-cards.astro +101 -0
- package/templates/kama-worker/index.js +38 -15
- package/templates/kama-worker/server.js +13 -5
- package/templates/kama-worker/setup-local-db.mjs +17 -0
- package/dist/assets/index-F2rfUymo.css +0 -1
- package/dist/assets/index-lXxH3VFK.js +0 -164
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
---
|
|
2
|
+
// src/components/blocks/Form.astro
|
|
3
|
+
interface Props {
|
|
4
|
+
data: {
|
|
5
|
+
formId: string;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { data } = Astro.props;
|
|
10
|
+
const formId = data?.formId;
|
|
11
|
+
|
|
12
|
+
const kamaApiUrl = import.meta.env.KAMA_API_URL || 'http://localhost:8787';
|
|
13
|
+
const turnstileSiteKey = import.meta.env.PUBLIC_TURNSTILE_SITE_KEY;
|
|
14
|
+
|
|
15
|
+
// Fetch form YAML schema at render time
|
|
16
|
+
let schema = null;
|
|
17
|
+
if (formId) {
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch(`${kamaApiUrl}/api/ext/kama-forms/render/${formId}`);
|
|
20
|
+
if (res.ok) schema = await res.json();
|
|
21
|
+
} catch (err) {
|
|
22
|
+
console.error(`Failed to fetch schema for ${formId}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const requiresTurnstile = schema?.settings?.requireTurnstile === true;
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
{schema ? (
|
|
30
|
+
<div class="kama-form-wrapper" data-form-id={formId} data-api-url={kamaApiUrl}>
|
|
31
|
+
|
|
32
|
+
<!-- 🎯 Conditionally load Cloudflare Turnstile Script -->
|
|
33
|
+
{requiresTurnstile && (
|
|
34
|
+
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
|
35
|
+
)}
|
|
36
|
+
|
|
37
|
+
<div class="form-header">
|
|
38
|
+
<h3>{schema.title}</h3>
|
|
39
|
+
{schema.description && <p>{schema.description}</p>}
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div class="form-alert success" style="display: none;"></div>
|
|
43
|
+
<div class="form-alert error" style="display: none;"></div>
|
|
44
|
+
|
|
45
|
+
<form class="kama-public-form">
|
|
46
|
+
{schema.fields?.map((field: any) => (
|
|
47
|
+
<div class="form-group">
|
|
48
|
+
<label for={`field-${field.name}`}>
|
|
49
|
+
{field.label}
|
|
50
|
+
{field.required && <span class="required">*</span>}
|
|
51
|
+
</label>
|
|
52
|
+
|
|
53
|
+
{field.type === 'textarea' ? (
|
|
54
|
+
<textarea id={`field-${field.name}`} name={field.name} placeholder={field.placeholder || ''} required={field.required} rows="4" />
|
|
55
|
+
) : field.type === 'select' ? (
|
|
56
|
+
<select id={`field-${field.name}`} name={field.name} required={field.required}>
|
|
57
|
+
<option value="" disabled selected>{field.placeholder || 'Select...'}</option>
|
|
58
|
+
{field.options?.map((opt: any) => (
|
|
59
|
+
<option value={opt.value}>{opt.label}</option>
|
|
60
|
+
))}
|
|
61
|
+
</select>
|
|
62
|
+
) : (
|
|
63
|
+
<input type={field.type || 'text'} id={`field-${field.name}`} name={field.name} placeholder={field.placeholder || ''} required={field.required} />
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
))}
|
|
67
|
+
|
|
68
|
+
<!-- 🎯 Turnstile Widget renders here -->
|
|
69
|
+
{requiresTurnstile && turnstileSiteKey && (
|
|
70
|
+
<div class="form-group turnstile-container">
|
|
71
|
+
<div class="cf-turnstile" data-sitekey={turnstileSiteKey}></div>
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
<button type="submit" class="submit-btn">Submit</button>
|
|
76
|
+
</form>
|
|
77
|
+
</div>
|
|
78
|
+
) : (
|
|
79
|
+
<p class="form-error">Form definition "{formId}" could not be loaded.</p>
|
|
80
|
+
)}
|
|
81
|
+
|
|
82
|
+
<script>
|
|
83
|
+
document.querySelectorAll('.kama-form-wrapper').forEach((container) => {
|
|
84
|
+
const form = container.querySelector<HTMLFormElement>('.kama-public-form');
|
|
85
|
+
const formId = container.getAttribute('data-form-id');
|
|
86
|
+
const apiUrl = container.getAttribute('data-api-url');
|
|
87
|
+
const successBox = container.querySelector<HTMLDivElement>('.form-alert.success');
|
|
88
|
+
const errorBox = container.querySelector<HTMLDivElement>('.form-alert.error');
|
|
89
|
+
const submitBtn = container.querySelector<HTMLButtonElement>('.submit-btn');
|
|
90
|
+
|
|
91
|
+
if (!form || !formId || !apiUrl) return;
|
|
92
|
+
|
|
93
|
+
form.addEventListener('submit', async (e) => {
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
if (submitBtn) submitBtn.disabled = true;
|
|
96
|
+
if (successBox) successBox.style.display = 'none';
|
|
97
|
+
if (errorBox) errorBox.style.display = 'none';
|
|
98
|
+
|
|
99
|
+
// FormData automatically picks up the hidden "cf-turnstile-response" input
|
|
100
|
+
const formData = new FormData(form);
|
|
101
|
+
const payload = Object.fromEntries(formData.entries());
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const res = await fetch(`${apiUrl}/api/ext/kama-forms/submit/${formId}`, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: { 'Content-Type': 'application/json' },
|
|
107
|
+
body: JSON.stringify(payload)
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const result = await res.json();
|
|
111
|
+
|
|
112
|
+
if (res.ok && result.success) {
|
|
113
|
+
if (successBox) {
|
|
114
|
+
successBox.textContent = result.message || 'Form submitted successfully!';
|
|
115
|
+
successBox.style.display = 'block';
|
|
116
|
+
}
|
|
117
|
+
form.reset();
|
|
118
|
+
|
|
119
|
+
// 🎯 Reset Turnstile after successful submission if the user wants to submit again
|
|
120
|
+
if (window.turnstile) {
|
|
121
|
+
window.turnstile.reset();
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
if (errorBox) {
|
|
125
|
+
errorBox.textContent = result.error || 'Form submission failed.';
|
|
126
|
+
errorBox.style.display = 'block';
|
|
127
|
+
}
|
|
128
|
+
// 🎯 Reset Turnstile on failure so they get a fresh token
|
|
129
|
+
if (window.turnstile) {
|
|
130
|
+
window.turnstile.reset();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch (err) {
|
|
134
|
+
if (errorBox) {
|
|
135
|
+
errorBox.textContent = 'Server connection error.';
|
|
136
|
+
errorBox.style.display = 'block';
|
|
137
|
+
}
|
|
138
|
+
} finally {
|
|
139
|
+
if (submitBtn) submitBtn.disabled = false;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
</script>
|
|
@@ -45,13 +45,13 @@ if (region.includes("_")) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
* Default single-column layout.
|
|
48
|
+
* Default single-column layout (aligned to backend CDN buckets).
|
|
49
49
|
*/
|
|
50
50
|
let maxWidth = "1352";
|
|
51
51
|
let targetColumnWidth = 1352;
|
|
52
52
|
|
|
53
|
-
// 1-col:
|
|
54
|
-
let optimizedWidths = [
|
|
53
|
+
// 1-col: 1x/2x desktop and retina buckets
|
|
54
|
+
let optimizedWidths = [768, 1352, 1920, 2560];
|
|
55
55
|
let sizes = "(max-width: 1399px) calc(100vw - 48px), 1352px";
|
|
56
56
|
|
|
57
57
|
/**
|
|
@@ -61,8 +61,7 @@ if (columnCount === 2) {
|
|
|
61
61
|
maxWidth = "660";
|
|
62
62
|
targetColumnWidth = 660;
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
optimizedWidths = [400, 660, 800, 1320];
|
|
64
|
+
optimizedWidths = [640, 768, 1024, 1352];
|
|
66
65
|
sizes =
|
|
67
66
|
"(max-width: 767px) calc(100vw - 48px), " +
|
|
68
67
|
"(max-width: 1399px) calc((100vw - 80px) / 2), " +
|
|
@@ -76,8 +75,7 @@ else if (columnCount === 3) {
|
|
|
76
75
|
maxWidth = "429";
|
|
77
76
|
targetColumnWidth = 429;
|
|
78
77
|
|
|
79
|
-
|
|
80
|
-
optimizedWidths = [300, 429, 858, 1024];
|
|
78
|
+
optimizedWidths = [320, 640, 768, 1024];
|
|
81
79
|
sizes =
|
|
82
80
|
"(max-width: 767px) calc(100vw - 48px), " +
|
|
83
81
|
"(max-width: 1399px) calc((100vw - 112px) / 3), " +
|
|
@@ -91,8 +89,7 @@ else if (columnCount === 4) {
|
|
|
91
89
|
maxWidth = "314";
|
|
92
90
|
targetColumnWidth = 314;
|
|
93
91
|
|
|
94
|
-
|
|
95
|
-
optimizedWidths = [264, 314, 628, 1024];
|
|
92
|
+
optimizedWidths = [320, 640, 768];
|
|
96
93
|
sizes =
|
|
97
94
|
"(max-width: 767px) calc(100vw - 48px), " +
|
|
98
95
|
"(max-width: 1399px) calc((100vw - 144px) / 4), " +
|
|
@@ -106,12 +103,9 @@ else if (columnCount === 4) {
|
|
|
106
103
|
sizes={sizes}
|
|
107
104
|
desktopWidths={optimizedWidths}
|
|
108
105
|
idealWidth={targetColumnWidth}
|
|
109
|
-
width={data.width || data.image_meta?.width}
|
|
110
|
-
height={data.height || data.image_meta?.height}
|
|
111
106
|
class="responsive-header-image"
|
|
112
107
|
loading={isLcp ? "eager" : "lazy"}
|
|
113
108
|
fetchpriority={isLcp ? "high" : "auto"}
|
|
114
|
-
decoding={isLcp ? "sync" : "async"}
|
|
115
109
|
/>
|
|
116
110
|
</div>
|
|
117
111
|
|
|
@@ -26,31 +26,31 @@ if (region.includes("_")) {
|
|
|
26
26
|
if (region.includes("_col4")) columnCount = 4;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
// 🎯 Column Config Lookup (
|
|
29
|
+
// 🎯 Column Config Lookup (Target width, aligned CDN breakpoint widths, sizes)
|
|
30
30
|
const COLUMN_CONFIGS: Record<
|
|
31
31
|
number,
|
|
32
32
|
{ targetWidth: number; widths: number[]; sizes: string }
|
|
33
33
|
> = {
|
|
34
34
|
1: {
|
|
35
35
|
targetWidth: 1352,
|
|
36
|
-
widths: [768, 1352],
|
|
36
|
+
widths: [768, 1352, 1920, 2560],
|
|
37
37
|
sizes: "(max-width: 1399px) calc(100vw - 48px), 1352px",
|
|
38
38
|
},
|
|
39
39
|
2: {
|
|
40
40
|
targetWidth: 660,
|
|
41
|
-
widths: [
|
|
41
|
+
widths: [640, 768, 1024, 1352],
|
|
42
42
|
sizes:
|
|
43
43
|
"(max-width: 767px) calc(100vw - 48px), (max-width: 1399px) calc((100vw - 80px) / 2), 660px",
|
|
44
44
|
},
|
|
45
45
|
3: {
|
|
46
46
|
targetWidth: 429,
|
|
47
|
-
widths: [
|
|
47
|
+
widths: [320, 640, 768, 1024],
|
|
48
48
|
sizes:
|
|
49
49
|
"(max-width: 767px) calc(100vw - 48px), (max-width: 1399px) calc((100vw - 112px) / 3), 429px",
|
|
50
50
|
},
|
|
51
51
|
4: {
|
|
52
52
|
targetWidth: 314,
|
|
53
|
-
widths: [
|
|
53
|
+
widths: [320, 640, 768],
|
|
54
54
|
sizes:
|
|
55
55
|
"(max-width: 767px) calc(100vw - 48px), (max-width: 1399px) calc((100vw - 144px) / 4), 314px",
|
|
56
56
|
},
|
|
@@ -67,7 +67,6 @@ const layout = COLUMN_CONFIGS[columnCount] || COLUMN_CONFIGS[1];
|
|
|
67
67
|
sizes={layout.sizes}
|
|
68
68
|
desktopWidths={layout.widths}
|
|
69
69
|
idealWidth={layout.targetWidth}
|
|
70
|
-
maxWidth={layout.targetWidth}
|
|
71
70
|
class="free-crop-image"
|
|
72
71
|
loading={isLcp ? "eager" : "lazy"}
|
|
73
72
|
fetchpriority={isLcp ? "high" : "auto"}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
---
|
|
2
|
+
// src/components/blocks/NodeList.astro
|
|
3
|
+
import KamaImage from "@/components/ui/KamaImage.astro";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
data: Record<string, any>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { data } = Astro.props;
|
|
10
|
+
const items = data.initialItems || [];
|
|
11
|
+
const listTitle = data.listTitle || "";
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<div class="cms-block node-list-wrapper">
|
|
15
|
+
{listTitle && <h2 class="node-list-title">{listTitle}</h2>}
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
items.length === 0 ? (
|
|
19
|
+
<p class="empty-state">No items found.</p>
|
|
20
|
+
) : (
|
|
21
|
+
<div class="node-grid" id={`grid-${data.blockId}`}>
|
|
22
|
+
{items.map((item: any) => (
|
|
23
|
+
<article class="node-card">
|
|
24
|
+
<a href={`/${item.slug}`} class="node-card-link">
|
|
25
|
+
{/* 🎯 ARCHITECTURAL FIX: Use KamaImage for initial load optimization */}
|
|
26
|
+
{item.image ? (
|
|
27
|
+
<KamaImage
|
|
28
|
+
image={item.image}
|
|
29
|
+
sizes="(max-width: 639px) 100vw, (max-width: 1023px) 50vw, 33vw"
|
|
30
|
+
desktopWidths={[320, 640, 768, 1024]}
|
|
31
|
+
idealWidth={429}
|
|
32
|
+
loading="lazy"
|
|
33
|
+
/>
|
|
34
|
+
) : (
|
|
35
|
+
<div class="image-placeholder" />
|
|
36
|
+
)}
|
|
37
|
+
<div class="node-card-content">
|
|
38
|
+
<h3 class="node-card-title">{item.title}</h3>
|
|
39
|
+
{item.createdAt && (
|
|
40
|
+
<time datetime={item.createdAt} class="node-card-date">
|
|
41
|
+
{new Date(item.createdAt).toLocaleDateString("en-GB", {
|
|
42
|
+
day: "numeric",
|
|
43
|
+
month: "short",
|
|
44
|
+
year: "numeric",
|
|
45
|
+
})}
|
|
46
|
+
</time>
|
|
47
|
+
)}
|
|
48
|
+
{item.excerpt && (
|
|
49
|
+
<p class="node-card-excerpt">{item.excerpt}</p>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
</a>
|
|
53
|
+
</article>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
{
|
|
60
|
+
data.loadMoreUrl && (
|
|
61
|
+
<div class="load-more-container">
|
|
62
|
+
<button
|
|
63
|
+
class="load-more-btn"
|
|
64
|
+
data-url={data.loadMoreUrl}
|
|
65
|
+
data-target={`grid-${data.blockId}`}
|
|
66
|
+
>
|
|
67
|
+
Load More
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<style is:global>
|
|
75
|
+
.node-list-wrapper {
|
|
76
|
+
margin: 2rem 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.node-list-title {
|
|
80
|
+
font-size: 1.5rem;
|
|
81
|
+
font-weight: 700;
|
|
82
|
+
margin-bottom: 1.5rem;
|
|
83
|
+
color: #1e293b;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.node-list-wrapper .empty-state {
|
|
87
|
+
color: #64748b;
|
|
88
|
+
font-style: italic;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.node-grid {
|
|
92
|
+
display: grid;
|
|
93
|
+
grid-template-columns: repeat(1, 1fr);
|
|
94
|
+
gap: 1.5rem;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@media (min-width: 640px) {
|
|
98
|
+
.node-grid {
|
|
99
|
+
grid-template-columns: repeat(2, 1fr);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@media (min-width: 1024px) {
|
|
104
|
+
.node-grid {
|
|
105
|
+
grid-template-columns: repeat(3, 1fr);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.node-card {
|
|
110
|
+
border: 1px solid #e2e8f0;
|
|
111
|
+
border-radius: 8px;
|
|
112
|
+
overflow: hidden;
|
|
113
|
+
background: #fff;
|
|
114
|
+
transition:
|
|
115
|
+
transform 0.2s ease,
|
|
116
|
+
box-shadow 0.2s ease;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.node-card:hover {
|
|
120
|
+
transform: translateY(-2px);
|
|
121
|
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.node-card-link {
|
|
125
|
+
text-decoration: none;
|
|
126
|
+
color: inherit;
|
|
127
|
+
display: flex;
|
|
128
|
+
flex-direction: column;
|
|
129
|
+
height: 100%;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* 🎯 Because of is:global, this styling will automatically apply to <KamaImage> outputs! */
|
|
133
|
+
.node-card img,
|
|
134
|
+
.node-card .image-placeholder {
|
|
135
|
+
width: 100%;
|
|
136
|
+
height: auto; /* 🎯 FIX: Allows tall images to display their natural height */
|
|
137
|
+
object-fit: cover;
|
|
138
|
+
background: #f1f5f9;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.node-card .image-placeholder {
|
|
142
|
+
aspect-ratio: 16 / 9; /* Keep the placeholder a rectangle if there is no image at all */
|
|
143
|
+
}
|
|
144
|
+
.node-card-content {
|
|
145
|
+
padding: 1.25rem;
|
|
146
|
+
display: flex;
|
|
147
|
+
flex-direction: column;
|
|
148
|
+
flex-grow: 1;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.node-card-title {
|
|
152
|
+
font-size: 1.125rem;
|
|
153
|
+
font-weight: 600;
|
|
154
|
+
margin: 0 0 0.5rem 0;
|
|
155
|
+
color: #0f172a;
|
|
156
|
+
line-height: 1.4;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.node-card-date {
|
|
160
|
+
font-size: 0.875rem;
|
|
161
|
+
color: #64748b;
|
|
162
|
+
margin-bottom: 0.75rem;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.node-card-excerpt {
|
|
166
|
+
font-size: 0.9375rem;
|
|
167
|
+
color: #475569;
|
|
168
|
+
margin: 0;
|
|
169
|
+
line-height: 1.5;
|
|
170
|
+
display: -webkit-box;
|
|
171
|
+
-webkit-line-clamp: 3;
|
|
172
|
+
-webkit-box-orient: vertical;
|
|
173
|
+
overflow: hidden;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.load-more-container {
|
|
177
|
+
margin-top: 2rem;
|
|
178
|
+
text-align: center;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.load-more-btn {
|
|
182
|
+
padding: 0.75rem 1.5rem;
|
|
183
|
+
background: #0f172a;
|
|
184
|
+
color: #fff;
|
|
185
|
+
border: none;
|
|
186
|
+
border-radius: 6px;
|
|
187
|
+
font-weight: 600;
|
|
188
|
+
cursor: pointer;
|
|
189
|
+
transition: background 0.2s;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.load-more-btn:hover {
|
|
193
|
+
background: #334155;
|
|
194
|
+
}
|
|
195
|
+
</style>
|
|
196
|
+
|
|
197
|
+
<script>
|
|
198
|
+
function initLoadMoreButtons() {
|
|
199
|
+
const buttons = document.querySelectorAll(".load-more-btn:not(.is-bound)");
|
|
200
|
+
|
|
201
|
+
buttons.forEach(btn => {
|
|
202
|
+
btn.classList.add("is-bound");
|
|
203
|
+
|
|
204
|
+
btn.addEventListener("click", async e => {
|
|
205
|
+
const targetBtn = e.currentTarget as HTMLButtonElement;
|
|
206
|
+
const urlString = targetBtn.getAttribute("data-url");
|
|
207
|
+
const targetId = targetBtn.getAttribute("data-target");
|
|
208
|
+
|
|
209
|
+
if (!urlString || !targetId) return;
|
|
210
|
+
|
|
211
|
+
const grid = document.getElementById(targetId);
|
|
212
|
+
if (!grid) return;
|
|
213
|
+
|
|
214
|
+
const urlObj = new URL(urlString, window.location.origin);
|
|
215
|
+
// 🎯 ARCHITECTURAL FIX: Route to the new HTML Partial Endpoint
|
|
216
|
+
const proxyUrl = `/api/partials/node-cards${urlObj.search}`;
|
|
217
|
+
|
|
218
|
+
targetBtn.textContent = "Loading...";
|
|
219
|
+
targetBtn.disabled = true;
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const res = await fetch(proxyUrl);
|
|
223
|
+
|
|
224
|
+
if (!res.ok) {
|
|
225
|
+
throw new Error(`HTTP error! status: ${res.status}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 1. Read metadata from headers sent by our partial endpoint
|
|
229
|
+
const rawCountStr = res.headers.get("X-Consumed-Rows");
|
|
230
|
+
const limitStr = res.headers.get("X-Limit");
|
|
231
|
+
const consumedRows = parseInt(rawCountStr || "0", 10);
|
|
232
|
+
const limit = parseInt(limitStr || "12", 10);
|
|
233
|
+
|
|
234
|
+
// 2. Extract and render the raw HTML
|
|
235
|
+
const htmlSnippet = await res.text();
|
|
236
|
+
|
|
237
|
+
if (htmlSnippet.trim().length > 0) {
|
|
238
|
+
grid.insertAdjacentHTML("beforeend", htmlSnippet);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 3. Handle Pagination
|
|
242
|
+
if (consumedRows >= limit && limit > 0) {
|
|
243
|
+
const currentOffset = parseInt(
|
|
244
|
+
urlObj.searchParams.get("offset") || "0",
|
|
245
|
+
10,
|
|
246
|
+
);
|
|
247
|
+
urlObj.searchParams.set(
|
|
248
|
+
"offset",
|
|
249
|
+
(currentOffset + limit).toString(),
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
targetBtn.setAttribute("data-url", urlObj.pathname + urlObj.search);
|
|
253
|
+
targetBtn.textContent = "Load More";
|
|
254
|
+
targetBtn.disabled = false;
|
|
255
|
+
} else {
|
|
256
|
+
if (targetBtn.parentElement)
|
|
257
|
+
targetBtn.parentElement.style.display = "none";
|
|
258
|
+
}
|
|
259
|
+
} catch (err) {
|
|
260
|
+
console.error("Failed to load more items:", err);
|
|
261
|
+
targetBtn.textContent = "Error. Try again";
|
|
262
|
+
targetBtn.disabled = false;
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (document.readyState === "loading") {
|
|
269
|
+
document.addEventListener("DOMContentLoaded", initLoadMoreButtons);
|
|
270
|
+
} else {
|
|
271
|
+
initLoadMoreButtons();
|
|
272
|
+
}
|
|
273
|
+
document.addEventListener("astro:page-load", initLoadMoreButtons);
|
|
274
|
+
</script>
|
|
@@ -7,24 +7,60 @@ interface Props {
|
|
|
7
7
|
isHomePage?: boolean;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
const { title,
|
|
10
|
+
const { title, isHomePage } = Astro.props;
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const currentPath = Astro.url.pathname;
|
|
13
|
+
|
|
14
|
+
// Extract non-empty path segments
|
|
15
|
+
const rawSegments = currentPath.split("/").filter(Boolean);
|
|
16
|
+
|
|
17
|
+
// Detect homepage
|
|
18
|
+
const isHome = isHomePage || currentPath === "/" || rawSegments.length === 0;
|
|
19
|
+
|
|
20
|
+
function formatSegmentTitle(segment: string): string {
|
|
21
|
+
return segment
|
|
22
|
+
.split("-")
|
|
23
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
24
|
+
.join(" ");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Build progressive paths: /some-page, /some-page/child-page, etc.
|
|
28
|
+
let runningPath = "";
|
|
29
|
+
const breadcrumbItems = rawSegments.map((segment, index) => {
|
|
30
|
+
runningPath += `/${segment}`;
|
|
31
|
+
const isLast = index === rawSegments.length - 1;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
label: isLast ? title : formatSegmentTitle(segment),
|
|
35
|
+
href: runningPath,
|
|
36
|
+
isCurrent: isLast,
|
|
37
|
+
};
|
|
38
|
+
});
|
|
14
39
|
---
|
|
15
40
|
|
|
16
41
|
<nav class="breadcrumb-nav" aria-label="Breadcrumb">
|
|
17
42
|
<ol class="breadcrumb-list">
|
|
43
|
+
<!-- Home Link -->
|
|
18
44
|
<li class="breadcrumb-item">
|
|
19
45
|
<a href="/">Home</a>
|
|
20
46
|
</li>
|
|
47
|
+
|
|
48
|
+
<!-- Subpage Breadcrumbs -->
|
|
21
49
|
{
|
|
22
|
-
!isHome &&
|
|
23
|
-
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
50
|
+
!isHome &&
|
|
51
|
+
breadcrumbItems.map(item => (
|
|
52
|
+
<li
|
|
53
|
+
class={`breadcrumb-item ${item.isCurrent ? "active" : ""}`}
|
|
54
|
+
aria-current={item.isCurrent ? "page" : undefined}
|
|
55
|
+
>
|
|
56
|
+
<span class="separator">/</span>
|
|
57
|
+
{item.isCurrent ? (
|
|
58
|
+
<span>{item.label}</span>
|
|
59
|
+
) : (
|
|
60
|
+
<a href={item.href}>{item.label}</a>
|
|
61
|
+
)}
|
|
62
|
+
</li>
|
|
63
|
+
))
|
|
28
64
|
}
|
|
29
65
|
</ol>
|
|
30
66
|
</nav>
|
|
@@ -45,6 +81,11 @@ const isHome = isHomePage || Astro.url.pathname === "/" || !slug || slug === "";
|
|
|
45
81
|
color: #64748b;
|
|
46
82
|
}
|
|
47
83
|
|
|
84
|
+
.breadcrumb-item {
|
|
85
|
+
display: flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
}
|
|
88
|
+
|
|
48
89
|
.breadcrumb-item a {
|
|
49
90
|
color: #334155;
|
|
50
91
|
text-decoration: none;
|
|
@@ -2,60 +2,21 @@
|
|
|
2
2
|
// src/components/global/Footer.astro
|
|
3
3
|
|
|
4
4
|
import BlockRenderer from "@/components/blocks/BlockRenderer.astro";
|
|
5
|
-
import { getChannelId } from "@/lib/cms";
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const channelId = getChannelId() || "website";
|
|
11
|
-
|
|
12
|
-
const customFetch = (input: RequestInfo | URL, init?: RequestInit) => {
|
|
13
|
-
const reqInit = { ...init };
|
|
14
|
-
delete reqInit.cache;
|
|
15
|
-
|
|
16
|
-
let urlStr = input instanceof URL ? input.toString() : String(input);
|
|
17
|
-
if (urlStr.startsWith("/")) {
|
|
18
|
-
const baseUrl =
|
|
19
|
-
import.meta.env.PUBLIC_API_URL ||
|
|
20
|
-
safeEnv.PUBLIC_API_URL ||
|
|
21
|
-
"http://127.0.0.1:8787";
|
|
22
|
-
urlStr = `${baseUrl}${urlStr}`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const req = new Request(urlStr, reqInit);
|
|
26
|
-
if (cmsWorker && typeof cmsWorker.fetch === "function") {
|
|
27
|
-
return cmsWorker.fetch(req);
|
|
28
|
-
}
|
|
29
|
-
return fetch(req);
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// 🎯 Resolve the Home Page slug
|
|
33
|
-
const homeSlug = String(import.meta.env.PUBLIC_HOME_SLUG || "home").trim();
|
|
34
|
-
|
|
35
|
-
let footerBlocks: any[] = [];
|
|
36
|
-
let contentMap: Record<string, any[]> = {};
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
const res = await customFetch(
|
|
40
|
-
`/api/v1/node/${homeSlug}?channelId=${channelId}`,
|
|
41
|
-
);
|
|
42
|
-
if (res.ok) {
|
|
43
|
-
const data = await res.json();
|
|
44
|
-
const allContent = data.content || {};
|
|
45
|
-
footerBlocks = allContent.footer || [];
|
|
46
|
-
contentMap = allContent;
|
|
47
|
-
}
|
|
48
|
-
} catch (e) {
|
|
49
|
-
console.error("[Footer] Failed to fetch home page blocks:", e);
|
|
6
|
+
interface Props {
|
|
7
|
+
blocks?: any[];
|
|
8
|
+
contentMap?: Record<string, any[]>;
|
|
50
9
|
}
|
|
10
|
+
|
|
11
|
+
const { blocks = [], contentMap = {} } = Astro.props;
|
|
51
12
|
---
|
|
52
13
|
|
|
53
14
|
<footer class="site-footer">
|
|
54
15
|
<div class="footer-container">
|
|
55
16
|
{
|
|
56
|
-
|
|
17
|
+
blocks && blocks.length > 0 ? (
|
|
57
18
|
<BlockRenderer
|
|
58
|
-
blocks={
|
|
19
|
+
blocks={blocks}
|
|
59
20
|
contentMap={contentMap}
|
|
60
21
|
region="footer"
|
|
61
22
|
/>
|