From b4b22aad94ed03e6a4a84c2bc2a17e50ff37a737 Mon Sep 17 00:00:00 2001 From: cromelex <96779452+cromelex@users.noreply.github.com> Date: Tue, 13 May 2025 15:03:40 +0100 Subject: [PATCH 1/8] feat:ReplyByEmail button adds the ability to add a ReplyByEmail button to your notes. Email address and pages to include/exclude can be added via layout file. Base64 encoding is used to *obfuscate* the email address from bots --- quartz/components/ReplyByEmail.tsx | 142 +++++++++++++++++++++++++++++ quartz/components/index.ts | 2 + 2 files changed, 144 insertions(+) create mode 100644 quartz/components/ReplyByEmail.tsx diff --git a/quartz/components/ReplyByEmail.tsx b/quartz/components/ReplyByEmail.tsx new file mode 100644 index 000000000..b8f8450ef --- /dev/null +++ b/quartz/components/ReplyByEmail.tsx @@ -0,0 +1,142 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { classNames } from "../util/lang" + +interface ReplyByEmailOptions { + username?: string + domain?: string + includeTitles?: string[] + excludeTitles?: string[] +} + +// Default options will be used if not provided in the layout file +const defaultOptions: ReplyByEmailOptions = { + username: "ZW1haWw=", // "email" in base64 + domain: "ZXhhbXBsZS5jb20=", // "email.com" in base64 + includeTitles: [], + excludeTitles: ["Home", "About me", "Contact me"] +} + +const ReplyByEmail: QuartzComponent = ({ + fileData, + displayClass, + username, + domain, + includeTitles, + excludeTitles +}: QuartzComponentProps & ReplyByEmailOptions) => { + const title = fileData.frontmatter?.title + + // Use provided values or defaults + const encodedPart1 = username || defaultOptions.username + const encodedPart2 = domain || defaultOptions.domain + const includeList = includeTitles || defaultOptions.includeTitles + const excludeList = excludeTitles || defaultOptions.excludeTitles + + // Display logic: + // 1. If includeTitles is not empty, only show on those pages + // 2. If includeTitles is empty, show on all pages except those in excludeTitles + const shouldDisplay = title && ( + (includeList.length > 0 && includeList.includes(title)) || + (includeList.length === 0 && !excludeList.includes(title)) + ) + + if (shouldDisplay) { + return ( +
+ +
+ ) + } else { + return null + } +} + +ReplyByEmail.css = ` +.center-wrapper { + display: flex; + justify-content: center; + margin-bottom: 2rem; +} + +.reply-by-email-button { + display: inline-block; + padding: 0.5rem 1rem; + background-color: var(--highlight); + color: var(--secondary); + border-radius: 5px; + transition: background-color 0.2s ease, transform 0.2s ease; + cursor: pointer; + border: none; + font-size: 1rem; + font-family: inherit; +} + +.reply-by-email-button:hover { + transform: scale(1.05); +} +` + +// Script that works with SPA navigation +ReplyByEmail.beforeDOMLoaded = ` +// Function to attach email button handlers +function attachEmailHandlers() { + document.querySelectorAll('.reply-by-email-button').forEach(function(button) { + // Remove existing event listeners first to prevent duplicates + button.removeEventListener('click', handleEmailButtonClick); + // Add fresh event listener + button.addEventListener('click', handleEmailButtonClick); + }); +} + +// Handler function for the email button click +function handleEmailButtonClick(e) { + e.preventDefault(); + + // Get data attributes + const username = atob(this.getAttribute('data-username')); + const domain = atob(this.getAttribute('data-domain')); + const title = this.getAttribute('data-title'); + + // Create email address and mailto link + const email = username + '@' + domain; + const mailtoLink = 'mailto:' + email + '?subject=' + title; + + // Open email client + window.location.href = mailtoLink; +} + +// Initial attachment when the page loads +document.addEventListener('DOMContentLoaded', attachEmailHandlers); + +// Re-attach handlers after SPA navigation +document.addEventListener('nav', function() { + // Small delay to ensure the new buttons are in the DOM + setTimeout(attachEmailHandlers, 10); +}); +` + +export default ((opts?: ReplyByEmailOptions) => { + // Component constructor that accepts options + const component: QuartzComponent = (props) => { + return ReplyByEmail({ + ...props, + username: opts?.username, + domain: opts?.domain, + includeTitles: opts?.includeTitles, + excludeTitles: opts?.excludeTitles + }) + } + + // Pass through the CSS and beforeDOMLoaded + component.css = ReplyByEmail.css + component.beforeDOMLoaded = ReplyByEmail.beforeDOMLoaded + + return component +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/index.ts b/quartz/components/index.ts index cece8e614..b5aade6fb 100644 --- a/quartz/components/index.ts +++ b/quartz/components/index.ts @@ -23,6 +23,7 @@ import Breadcrumbs from "./Breadcrumbs" import Comments from "./Comments" import Flex from "./Flex" import ConditionalRender from "./ConditionalRender" +import ReplyByEmail from "./ReplyByEmail" export { ArticleTitle, @@ -50,4 +51,5 @@ export { Comments, Flex, ConditionalRender, + ReplyByEmail, } From e5049e89c6705b76a53fbde3a032bf89952f3722 Mon Sep 17 00:00:00 2001 From: cromelex <96779452+cromelex@users.noreply.github.com> Date: Tue, 13 May 2025 15:29:46 +0100 Subject: [PATCH 2/8] feat:ReplyByEmail button adds a 'buttonLabel' support to allow changing the ReplyByEmail button label from the layout file --- quartz/components/ReplyByEmail.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/quartz/components/ReplyByEmail.tsx b/quartz/components/ReplyByEmail.tsx index b8f8450ef..416d629db 100644 --- a/quartz/components/ReplyByEmail.tsx +++ b/quartz/components/ReplyByEmail.tsx @@ -6,6 +6,7 @@ interface ReplyByEmailOptions { domain?: string includeTitles?: string[] excludeTitles?: string[] + buttonLabel?: string } // Default options will be used if not provided in the layout file @@ -13,7 +14,8 @@ const defaultOptions: ReplyByEmailOptions = { username: "ZW1haWw=", // "email" in base64 domain: "ZXhhbXBsZS5jb20=", // "email.com" in base64 includeTitles: [], - excludeTitles: ["Home", "About me", "Contact me"] + excludeTitles: ["Home", "About me", "Contact me"], + buttonLabel: "Reply by email" } const ReplyByEmail: QuartzComponent = ({ @@ -22,7 +24,8 @@ const ReplyByEmail: QuartzComponent = ({ username, domain, includeTitles, - excludeTitles + excludeTitles, + buttonLabel }: QuartzComponentProps & ReplyByEmailOptions) => { const title = fileData.frontmatter?.title @@ -31,6 +34,7 @@ const ReplyByEmail: QuartzComponent = ({ const encodedPart2 = domain || defaultOptions.domain const includeList = includeTitles || defaultOptions.includeTitles const excludeList = excludeTitles || defaultOptions.excludeTitles + const label = buttonLabel || defaultOptions.buttonLabel // Display logic: // 1. If includeTitles is not empty, only show on those pages @@ -49,7 +53,7 @@ const ReplyByEmail: QuartzComponent = ({ data-domain={encodedPart2} data-title={encodeURIComponent(title)} > - Reply by email + {label} ) @@ -130,7 +134,8 @@ export default ((opts?: ReplyByEmailOptions) => { username: opts?.username, domain: opts?.domain, includeTitles: opts?.includeTitles, - excludeTitles: opts?.excludeTitles + excludeTitles: opts?.excludeTitles, + buttonLabel: opts?.buttonLabel }) } From d94a68edb24d859e97c96dcc649bbb33775d2cac Mon Sep 17 00:00:00 2001 From: cromelex <96779452+cromelex@users.noreply.github.com> Date: Tue, 13 May 2025 15:49:02 +0100 Subject: [PATCH 3/8] feat:ReplyByEmail button fix formatting --- quartz/components/ReplyByEmail.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/quartz/components/ReplyByEmail.tsx b/quartz/components/ReplyByEmail.tsx index 416d629db..43a9807c6 100644 --- a/quartz/components/ReplyByEmail.tsx +++ b/quartz/components/ReplyByEmail.tsx @@ -47,14 +47,14 @@ const ReplyByEmail: QuartzComponent = ({ if (shouldDisplay) { return (
- +
) } else { From 64a101c0d9ca4df3c481e2c4a004178c7a01d007 Mon Sep 17 00:00:00 2001 From: cromelex <96779452+cromelex@users.noreply.github.com> Date: Tue, 13 May 2025 16:40:14 +0100 Subject: [PATCH 4/8] feat:ReplyByEmail button Add documentation for the component --- docs/features/replybyemail button.md | 67 ++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 docs/features/replybyemail button.md diff --git a/docs/features/replybyemail button.md b/docs/features/replybyemail button.md new file mode 100644 index 000000000..ead09cf67 --- /dev/null +++ b/docs/features/replybyemail button.md @@ -0,0 +1,67 @@ +--- +title: Reply by Email Button +tags: + - component +--- + +The Reply By Email Button is a feature that allows users to display a button under their notes, allowing visitors to send them an email to an obfuscated email address. +- The email address is base64 encoded to provide some basic protection from bots. +- The subject line of the email will be taken from the page Title where the button is clicked. +- You can specify on what notes the button should be displayed or excluded. +- The label on the button can also be customised. + +## Configuration + +The Reply By Email Button is disabled by default. To enable it, you can add the component to your layout configuration in `quartz.layout.ts`. + +Minimal configuration: +```ts +Component.ReplyByEmail({ + username: "Y29udGFjdA==", // "contact" encoded in base64, as in contact@example.com + domain: "ZXhhbXBsZS5jb20=", // "example.com" encoded in base64, as in contact@example.com +}), +``` +You can specify under which notes to display the button by adding the following: +```ts +includeTitles: ["Welcome to Quartz 4", "Reply by Email Button"], +``` +Alternatively, you can include the button by default by ommiting the `includeTitles` line, and specify notes under which the button should not be displayed: +```ts +excludeTitles: ["Welcome to Quartz 4", "Home"], +``` +You can also override the default `buttonLabel` text: +```ts +buttonLabel: "Reply by email" +``` + + +## Usage + +The natural placement for the Reply By Email Button is within `afterBody` in the Content Pages, so that it is displayed right under the note: + +```ts + afterBody: [ + Component.ReplyByEmail({ + username: "Y29udGFjdA==", // "contact" encoded in base64, as in contact@example.com + domain: "ZXhhbXBsZS5jb20=", // "example.com" encoded in base64, as in contact@example.com + // includeTitles: ["Welcome to Quartz 4"], // You can specify which page titles to include or comment out the line to include on all pages + excludeTitles: ["Welcome to Quartz 4", "Home"], // You can specify which page titles to exclude when includeTitles is empty + buttonLabel: "Submit feedback by email" // You can override the default button text "Reply by email" + }), + ], +``` + +## Customization + +You can customize the appearance of the ReplyByEmail button through CSS variables and styles. The component uses the following class: + +- `.reply-by-email-button` + + +Example customization in your custom CSS: + +```scss +.reply-by-email-button { + color: var(--tertiary); +} +``` From e8b3397f8cc5fb19ebbd9ed1012f3c236d760b03 Mon Sep 17 00:00:00 2001 From: cromelex <96779452+cromelex@users.noreply.github.com> Date: Tue, 13 May 2025 16:55:29 +0100 Subject: [PATCH 5/8] feat:ReplyByEmail button fix defaults --- quartz/components/ReplyByEmail.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/quartz/components/ReplyByEmail.tsx b/quartz/components/ReplyByEmail.tsx index 43a9807c6..91054faec 100644 --- a/quartz/components/ReplyByEmail.tsx +++ b/quartz/components/ReplyByEmail.tsx @@ -11,10 +11,10 @@ interface ReplyByEmailOptions { // Default options will be used if not provided in the layout file const defaultOptions: ReplyByEmailOptions = { - username: "ZW1haWw=", // "email" in base64 - domain: "ZXhhbXBsZS5jb20=", // "email.com" in base64 + username: "Y29udGFjdA==", // "contact" encoded in base64, as in contact@example.com + domain: "ZXhhbXBsZS5jb20=", // "example.com" encoded in base64, as in contact@example.com includeTitles: [], - excludeTitles: ["Home", "About me", "Contact me"], + excludeTitles: [], buttonLabel: "Reply by email" } From 93aed112af988b64e4778eb583b246a403b1f27a Mon Sep 17 00:00:00 2001 From: cromelex <96779452+cromelex@users.noreply.github.com> Date: Thu, 15 May 2025 23:50:35 +0100 Subject: [PATCH 6/8] feat:ReplyByEmail button simplified and updated component --- quartz/components/ReplyByEmail.tsx | 86 +++++++++--------------------- 1 file changed, 24 insertions(+), 62 deletions(-) diff --git a/quartz/components/ReplyByEmail.tsx b/quartz/components/ReplyByEmail.tsx index 91054faec..37a6baa6c 100644 --- a/quartz/components/ReplyByEmail.tsx +++ b/quartz/components/ReplyByEmail.tsx @@ -2,17 +2,13 @@ import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } fro import { classNames } from "../util/lang" interface ReplyByEmailOptions { - username?: string - domain?: string + email: string includeTitles?: string[] excludeTitles?: string[] buttonLabel?: string } -// Default options will be used if not provided in the layout file -const defaultOptions: ReplyByEmailOptions = { - username: "Y29udGFjdA==", // "contact" encoded in base64, as in contact@example.com - domain: "ZXhhbXBsZS5jb20=", // "example.com" encoded in base64, as in contact@example.com +const defaultOptions: Partial = { includeTitles: [], excludeTitles: [], buttonLabel: "Reply by email" @@ -21,17 +17,15 @@ const defaultOptions: ReplyByEmailOptions = { const ReplyByEmail: QuartzComponent = ({ fileData, displayClass, - username, - domain, + email, includeTitles, excludeTitles, buttonLabel }: QuartzComponentProps & ReplyByEmailOptions) => { const title = fileData.frontmatter?.title - // Use provided values or defaults - const encodedPart1 = username || defaultOptions.username - const encodedPart2 = domain || defaultOptions.domain + const encodedEmail = btoa(email) + const includeList = includeTitles || defaultOptions.includeTitles const excludeList = excludeTitles || defaultOptions.excludeTitles const label = buttonLabel || defaultOptions.buttonLabel @@ -47,15 +41,22 @@ const ReplyByEmail: QuartzComponent = ({ if (shouldDisplay) { return (
- -
+ ) } else { return null