From 78880ed6baee641a233c8b34a2ad8a854f01d2bd Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sun, 23 Aug 2026 05:00:16 -0700 Subject: [PATCH 01/18] Email updates (#365) * so far * car newsletter * comp * fix wrap * use grid * generic * on op page * on op page * footer * add * dedup * tweaks * tweaks * tweaks * clean up * dedup crappy codex * Revert "dedup crappy codex" This reverts commit e13b2f037752ff3dbc7e05fcc24d0666dad0b081. * need to supervise >:( * one place * big dedup * simplif * dir * fix all the weird sizes * fix submit * minor * switch * fix * better * clean up * dont need * more concrete connect copy * back * clean up * generic * more simple!!!!!! * more simple!!!!!! * codex is terrible * simplify and limit to 2.75rem for only vehicles * more :( * remove more * remove more * bett er- * stupid ios * not this * fix small widths * cleanup * cleanup * margin * def * simplify logic * rm --- .../EmailUpdates/EmailUpdatesForm.svelte | 286 ++++++++++++++++++ .../FooterEmailUpdatesForm.svelte | 234 ++++++++++++++ .../EmailUpdates/InterestCheckboxes.svelte | 65 ++++ .../HarnessSelector/HarnessSelector.svelte | 2 +- src/lib/components/MailingListForm.svelte | 144 --------- src/lib/email-updates.js | 108 +++++++ src/routes/+layout.svelte | 20 +- src/routes/connect/+page.svelte | 10 + src/routes/openpilot/+page.svelte | 9 + src/routes/shop/body/+page.svelte | 32 +- src/routes/vehicles/+page.svelte | 115 ++++--- 11 files changed, 776 insertions(+), 249 deletions(-) create mode 100644 src/lib/components/EmailUpdates/EmailUpdatesForm.svelte create mode 100644 src/lib/components/EmailUpdates/FooterEmailUpdatesForm.svelte create mode 100644 src/lib/components/EmailUpdates/InterestCheckboxes.svelte delete mode 100644 src/lib/components/MailingListForm.svelte create mode 100644 src/lib/email-updates.js diff --git a/src/lib/components/EmailUpdates/EmailUpdatesForm.svelte b/src/lib/components/EmailUpdates/EmailUpdatesForm.svelte new file mode 100644 index 00000000..06513c2f --- /dev/null +++ b/src/lib/components/EmailUpdates/EmailUpdatesForm.svelte @@ -0,0 +1,286 @@ + + + + + diff --git a/src/lib/components/EmailUpdates/FooterEmailUpdatesForm.svelte b/src/lib/components/EmailUpdates/FooterEmailUpdatesForm.svelte new file mode 100644 index 00000000..b781edd1 --- /dev/null +++ b/src/lib/components/EmailUpdates/FooterEmailUpdatesForm.svelte @@ -0,0 +1,234 @@ + + + { + if (showOptions && componentElement && !event.composedPath().includes(componentElement)) showOptions = false; + }} + on:keydown={(event) => { + if (event.key === 'Escape') showOptions = false; + }} +/> + + + + diff --git a/src/lib/components/EmailUpdates/InterestCheckboxes.svelte b/src/lib/components/EmailUpdates/InterestCheckboxes.svelte new file mode 100644 index 00000000..0c7ed7ed --- /dev/null +++ b/src/lib/components/EmailUpdates/InterestCheckboxes.svelte @@ -0,0 +1,65 @@ + + + + +{#each interests as { key, label }} + +{/each} + + diff --git a/src/lib/components/HarnessSelector/HarnessSelector.svelte b/src/lib/components/HarnessSelector/HarnessSelector.svelte index 4c679cdd..060a42d5 100644 --- a/src/lib/components/HarnessSelector/HarnessSelector.svelte +++ b/src/lib/components/HarnessSelector/HarnessSelector.svelte @@ -40,7 +40,7 @@ onChange(selection); updateQueryParams(selection); - // remember with cookie + // Remember the selection in local storage if (selection?.car) { selectedCar.set(selection.car); } else { diff --git a/src/lib/components/MailingListForm.svelte b/src/lib/components/MailingListForm.svelte deleted file mode 100644 index f5a90b1f..00000000 --- a/src/lib/components/MailingListForm.svelte +++ /dev/null @@ -1,144 +0,0 @@ - - -{#if !completed} -
- - - -
-{:else} - {#await submissionPromise then response} -
-

Thanks for signing up! We only send emails we would want to receive.

-
- {:catch error} -
-

Oops! Something went wrong while submitting the form.

-

{@html error.message}

-
- {/await} -{/if} - diff --git a/src/lib/email-updates.js b/src/lib/email-updates.js new file mode 100644 index 00000000..ecf6877a --- /dev/null +++ b/src/lib/email-updates.js @@ -0,0 +1,108 @@ +import { get, writable } from 'svelte/store'; + +import { selectedCar } from '../store.js'; + +export const EMAIL_INTERESTS = [ + { key: 'general', label: 'General updates', description: 'New products, sales, and more', fieldName: 'group[54660][1]' }, + { key: 'releases', label: 'New openpilot releases', description: 'Major changes and improvements', fieldName: 'group[54660][4]' }, + { key: 'compatibility', label: 'Car compatibility updates', description: 'Newly supported cars', fieldName: 'group[54660][2]' }, + { key: 'blog', label: 'New blog posts', description: 'New posts on the comma blog', fieldName: 'group[54660][8]' }, +]; + +const INTEREST_KEYS = EMAIL_INTERESTS.map(({ key }) => key); + +// No category means every interest +function createEmailInterestSelection(defaultCategory) { + return Object.fromEntries( + INTEREST_KEYS.map((key) => [key, !defaultCategory || key === defaultCategory]), + ); +} + +export function anySelected(selectedInterests) { + return INTEREST_KEYS.some((key) => selectedInterests[key]); +} + +export function allSelected(selectedInterests) { + return INTEREST_KEYS.every((key) => selectedInterests[key]); +} + +function cleanMailchimpMessage(message) { + const element = document.createElement('div'); + element.innerHTML = message ?? ''; + return element.textContent || 'Please try again.'; +} + +function submitEmailUpdates(email, selectedInterests) { + return new Promise((resolve, reject) => { + const callbackName = `mailchimpEmailUpdates_${Math.random().toString(36).slice(2, 11)}`; + const script = document.createElement('script'); + const params = new URLSearchParams({ + u: 'e127cf7151180db2b566d880b', + id: 'f150bd2a9c', + EMAIL: email, + SOURCE: window.location.pathname, + c: callbackName, + }); + + const car = get(selectedCar); + if (car) params.set('SELECTCAR', car); + for (const { key, fieldName } of EMAIL_INTERESTS) { + if (selectedInterests[key]) params.set(fieldName, '1'); + } + + function cleanUp() { + script.remove(); + delete window[callbackName]; + } + + window[callbackName] = function(response) { + const alreadySubscribed = /already subscribed/i.test(response.msg || ''); + cleanUp(); + + if (response.result === 'success' || alreadySubscribed) { + resolve(response); + } else { + reject(new Error(cleanMailchimpMessage(response.msg))); + } + }; + + script.onerror = function() { + cleanUp(); + reject(new Error('Could not connect. Please try again.')); + }; + + script.src = `https://comma.us12.list-manage.com/subscribe/post?${params}`; + document.body.appendChild(script); + }); +} + +export function createEmailUpdatesForm(defaultCategory) { + const email = writable(''); + const interests = writable(createEmailInterestSelection(defaultCategory)); + const status = writable('idle'); // idle | submitting | success | error + const errorMessage = writable(''); + + function toggle(interest, checked) { + interests.update((current) => ({ ...current, [interest]: checked })); + } + + async function submit() { + if (!anySelected(get(interests))) { + errorMessage.set('Choose at least one type of update.'); + status.set('error'); + return; + } + + status.set('submitting'); + + try { + await submitEmailUpdates(get(email), get(interests)); + status.set('success'); + } catch (error) { + errorMessage.set(error.message); + status.set('error'); + } + } + + return { email, interests, status, errorMessage, toggle, submit }; +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 2d88b568..ea4d47e4 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -12,7 +12,7 @@ import Badge from "$lib/components/Badge.svelte"; import Grid from "$lib/components/Grid.svelte"; import SocialIcons from "$lib/components/SocialIcons.svelte"; - import MailingListForm from "$lib/components/MailingListForm.svelte"; + import FooterEmailUpdatesForm from "$lib/components/EmailUpdates/FooterEmailUpdatesForm.svelte"; import CommaIcon from "$lib/icons/comma.svg?raw"; import CartIcon from "$lib/icons/ui/cart.svg?raw"; @@ -63,20 +63,6 @@ showCart.set(false); } }); - - // Focus mailing list input when hash is #mailing-list - const focusMailingList = () => { - if (window.location.hash === '#mailing-list') { - setTimeout(() => { - const input = document.querySelector('#mailing-list input[type="email"]'); - if (input) input.focus(); - }, 300); - } - }; - - // Check on load and on hash change - focusMailingList(); - window.addEventListener('hashchange', focusMailingList); }); printConsoleBanner(); @@ -172,8 +158,8 @@ diff --git a/src/routes/shop/body/+page.svelte b/src/routes/shop/body/+page.svelte index c4458b48..c8a84e86 100644 --- a/src/routes/shop/body/+page.svelte +++ b/src/routes/shop/body/+page.svelte @@ -3,7 +3,7 @@ import Grid from "$lib/components/Grid.svelte"; import Accordion from "$lib/components/Accordion.svelte"; import Badge from "$lib/components/Badge.svelte"; - import MailingListForm from "$lib/components/MailingListForm.svelte"; + import EmailUpdatesForm from "$lib/components/EmailUpdates/EmailUpdatesForm.svelte"; import NoteCard from "$lib/components/NoteCard.svelte"; import CommunityBanner from "$lib/components/CommunityBanner.svelte"; @@ -177,13 +177,12 @@ -
- Follow along -

Get body updates

-
- -
-
+ +

Follow along with comma body.

+
@@ -426,23 +425,6 @@ } } } - - & .mailing-list { - display: flex; - flex-direction: column; - align-items: center; - - & .form { - width: 45ch; - color: black; - } - - @media screen and (max-width: 480px) { - & .form { - width: 100%; - } - } - } } #description { diff --git a/src/routes/vehicles/+page.svelte b/src/routes/vehicles/+page.svelte index f8fa8259..fd92d877 100644 --- a/src/routes/vehicles/+page.svelte +++ b/src/routes/vehicles/+page.svelte @@ -7,6 +7,7 @@ import Faq from '$lib/components/Faq.svelte'; import LinkButton from '$lib/components/LinkButton.svelte'; import NoteCard from '$lib/components/NoteCard.svelte'; + import EmailUpdatesForm from '$lib/components/EmailUpdates/EmailUpdatesForm.svelte'; import { faq } from '$lib/constants/faq.svelte'; @@ -51,32 +52,24 @@ {/each} -
- Don't see your car? +

- If you don't see your car, it's not currently supported, however new cars are added with each openpilot release. + New cars are added with each openpilot release. Get an email when compatibility changes.

-

- Join the mailing list to stay updated. -

-

If you have a modern car and some programming skills, you can likely add support for your car. -

-

- Watch - this talk - and check out the - docs + Watch this talk and check out the + docs to learn more.

-
- -

Last updated: {compatibilityMeta.last_updated}

+ @@ -110,6 +103,8 @@
+

Last updated: {compatibilityMeta.last_updated}

+ {#each Object.entries(vehicles) as [make, cars]} {#if cars.length !== 0} {@const brand_img_path = `/src/lib/images/vehicles/brand-icons/Logo-${make}.png`} @@ -248,34 +243,6 @@ line-height: 1.3; } - & .headline { - margin: 3rem auto 0; - - & span { - margin-bottom: 0.5rem; - } - - & p { - margin: 0; - text-wrap: balance; - } - } - - & hgroup { - text-align: center; - font-size: 1.25rem; - - & span { - font-size: 1.5rem; - font-weight: 700; - display: block; - } - - & p { - margin-top: 0.5rem; - } - } - & .compatibility-make-links { display: grid; gap: 1.5rem 1rem; @@ -329,26 +296,50 @@ margin-bottom: 1rem; } + /* TODO: extract shared card class */ .recommended-cars { - width: 85%; - margin: 2rem auto; + margin: 3rem 0; background-color: var(--color-card-background); border: 1px solid rgba(0, 0, 0, .4); - padding: 2rem 1rem; + padding: 3rem; & .recommended-car-columns { margin-top: 3rem; & .recommended-car-stack { - text-align: center; - font-size: 1.25rem; - - & strong { - margin-bottom: .5rem; - font-weight: 700; - } + display: grid; + gap: 0.5em; + font-size: 1.125rem; + line-height: 1.35; } } + + @media screen and (max-width: 768px) { + padding: 2rem 1rem; + } + } + + .recommended-cars hgroup { + & h2 { + margin: 0 0 0.75rem; + font-weight: 600; + line-height: 1.05; + letter-spacing: -0.06em; + } + + & p { + margin: 0; + font-size: 1.125rem; + line-height: 1.35; + } + } + + /* Only the top step; below 1024 the global h2 scale already applies. */ + @media screen and (min-width: 1025px) { + #vehicles :global(.updates-card h2), + .recommended-cars h2 { + font-size: 2.75rem; + } } .car-make-header { From 235c98a2247edaf338982e96281fdd772188a8a4 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sun, 23 Aug 2026 05:27:55 -0700 Subject: [PATCH 02/18] fix footer email cut off on certain widths --- src/lib/components/EmailUpdates/FooterEmailUpdatesForm.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/EmailUpdates/FooterEmailUpdatesForm.svelte b/src/lib/components/EmailUpdates/FooterEmailUpdatesForm.svelte index b781edd1..b522ec1a 100644 --- a/src/lib/components/EmailUpdates/FooterEmailUpdatesForm.svelte +++ b/src/lib/components/EmailUpdates/FooterEmailUpdatesForm.svelte @@ -41,7 +41,7 @@ name="email" type="email" autocomplete="email" - placeholder="Enter your email" + placeholder="Enter email" maxlength="256" required bind:value={$email} From 6ed947fee21f1ed747dcf0c9d300cb9a88445e5f Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 24 Aug 2026 14:35:22 -0700 Subject: [PATCH 03/18] rm selected car --- src/lib/email-updates.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/lib/email-updates.js b/src/lib/email-updates.js index ecf6877a..be0dc3e3 100644 --- a/src/lib/email-updates.js +++ b/src/lib/email-updates.js @@ -1,7 +1,5 @@ import { get, writable } from 'svelte/store'; -import { selectedCar } from '../store.js'; - export const EMAIL_INTERESTS = [ { key: 'general', label: 'General updates', description: 'New products, sales, and more', fieldName: 'group[54660][1]' }, { key: 'releases', label: 'New openpilot releases', description: 'Major changes and improvements', fieldName: 'group[54660][4]' }, @@ -44,8 +42,6 @@ function submitEmailUpdates(email, selectedInterests) { c: callbackName, }); - const car = get(selectedCar); - if (car) params.set('SELECTCAR', car); for (const { key, fieldName } of EMAIL_INTERESTS) { if (selectedInterests[key]) params.set(fieldName, '1'); } From b8732e1406d041939a7d9d7dc7118109ed16294f Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 24 Aug 2026 15:04:53 -0700 Subject: [PATCH 04/18] simpler --- live.sh | 2 +- .../EmailUpdates/EmailUpdatesForm.svelte | 139 ++++++------------ .../EmailUpdates/InterestCheckboxes.svelte | 27 ---- src/lib/email-updates.js | 21 +-- 4 files changed, 53 insertions(+), 136 deletions(-) diff --git a/live.sh b/live.sh index 8dab5bd7..f55b3bed 100755 --- a/live.sh +++ b/live.sh @@ -10,4 +10,4 @@ fi bun upgrade bun install -bun run dev -- --open +bun run dev -- --open --host diff --git a/src/lib/components/EmailUpdates/EmailUpdatesForm.svelte b/src/lib/components/EmailUpdates/EmailUpdatesForm.svelte index 06513c2f..f38a654e 100644 --- a/src/lib/components/EmailUpdates/EmailUpdatesForm.svelte +++ b/src/lib/components/EmailUpdates/EmailUpdatesForm.svelte @@ -1,22 +1,21 @@ @@ -50,37 +49,31 @@ bind:value={$email} > -
-
- +