diff --git a/src/lib/components/EmailUpdates/EmailUpdatesForm.svelte b/src/lib/components/EmailUpdates/EmailUpdatesForm.svelte new file mode 100644 index 00000000..1a59fdc1 --- /dev/null +++ b/src/lib/components/EmailUpdates/EmailUpdatesForm.svelte @@ -0,0 +1,239 @@ + + + + + diff --git a/src/lib/components/EmailUpdates/FooterEmailUpdatesForm.svelte b/src/lib/components/EmailUpdates/FooterEmailUpdatesForm.svelte new file mode 100644 index 00000000..e6929755 --- /dev/null +++ b/src/lib/components/EmailUpdates/FooterEmailUpdatesForm.svelte @@ -0,0 +1,249 @@ + + + { + if (showOptions && componentElement && !event.composedPath().includes(componentElement)) showOptions = false; + }} + on:keydown={(event) => { + if (event.key === 'Escape') showOptions = false; + }} +/> + + + + 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..df0ed7e5 --- /dev/null +++ b/src/lib/email-updates.js @@ -0,0 +1,94 @@ +import { get, writable } from 'svelte/store'; + +export const EMAIL_CATEGORIES = [ + { key: 'product', label: 'Product updates', description: 'New products and sales', 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]' }, +]; + +// Not shown on the site - people who subscribe to every category get added to new ones +const NEW_UPDATES_FIELD = 'group[54660][16]'; + +function cleanMailchimpMessage(message) { + const element = document.createElement('div'); + element.innerHTML = message ?? ''; + // Mailchimp prefixes errors with the field index, e.g. "0 - Enter a real email" + const text = element.textContent.replace(/^\s*\d+\s*-\s*/, '').trim(); + return text || 'Please try again.'; +} + +function submitEmailUpdates(email, selectedCategories, car) { + 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, + }); + + // Send entered car by user + if (car) params.set('VCAR', car); + + for (const { key, fieldName } of EMAIL_CATEGORIES) { + if (selectedCategories.includes(key)) params.set(fieldName, '1'); + } + + if (selectedCategories.length === EMAIL_CATEGORIES.length) params.set(NEW_UPDATES_FIELD, '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() { + const email = writable(''); + const car = writable(''); + const selectedCategories = writable(EMAIL_CATEGORIES.map(({ key }) => key)); + const status = writable('idle'); // idle | submitting | success | error + const errorMessage = writable(''); + + async function submit() { + if (!get(selectedCategories).length) { + errorMessage.set('Choose at least one type of update'); + status.set('error'); + return; + } + + status.set('submitting'); + + try { + await submitEmailUpdates(get(email), get(selectedCategories), get(car).trim()); + status.set('success'); + } catch (error) { + errorMessage.set(error.message); + status.set('error'); + } + } + + return { email, car, selectedCategories, status, errorMessage, submit }; +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 2d88b568..97358878 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..a2bce3f9 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..ba6203d1 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,25 @@ {/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 +104,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 +244,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 +297,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 {