Skip to content

Commit e046ed2

Browse files
authored
Redesign the official site landing page (#1520)
Add responsive live SQLPage examples, flowing section transitions and an interactive sculpture with accessible fallbacks. Organize the implementation into documented controllers and standalone GLSL sources, keep form demos private, and preserve homepage SEO metadata and crawlable navigation. Validated with frontend checks, 143 browser tests, official-site Hurl tests, and all 51 GitHub CI checks.
1 parent a826e7c commit e046ed2

82 files changed

Lines changed: 8930 additions & 1522 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# CHANGELOG.md
22

33
## v0.47.0 (unreleased)
4+
- Redesigned the official website homepage with live SQLPage examples, an interactive database sculpture, flowing section transitions, responsive layouts, and direct links to documentation and deployment guides.
45
- Loading embedded card content no longer adds duplicate form submission or file validation handlers to existing fields. Fragment initialization also includes the element announcing the fragment.
56
- **Mac users:** the downloadable `sqlpage-macos.tgz` is now code-signed with a Developer ID certificate and notarized by Apple. macOS may still ask you to confirm opening it the first time. The binary can still load third-party ODBC database drivers.
67
- **Mac users:** the downloadable `sqlpage-macos.tgz` now runs natively on Apple silicon (M-series Macs) and no longer runs on Intel Macs. Homebrew remains the recommended and easiest installation method. On an Intel Mac, [install Homebrew](https://brew.sh/) if needed, then run `brew install sqlpage` (or `brew update` followed by `brew upgrade sqlpage` if you already installed it with Homebrew). Open Terminal in your existing website folder and run `sqlpage` instead of `./sqlpage.bin`; keep your SQL files, database, and `sqlpage` configuration folder in place. Intel installations may build from source and take longer; see the [macOS installation guide](https://sql-page.com/your-first-sql-website/?os=macos#download) for setup and older macOS requirements.

‎examples/official-site/README.md‎

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,61 @@
33
The SQLPage website is of course built with SQLPage !
44

55
Things that you may be interested to look at:
6-
- The [custom component we use for our stylish home page](./sqlpage/templates/shell-home.handlebars)
6+
- The [custom component we use for our stylish home page](./sqlpage/templates/landing-page.handlebars)
77
- The [migrations](./sqlpage/migrations) that populate the database with the documentation for all components
88
- The [advanced multistep form example](./examples/multistep-form/)
99

1010
It is hosted as a simple Docker container on [sql-page.com](https://sql-page.com).
1111

12-
Feel free to [open a pull request](https://github.com/lovasoa/sqlpage/pulls) if you would like to add or change anything !
12+
Feel free to [open a pull request](https://github.com/lovasoa/sqlpage/pulls) if you would like to add or change anything !
13+
14+
The home page is rendered by `index.sql` and the `landing-page` custom template.
15+
Its styles, ES modules, model, lighting map, and static preview are under
16+
`assets/landing/`; no separate HTML entry point or frontend build is required.
17+
Internal navigation uses site-relative URLs so it also works in local previews.
18+
19+
A small nonce-protected marker in the template reserves the animated opening
20+
before first paint; it does not run when JavaScript is disabled. The static
21+
preview appears once its page anchor is measured, and WebGL readiness changes
22+
only the renderer, so slow or failed 3D loading does not shift the page.
23+
24+
The landing page runs as native ES modules. `main.js` starts independent
25+
navigation, selectors, streaming timeline, section transitions, waves, and scene
26+
controllers; each returns its cleanup function. `sections.js` shares keyboard tab
27+
behavior through `tabs.js` and delegates iframe/source switching to
28+
`component-demos.js`. Each live preview uses the normal SQLPage shell and built-in
29+
components in `landing-demos/`, with `demo.css` sizing the rendered content.
30+
31+
Demo customer, ticket, feedback and profile defaults come from
32+
`78_landing_demos.sql`. The source endpoint reads the same SQL files from a fixed
33+
allowlist, never an arbitrary request path. Profile submissions are validated on
34+
the server and rendered in that POST response only. They do not change the shared
35+
database, create visitor storage, or put submitted values in URLs. A new GET of
36+
the form restores its seeded defaults.
37+
38+
`section-motion.js` measures authored content before applying sticky positioning
39+
and element entrance/outro progress. A short section holds once it fits; a long
40+
section scrolls through its full content before its tail holds beneath the next
41+
section's wave. `wave-motion.js` combines slow idle waves with scroll-driven wind
42+
impulses that decay after scrolling stops. Visual poses and responsive layout
43+
remain in the CSS modules rather than the JavaScript controllers.
44+
45+
`experience.js` isolates the optional WebGL lifecycle. `scroll-progress.js` owns
46+
page geometry and scroll state; `scene/database-scene.js` owns rendering and
47+
interaction. One shared canvas flies from the opening composition to the demo,
48+
then stays at each section's `data-scene-stop` page anchor while rotating. Later
49+
handoffs fade between anchors without interpolating position. Anchor measurements
50+
refresh after content and viewport changes, including iframe resizing.
51+
52+
Scene modules use pinned Three.js 0.186.1 ES modules from jsDelivr. Keep their core
53+
module URLs identical when upgrading. Shaders are ordinary `.glsl` files in
54+
`js/scene/shaders/`, fetched relative to `shader-sources.js`; successful loads are
55+
shared, while failed loads can be retried. The model, lighting map, and static
56+
preview are versioned local assets. The official site's `sqlpage.yaml` allows
57+
jsDelivr scripts while preserving SQLPage's per-request nonce.
58+
59+
Without JavaScript the document retains its content, initial live example, and
60+
navigation. CDN or WebGL failure leaves the static sculpture preview and working
61+
controls, and a retry is available for the optional scene. Reduced motion disables
62+
the pinned opening, covering transitions, and idle animations. SQL source uses a
63+
pinned SQL-only Highlight.js grammar with readable plain text if its CDN fails.
20.9 KB
Loading
Binary file not shown.
105 KB
Loading
Lines changed: 28 additions & 0 deletions
Loading
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import { initSqlHighlight } from "./sql-highlight.js";
2+
import { initTabs, selectTab } from "./tabs.js";
3+
4+
/** Prepare the next live example before changing the visible preview and SQL. */
5+
export function initComponentDemos(root, signal) {
6+
const panel = root.querySelector("#demo-panel");
7+
const viewport = panel.querySelector(".demo-preview");
8+
const source = root.querySelector("#demo-source");
9+
const status = root.querySelector(".demo-status");
10+
const renderSource = initSqlHighlight(source, signal);
11+
const media = matchMedia("(prefers-reduced-motion: reduce)");
12+
const picker = root.querySelector(".component-picker");
13+
let preview = viewport.querySelector("iframe");
14+
let component = "table";
15+
let sourceFiles = { main: source.textContent };
16+
let request;
17+
let pending;
18+
let animation;
19+
let fade;
20+
let resizePreview;
21+
const previewHeight = (content) =>
22+
`${Math.ceil(content.getBoundingClientRect().height) + 2}px`;
23+
24+
function resizePanel(change, entering = false) {
25+
const before = panel.getBoundingClientRect().height;
26+
animation?.cancel();
27+
fade?.cancel();
28+
change();
29+
const after = panel.getBoundingClientRect().height;
30+
if (media.matches) return;
31+
animation = panel.animate(
32+
[
33+
{ height: `${before}px`, opacity: entering ? 0 : 1 },
34+
{ height: `${after}px`, opacity: 1 },
35+
],
36+
{ duration: 320, easing: "cubic-bezier(0.22, 1, 0.36, 1)" },
37+
);
38+
}
39+
40+
function fitPreview() {
41+
resizePreview?.disconnect();
42+
const content = preview.contentDocument?.querySelector(
43+
"#sqlpage_main_wrapper",
44+
);
45+
if (!content) return;
46+
const fit = () => {
47+
const height = previewHeight(content);
48+
if (viewport.style.height !== height)
49+
resizePanel(() => {
50+
viewport.style.height = height;
51+
});
52+
};
53+
resizePreview = new ResizeObserver(fit);
54+
resizePreview.observe(content);
55+
fit();
56+
}
57+
preview.addEventListener("load", fitPreview, { signal });
58+
fitPreview();
59+
60+
root.querySelector("[data-copy-sql]").addEventListener(
61+
"click",
62+
async () => {
63+
try {
64+
await navigator.clipboard.writeText(source.textContent);
65+
status.textContent = "SQL copied.";
66+
} catch {
67+
status.textContent = "Copy unavailable. Select the SQL below.";
68+
}
69+
},
70+
{ signal },
71+
);
72+
initTabs(
73+
root.querySelector(".source-tabs"),
74+
"aria-selected",
75+
(button) => {
76+
source.setAttribute("aria-labelledby", button.id);
77+
status.textContent = "";
78+
resizePanel(() => {
79+
void renderSource(sourceFiles[button.dataset.file]);
80+
});
81+
},
82+
signal,
83+
);
84+
85+
function loadPreview(frame, requestSignal) {
86+
return new Promise((resolve, reject) => {
87+
let observer;
88+
const abort = () => {
89+
observer?.disconnect();
90+
reject(new DOMException("Demo cancelled", "AbortError"));
91+
};
92+
requestSignal.addEventListener("abort", abort, { once: true });
93+
frame.addEventListener(
94+
"load",
95+
async () => {
96+
const document = frame.contentDocument;
97+
if (!document?.querySelector("#sqlpage_main_wrapper")) {
98+
reject(new Error("Preview unavailable"));
99+
return;
100+
}
101+
const ready = () => {
102+
if (document.querySelector("[data-pre-init]")) return;
103+
observer?.disconnect();
104+
requestSignal.removeEventListener("abort", abort);
105+
resolve();
106+
};
107+
await document.fonts.ready;
108+
if (requestSignal.aborted) return;
109+
observer = new MutationObserver(ready);
110+
observer.observe(document, { attributes: true, subtree: true });
111+
ready();
112+
},
113+
{ once: true, signal: requestSignal },
114+
);
115+
});
116+
}
117+
118+
async function selectDemo(button) {
119+
request?.abort();
120+
pending?.remove();
121+
fade?.cancel();
122+
const next = button.dataset.demo;
123+
if (next === component) {
124+
panel.removeAttribute("aria-busy");
125+
return;
126+
}
127+
const controller = new AbortController();
128+
request = controller;
129+
const incoming = preview.cloneNode(false);
130+
pending = incoming;
131+
incoming.title = "Loading SQLPage component demo";
132+
incoming.setAttribute("aria-hidden", "true");
133+
incoming.inert = true;
134+
incoming.loading = "eager";
135+
const loaded = loadPreview(incoming, controller.signal);
136+
incoming.src = `/landing-demos/demo.sql?component=${next}`;
137+
viewport.append(incoming);
138+
panel.setAttribute("aria-busy", "true");
139+
status.textContent = "";
140+
try {
141+
const [files] = await Promise.all([
142+
next === "catalog"
143+
? null
144+
: fetch(`/landing-demos/source.sql?component=${next}`, {
145+
signal: controller.signal,
146+
}).then((response) => {
147+
if (!response.ok) throw new Error("Source unavailable");
148+
return response.json();
149+
}),
150+
loaded,
151+
]);
152+
if (controller.signal.aborted) return;
153+
if (!media.matches) {
154+
fade = panel.animate([{ opacity: 1 }, { opacity: 0 }], {
155+
duration: 100,
156+
fill: "forwards",
157+
});
158+
await fade.finished;
159+
}
160+
if (controller.signal.aborted) return;
161+
resizePreview?.disconnect();
162+
preview.removeEventListener("load", fitPreview);
163+
const outgoing = preview;
164+
resizePanel(() => {
165+
outgoing.remove();
166+
preview = incoming;
167+
pending = null;
168+
component = next;
169+
incoming.title = "Live SQLPage component demo";
170+
incoming.removeAttribute("aria-hidden");
171+
incoming.inert = false;
172+
viewport.style.height = previewHeight(
173+
incoming.contentDocument.querySelector("#sqlpage_main_wrapper"),
174+
);
175+
panel.dataset.demo = next;
176+
panel.setAttribute("aria-labelledby", button.id);
177+
root.querySelector('[data-file="save"]').hidden = next !== "form";
178+
root.querySelector("#demo-filename").textContent = `${next}.sql`;
179+
const link = panel.querySelector(".demo-docs");
180+
link.href = `/component.sql?component=${next}`;
181+
link.firstChild.textContent = `Explore the ${next} component `;
182+
if (files) {
183+
sourceFiles = { main: files.source, save: files.save_source };
184+
// Avoid an intermediate one-line loading state or a nested resize.
185+
const main = root.querySelector("#demo-filename");
186+
selectTab(root.querySelector(".source-tabs"), main);
187+
source.setAttribute("aria-labelledby", main.id);
188+
void renderSource(sourceFiles.main);
189+
}
190+
}, true);
191+
preview.addEventListener("load", fitPreview, { signal });
192+
fitPreview();
193+
panel.removeAttribute("aria-busy");
194+
} catch {
195+
if (controller.signal.aborted) return;
196+
controller.abort();
197+
incoming.remove();
198+
fade?.cancel();
199+
panel.removeAttribute("aria-busy");
200+
selectTab(picker, picker.querySelector(`[data-demo="${component}"]`));
201+
status.textContent = "The demo could not load. Please try again.";
202+
}
203+
}
204+
initTabs(
205+
picker,
206+
"aria-selected",
207+
(button) => void selectDemo(button),
208+
signal,
209+
);
210+
media.addEventListener(
211+
"change",
212+
() => {
213+
if (media.matches) animation?.cancel();
214+
},
215+
{ signal },
216+
);
217+
return () => {
218+
request?.abort();
219+
pending?.remove();
220+
resizePreview?.disconnect();
221+
animation?.cancel();
222+
fade?.cancel();
223+
};
224+
}

0 commit comments

Comments
 (0)