Repository navigation
Poc in two weeks - #760
Poc in two weeks#760smykytenk0 wants to merge 13 commits into
Conversation
Resolve conflicts with the StarlightPage-based BaseLayout: port the hideHeader/hideFooter props to Astro.locals flags honoured by the Starlight Header/Footer overrides, and keep ImageComparison's width/height/class props and overlay slot alongside main's image descriptions and print captions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vvlladd28
left a comment
There was a problem hiding this comment.
Review summary
Reviewed 7 changed source files in Poc in two weeks (plus a pass over the 29 new assets). Left 18 comments inline.
Where to look first:
- Hidden header and footer (
index.astro:283): the page is indexable but has no logo, no way back to the site and no legal footer. Proposed fix: aBaseLayout chrome="minimal"prop (small header + legal-only footer).LegalLayoutisn't an option, because it drops GTM and cookie consent. - Duplicated sources of truth: the client logos, case-study stats, testimonials, the Formspree endpoint and the whole contact modal are copies of things the repo already has, so each copy can drift from its original.
- Assets that should be code:
head-of-delivery-quote.pngis unused, the 7 glow SVGs are just radial gradients, and the 7 single-color icons duplicate Tabler icons the site already uses throughastro-icon. - Copy: several grammar and consistency fixes, listed inline.
Not reviewed line by line: 29 image assets (SVG/PNG). They were checked for use and for duplicates of existing assets.
Suggested patch for ContactModal.astro
This patch fixes the consent gap (utm_*/fpr only with Marketing consent, client_id only with Analytics, via bindTrackingFields), uses FORMSPREE_CONTACT, portals the modal to <body> so the chat launcher no longer paints over it, and replaces the copied validation, honeypot and timing code with shared helpers.
Dependency: the patch imports @util/form-guard. That module extracts the validation, XSS, honeypot and timing logic that is currently copy-pasted in ContactForm, dev-services and this modal. It is being prepared as a separate PR from main and is not pushed yet, so this PR would rebase onto it once it merges. git apply --check passes against this PR's head.
contact-modal-form-guard.patch
--- a/src/components/Services/ContactModal.astro
+++ b/src/components/Services/ContactModal.astro
@@ -1,4 +1,6 @@
---
+import { FORMSPREE_CONTACT } from '@data/formspree';
+
interface Props {
formId: string;
tags: string;
@@ -20,7 +22,7 @@
</div>
<form
method="post"
- action="https://formspree.io/f/xrbnqnna"
+ action={FORMSPREE_CONTACT}
class="gtm_form contactModalForm"
novalidate
>
@@ -73,6 +75,8 @@
<input type="hidden" name="utm_campaign" />
<input type="hidden" name="utm_term" />
<input type="hidden" name="utm_content" />
+ <input type="hidden" name="client_id" />
+ <input type="hidden" name="fpr" />
<input type="hidden" name="tags" value={tags} />
<input type="hidden" name="form_id" value={formId} />
<input type="hidden" name="path" />
@@ -90,13 +94,31 @@
</div>
<script>
+ import {
+ armHoneypot,
+ bindTrackingFields,
+ disarmHoneypot,
+ isLikelyBot,
+ showFakeThanks,
+ stampRenderedAt,
+ validateFields,
+ } from '@util/form-guard';
+ import { portalToBody } from '@util/portal-to-body';
+
+ const FIELDS = [
+ { name: 'name', label: 'Name', type: 'text' },
+ { name: 'email', label: 'Email Address', type: 'email' },
+ { name: 'message', label: 'Message', type: 'textarea' },
+ ] as const;
+
const modal = document.getElementById('contactModal') as HTMLElement | null;
const form = modal?.querySelector<HTMLFormElement>('.contactModalForm') ?? null;
function openModal() {
if (!modal) return;
- const timeInput = modal.querySelector<HTMLInputElement>('.form-rendered-at');
- if (timeInput) timeInput.value = String(Math.floor(Date.now() / 1000));
+ if (form) stampRenderedAt(form);
+ // Out of Starlight's isolated .main-pane, or the chat launcher paints over the backdrop.
+ portalToBody(modal);
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
modal.querySelector<HTMLInputElement>('input[name="name"]')?.focus();
@@ -122,64 +144,13 @@
if (e.key === 'Escape' && modal?.style.display === 'flex') closeModal();
});
- function validate(form: HTMLFormElement): boolean {
- const fields = [
- { name: 'name', label: 'Name', type: 'text' },
- { name: 'email', label: 'Email Address', type: 'email' },
- { name: 'message', label: 'Message', type: 'textarea' },
- ];
- const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$/;
- // Reject common XSS payloads client-side so users get an inline
- // error instead of Cloudflare's "you have been blocked" page.
- const xssPattern =
- /<\s*\/?\s*(?:script|iframe|object|embed)\b|javascript\s*:|on(?:click|error|load|focus|blur|mouseover|mouseout|change|submit|input|toggle|animationend)\s*=/i;
-
- let isValid = true;
- fields.forEach(({ name, label, type }) => {
- const el = form.elements.namedItem(name) as HTMLInputElement | null;
- if (!el) return;
- el.classList.remove('invalid-input');
- el.setAttribute('aria-invalid', 'false');
- const oldHint = el.closest('label')?.nextElementSibling;
- if (oldHint?.classList.contains('error-message-hint')) oldHint.remove();
-
- const value = el.value.trim();
- let msg = '';
- if (!value) msg = `${label} is required.`;
- else if (type === 'email' && !emailRegex.test(value)) msg = `Please enter a valid ${label.toLowerCase()}.`;
- else if (xssPattern.test(value)) msg = `Please remove HTML or script tags from ${label.toLowerCase()}.`;
- if (msg) {
- isValid = false;
- el.classList.add('invalid-input');
- el.setAttribute('aria-invalid', 'true');
- const hint = document.createElement('p');
- hint.className = 'error-message-hint';
- hint.textContent = msg;
- el.closest('label')?.insertAdjacentElement('afterend', hint);
- }
- });
- if (!isValid) form.querySelector<HTMLElement>('.invalid-input')?.focus();
- return isValid;
- }
-
if (form) {
- const params = new URLSearchParams(window.location.search);
- ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'].forEach((key) => {
- const el = form.querySelector<HTMLInputElement>(`[name="${key}"]`);
- if (el) el.value = params.get(key) || '';
- });
+ // utm_* and fpr only with Marketing consent, client_id only with Analytics consent.
+ bindTrackingFields(form);
const pathEl = form.querySelector<HTMLInputElement>('[name="path"]');
if (pathEl) pathEl.value = window.location.pathname;
- // Randomize honeypot name so bots can't learn it
- const hpInput = form.querySelector<HTMLInputElement>('.cus-hp-container input');
- const hpLabel = form.querySelector<HTMLLabelElement>('.cus-hp-container label');
- if (hpInput && hpLabel) {
- const newName = 'hp_' + Math.random().toString(36).substring(2, 10);
- hpInput.name = newName;
- hpInput.id = newName;
- hpLabel.htmlFor = newName;
- }
+ armHoneypot(form, '.cus-hp-container');
// input--empty class drives the floating labels
form.querySelectorAll<HTMLInputElement | HTMLTextAreaElement>('.form-control').forEach((el) => {
@@ -187,22 +158,18 @@
});
form.addEventListener('submit', (e) => {
- if (!validate(form)) {
+ if (!validateFields(form, FIELDS, { announce: false })) {
e.preventDefault();
return;
}
// Honeypot / timing check — bot signals. Don't post to Formspree,
// just show the "Thank you" placeholder text.
- const timeInput = form.querySelector<HTMLInputElement>('.form-rendered-at');
- if (hpInput && timeInput) {
- const tooFast = Math.floor(Date.now() / 1000) - parseInt(timeInput.value, 10) < 3;
- if (hpInput.value.trim() !== '' || tooFast) {
- e.preventDefault();
- form.innerHTML = '<p>Thank you! Your message has been received.</p>';
- return;
- }
- hpInput.removeAttribute('name');
+ if (isLikelyBot(form)) {
+ e.preventDefault();
+ showFakeThanks(form);
+ return;
}
+ disarmHoneypot(form);
});
}
</script>Additional findings
- src/assets/images/proof-of-concept/head-of-delivery-quote.png — nothing imports this file (
grep -r head-of-delivery-quote srcreturns nothing). It is a design export of the speech bubble thatindex.astro:291-306already renders in HTML. Please delete it. - src/assets/images/proof-of-concept/nizar-ali.png — this is a 52×52 PNG shown at 44px, so it looks blurry on 2× screens. The site already has a team photo of the same person at
/images/company/thingsboard-team-photos/solutions-team/nizar-ali.webp(src/data/company/team.ts:75). Please reuse that one instead. - src/assets/images/proof-of-concept/testimonials/vodafone.svg — a second copy of
public/images/customers/vodafone.svg.testimonials/awake.pngis a raster copy of a logo that already exists as SVG atpublic/images/case-studies/awake.svg. - src/pages/services/development-services/index.astro:2226 — this page still contains its own copy of the modal that
ContactModal.astrowas extracted from. Please switch it to the new component, or there will be two copies to keep in sync. - src/components/Services/ContactModal.astro:28 — trailing whitespace (
git diff --check).
This review was auto-generated. Findings may contain errors — please verify before applying changes.
vvlladd28
left a comment
There was a problem hiding this comment.
Re-review summary
Re-reviewed Poc in two weeks and checked the 18 inline findings from the previous review, plus the notes in its body.
| Status | Count |
|---|---|
| ✅ Resolved | 17 |
| 💬 Acknowledged | 0 |
| ❌ Unresolved | 1 inline, 1 from the review body |
The page was largely rewritten, and most of the rewrite improves it. The case-study cards and testimonials now come from @data and fail the build if they drift. The glows are CSS gradients, the icons come from Tabler, the duplicated logo and testimonial assets are gone, and the page uses the shared native-dialog ContactModal from #773.
The main requirement is still missing, and 4 comments are inline:
- Compact header and footer, no promo banner (
index.astro:254): this is a landing page for social traffic, and the page needs a compact header (logo and theme toggle only), the compact footer from docs pages and no promo banner above the header. Instead, the hide mechanism was removed fromBaseLayout,HeaderandFooter, so the page now renders the full site header with the mega menu, the promo banner and the full footer. The page still passeshideHeader hideFooter, whichUseCaseLayoutdoesn't declare, soastro checkshould fail too. nizar-ali.pngis still the blurry 52×52 export (a note from the previous review body).- Leftovers: the three audience icons are still committed as SVG files (they are Material Symbols glyphs and should come from an icon set), and the price and stat sizes use literal
pxvalues.
Finding details
- ❌ src/pages/proof-of-concept/index.astro:254 — the social landing page needs a compact header and footer, with no promo banner — Not delivered: the hide mechanism was removed instead, so the page renders the full site chrome. The
hideHeader hideFooterprops are left behind and go nowhere. - ✅ src/components/starlight/Header.astro — inline
!importantoverrides that depend on Starlight's DOM — Removed. The compact header that should replace them is tracked under the item above. - ✅ src/pages/proof-of-concept/index.astro —
../../and~/imports — Fixed: switched to@aliases. - ✅ src/pages/proof-of-concept/index.astro — hero and audience glow SVGs — Fixed: replaced by
radial-gradient()on theme tokens. - ✅ src/pages/proof-of-concept/index.astro —
start-glow-*.svg— Fixed: replaced by a CSS gradient for each card. - ✅ src/pages/proof-of-concept/index.astro — single-color icons as
<img>— Fixed: replaced by TablerIcons (see the new comment about the three audience icons that are left). - ✅ src/pages/proof-of-concept/index.astro — duplicated client logos — Fixed: now uses
public/images/development-services/*. - ✅ src/pages/proof-of-concept/index.astro — case-study stats and testimonials copied by hand — Fixed: derived from
caseStudyBySlug/devServicesFeedback, with build-time guards. - ✅ src/components/Services/ContactModal.astro — hardcoded Formspree URL — Fixed: the file was removed and the page uses the shared
ContactModal. - ✅ src/components/Services/ContactModal.astro — single instance, no focus trap, hardcoded colors, stacking bug — Fixed: replaced by the native-dialog
Modal. - ✅ src/components/Services/ContactModal.astro —
animatetopanimatingtop— Fixed: the file was removed. - ✅ src/pages/proof-of-concept/index.astro — lead caption dash, title and unused bubble PNG — Fixed.
- ✅ src/pages/proof-of-concept/index.astro — note and hero quote repeated — Fixed: the intros were rewritten.
- ✅ src/pages/proof-of-concept/index.astro — ungrammatical deliverable title — Fixed.
- ✅ src/pages/proof-of-concept/index.astro — "Logistic & supply-chain" — Fixed: the tag now comes from the case-study data.
- ✅ src/pages/proof-of-concept/index.astro — dashes,
8×,$7,000, periods, US English, article, "200+", meta description length — Fixed (metaDescriptionwas added). - ✅ src/pages/proof-of-concept/index.astro — timeline column header "Days" — Fixed: now "Task".
- ✅ src/components/Services/ContactModal.astro — three hand-rolled modals — Fixed via #773.
- ❌ src/assets/images/proof-of-concept/nizar-ali.png (previous review body) — 52×52 PNG that looks blurry on 2× screens, while a team photo already exists — Still present. See the inline comment at line 270.
Additional findings
- src/util/ogContext.ts:74 —
/proof-of-concept/isn't inMARKETING_ALLOWLIST, sogetOgImageUrlreturnsundefinedand the page falls back to the generic/thingsboard-og.png. This page is built for ad and social traffic, so it is the one most likely to be shared as a link. Adding'/proof-of-concept/'next to'/services/*'would give it its own OG card (CLAUDE.md → OG image generation).
Not reviewed line by line: 5 image assets (3 audience SVG icons, nizar-ali.png, poc-dashboard.png). They were checked for use and hardcoded colors.
This re-review was auto-generated. Findings may contain errors — please verify before applying changes.
vvlladd28
left a comment
There was a problem hiding this comment.
Re-review summary
Re-reviewed Poc in two weeks. I checked the 4 inline findings and the notes in the body of the previous review, then went over the whole page again for markup and style quality, measuring it in the browser at 1920px. The mobile layout was checked from the code only.
| Status | Count |
|---|---|
| ✅ Resolved | 4 |
| 💬 Acknowledged | 0 |
| ❌ Unresolved | 0 inline, 1 from the review body |
The minimal chrome is implemented the way we agreed:
BaseLayout chrome="minimal"is passed throughUseCaseLayout.PromoBanner hiddentakes the existing no-slide path, so the banner is gone and its offset is zeroed.HeaderContentrenders only the logo and the theme toggle (2 KB of header HTML instead of ~42 KB).- The docs footer is extracted into
Landing/CompactFooter, whichDocFooterand the minimal branch both use. - The swipe-to-open guard for the missing burger is a nice catch.
I left 15 comments inline. Where to look first:
- The 80px empty band between the workshop block and the footer (
Footer.astro:20). - The case cards (
index.astro:652): they copy the development-services cards but load the four photos as eager inlinebackground-images, which the original deliberately avoids. - Breakpoints and tokens: the page uses its own 1079/959/599px breakpoints and literal radii, sizes and colors where tokens exist.
- Mobile title overrides that leave the site's typography scale.
Finding details
- ✅ src/pages/proof-of-concept/index.astro:251 — compact header and footer, no promo banner — Fixed:
chrome="minimal", and the deadhideHeader hideFooterprops are gone. - ✅ src/pages/proof-of-concept/index.astro:267 — blurry
nizar-ali.png— Fixed: both avatars use the team.webp, and the PNG is deleted. - ✅ src/pages/proof-of-concept/index.astro:339 — audience icons as hardcoded-color SVG files — Fixed:
tabler:tool,tabler:user-cogandtabler:topology-star-3in accent color, and the three SVGs are deleted. - ✅ src/pages/proof-of-concept/index.astro:1700 — literal
pxfont sizes — Fixed: they use$font-size-*tokens now (see the inline note about the rendered sizes). - ❌ src/util/ogContext.ts:74 (previous review body) —
/proof-of-concept/is still missing fromMARKETING_ALLOWLIST, so the page that is built to be shared on social networks still gets the generic/thingsboard-og.pngcard. Add'/proof-of-concept/'next to'/services/*'.
Sizes and spacing to confirm with design
These values were measured on the rendered page. Each one breaks a pattern the rest of the page follows, so it may be a slip in the mockup rather than a deliberate choice. They are listed here rather than inline because only design can say which ones are intended.
| Where | Values on the page | Why it looks off |
|---|---|---|
| Space above a section | 120 (audience, journey), 80 (every other section), 56 (testimonials, cases) | Only the first two sections get 120. 56 is off the spacing scale, although it may be deliberate if testimonials and cases are sub-blocks of "The scope comes from…" |
| Section title to its content | 48, 32, 20 (+ intro), 64 ("The scope comes from work already delivered") | 64 appears in this one section only |
| Card padding | audience 20, deliverable / testimonial / note 24, fit 32, start 40 / 24 / 36 | 20 and 36 stand out, and 36 at the bottom reads like a typo for 40 or 32 |
| Space under the testimonial quote | 44px | Off the scale; probably 40 or 48 |
| Grid gap | 24 everywhere, 32 in the case grid | |
| Corner radius | 12 (cards), 16 (price, CTA banner), 24 (case cards) | Three radii on one page, and 12 isn't in the radius scale (8 / 16) |
| Avatar size | 44px in the hero, 52px in testimonials and "Talk to" | |
| Mobile titles | H2 24px, H1 28px | The site's section-title is 32px on mobile and page-title is 30px |
Not reviewed line by line: poc-dashboard.png (an image asset).
This re-review was auto-generated. Findings may contain errors — please verify before applying changes.
vvlladd28
left a comment
There was a problem hiding this comment.
Re-review summary
Re-reviewed Poc in two weeks and checked the 15 inline findings and the notes in the body of the previous review.
| Status | Count |
|---|---|
| ✅ Resolved | 13 |
| 💬 Acknowledged | 2 |
| ❌ Unresolved | 0 |
All previous findings have been addressed, and the OG card for /proof-of-concept/ is now generated. The shared CaseStudyCard + @data/case-study-cards refactor removes about 400 lines of duplicated markup and CSS from development-services, and ImageComparison stackOnMobile replaces the mobile copy of the brief. Both are good moves.
I found 5 new issues in the fix commit, left inline. Three are in the new shared card or its data, one is in ImageComparison and one is on the page:
- Keyboard focus on
CaseStudyCardis likely invisible. - A copy regression: the Senseing tag is "Logistic & supply-chain" again.
- Hardcoded typography and colors in the new shared component.
- The drag script still runs in stacked mode, hidden only by
clip-path: none !important. - The pale-blue tint is written out five times on the page.
Finding details
- ✅ src/components/starlight/Footer.astro — 80px empty band above the compact footer — Fixed:
data-chrome="minimal"drops.use-case-page's bottom padding. - ✅ src/pages/proof-of-concept/index.astro — case cards copied from development-services with eager
background-image— Fixed: both pages render the newCaseStudyCard, with a lazy<img>. - ✅ src/pages/proof-of-concept/index.astro —
.poc-compare__brief/.poc-briefdouble wrapper — Fixed. - ✅ src/pages/proof-of-concept/index.astro — mobile duplicate of the brief and dashboard — Fixed:
ImageComparison stackOnMobile, checked on the dev server at 375, 900 and 1920px, and the mobile layout matches the old section. See the new note on the drag script. - ✅ src/pages/proof-of-concept/index.astro — two
<ul>s from an array of arrays — Fixed: one list in a column-flow grid. - ✅ src/pages/proof-of-concept/index.astro —
.poc-hero__mediawrapper only for the glow — Fixed: the glow moved to.poc-lead::before. - ✅ src/pages/proof-of-concept/index.astro — page-only 1079/959/599px breakpoints — Fixed:
media-down(lg)on the page. - ✅ src/pages/proof-of-concept/index.astro —
calc(-1 * $spacing-5 - 1px)three times — Fixed: onepoc-bleedmixin with the reason in one comment. - ✅ src/pages/proof-of-concept/index.astro — literal radii and spacing — Fixed: new
$radius-lg-plusand$spacing-14tokens, 44→40, 32→24. - ✅ src/pages/proof-of-concept/index.astro — hardcoded
#b3c7ff— Fixed: uses$color-brand-dark. - ✅ src/pages/proof-of-concept/index.astro — duplicated
.poc-checklist__iconrule — Fixed. - ✅ src/pages/proof-of-concept/index.astro — mobile title overrides off the typography scale — Fixed: removed, so the titles follow the mixins.
- 💬 src/pages/proof-of-concept/index.astro —
.poc-ctaoverridingNButton— Developer: "According to the design choice, I've removed all rules except font-weight." - ✅ src/pages/proof-of-concept/index.astro — hover lift on non-clickable cards — Fixed: removed.
- 💬 src/pages/proof-of-concept/index.astro — price and stat sizes moved off the mockup — Developer: "Yes, I checked this with the designer."
- ✅ src/util/ogContext.ts (previous review body) —
/proof-of-concept/missing fromMARKETING_ALLOWLIST— Fixed, plus an eyebrow and title inmarketing-meta.ts.
Not reviewed line by line: poc-dashboard.png (an image asset).
This re-review was auto-generated. Findings may contain errors — please verify before applying changes.
vvlladd28
left a comment
There was a problem hiding this comment.
Re-review summary
Re-reviewed Poc in two weeks and checked the 5 inline findings from the previous review.
| Status | Count |
|---|---|
| ✅ Resolved | 5 |
| 💬 Acknowledged | 0 |
| ❌ Unresolved | 0 |
All previous findings have been addressed. Checked on the dev server: the case-card focus ring, the long labels on focus, "Logistics & supply chain", and $poc-tint-bg, which renders the same color as the old gradient. I found 1 new issue in the fix commit, left inline: dropping !important from the stacked clip-path lets a clip left over from an earlier drag hide the brief text after the viewport narrows, e.g. when a phone is rotated from landscape to portrait.
Finding details
- ✅ src/components/CaseStudy/CaseStudyCard.astro — invisible keyboard focus, hover-only label swap and zoom — Fixed:
:focus-withinnext to:hover, an inset:focus-visibleoutline and aprefers-reduced-motionguard. - ✅ src/data/case-study-cards.ts — "Logistic & supply-chain" — Fixed on both pages: "Logistics & supply chain".
- ✅ src/components/CaseStudy/CaseStudyCard.astro — hardcoded typography and colors — Fixed:
text-overlay,text-s, spacing and radius tokens,$color-white,$color-text-primary. The stat figures (14px / 24px,24px / 500 / 36px) and the hoverpadding-bottom: 45pxare still literals. That's optional, since no mixin matches the 24/36 figure exactly. - ✅ src/components/UseCase/ImageComparison.astro — drag script still running in stacked mode — Fixed:
onPointerDownreturns early when the block is stacked (see the new inline note about the!importantthat went with it). - ✅ src/pages/proof-of-concept/index.astro — pale-blue tint written out five times — Fixed: one
$poc-tint-bgviacolor-mix(), and--poc-timeline-bgis gone.
Not reviewed line by line: poc-dashboard.png (an image asset).
This re-review was auto-generated. Findings may contain errors — please verify before applying changes.
Description
Type of change
src/content/docs/**)src/content/_includes/**)src/components/**,src/styles/**)src/pages/**,src/data/**)src/data/redirects.ts)releaseskill)Affected products
Related issues
Checklist
pnpm checkpasses (Astro / TypeScript)pnpm lint:eslintpassespnpm lint:slugcheckpasses (required if pages were added/renamed/moved across languages)pnpm lint:linkcheckpasses locally — required to merge; run it before requesting review (usepnpm lint:linkcheck:nobuildif you already ran a build)src/data/redirects.ts, andpnpm generate:redirectswas runsrc/data/versions.ts