Skip to content

Commit 0ac9e44

Browse files
asboyergeorge-gca
andauthored
Added support for a newsletter (#2517)
In reference to idea: #2097 In reference to request: #923 (comment) Added support to integrate a [loops.so](https://loops.so/) mailing list into the site. To use, you need to enable `newsletter` in `_config.yml`. You also must specify a loops endpoint (although I think any mailing list endpoint can work), which you can get when you set up a mailing list on loops. More documentation on loops: [here](https://loops.so/docs/forms/custom-form). Once that is enabled, the behavior is different depending on how you specified your footer to behave in `_config.yml`. If `footer_fixed: true`, then the sign up will appear at the bottom of the about page, as well as at the bottom of blog posts, if you enable `related_posts`. If `footer_fixed: false`, then the newsletter signup will be in the footer (on every page), like it is in on [my website](https://asboyer.com). I'm not attached to the placement of the signup, and you can choose to include it wherever you want with `{% include scripts/newsletter.liquid %}`. Also if you include positional variables into that, you can choose how you center the signup. So `{% include scripts/newsletter.liquid left=true %}` positions the signup bar to the left. Here are some screenshots below: ## Dark version ![image](https://github.com/alshedivat/al-folio/assets/52665298/af7fdb81-6e5f-47a9-958b-4cb93bba9e8f) ## Light version ![image](https://github.com/alshedivat/al-folio/assets/52665298/927f8bc5-b481-448b-ae5e-6f5b1c613243) I think the input field color should probably change to maybe be light for both themes? What do you think? I think the dark background looks cool, but I don't usually see that done like that on other sites. ## Footer fixed ![image](https://github.com/alshedivat/al-folio/assets/52665298/c52f3dc1-0e45-400e-8b71-eeb00d00cb01) ![image](https://github.com/alshedivat/al-folio/assets/52665298/678a2d45-88ab-4d9a-b8cc-9fc6db26d744) ## Footer not fixed ![image](https://github.com/alshedivat/al-folio/assets/52665298/fd2c0228-2bce-4335-ac3c-5cb20a3307e2) ![image](https://github.com/alshedivat/al-folio/assets/52665298/f594b4f2-67e0-4f2b-a3e8-febd579aaf19) To clarify, if footer isn't fixed, the email signup will appear on every page. --------- Co-authored-by: George <[email protected]>
1 parent a25df79 commit 0ac9e44

File tree

7 files changed

+320
-0
lines changed

7 files changed

+320
-0
lines changed

_config.yml

+9
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,15 @@ external_sources:
174174
- url: https://blog.google/technology/ai/google-gemini-update-flash-ai-assistant-io-2024/
175175
published_date: 2024-05-14
176176

177+
# -----------------------------------------------------------------------------
178+
# Newsletter
179+
# -----------------------------------------------------------------------------
180+
181+
newsletter:
182+
enabled: false
183+
endpoint: # your loops endpoint (e.g., https://app.loops.so/api/newsletter-form/YOUR-ENDPOINT)
184+
# https://loops.so/docs/forms/custom-form
185+
177186
# -----------------------------------------------------------------------------
178187
# Collections
179188
# -----------------------------------------------------------------------------

_includes/footer.liquid

+4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
</footer>
1616
{% else %}
1717
<footer class="sticky-bottom mt-5" role="contentinfo">
18+
{% if site.newsletter.enabled %}
19+
{% include scripts/newsletter.liquid %}
20+
{% endif %}
21+
1822
<div class="container">
1923
&copy; Copyright {{ site.time | date: '%Y' }}
2024
{{ site.first_name }}

_includes/related_posts.liquid

+4
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,7 @@
1616
<a class="text-pink-700 underline font-semibold hover:text-pink-800" href="{{ site.baseurl }}{{ post.url }}">{{ post.title }}</a>
1717
</li>
1818
{% endfor %}
19+
{% if site.newsletter.enabled and site.footer_fixed %}
20+
<p class="mb-2" style="margin-top: 1.5rem !important">Subscribe to be notified of future articles:</p>
21+
{% include scripts/newsletter.liquid left=true %}
22+
{% endif %}

_includes/scripts/newsletter.liquid

+176
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
<div
2+
class="newsletter-form-container"
3+
{% if include.center %}
4+
style="margin: 20px"
5+
{% endif %}
6+
>
7+
<form
8+
class="newsletter-form"
9+
action="https://app.loops.so/api/newsletter-form/{{ site.newsletter.endpoint }}"
10+
method="POST"
11+
style="justify-content: {% if include.left %}flex-start{% elsif include.right %}flex-end{% else %}center{% endif %}"
12+
>
13+
<input
14+
class="newsletter-form-input"
15+
name="newsletter-form-input"
16+
type="email"
17+
placeholder="[email protected]"
18+
required=""
19+
>
20+
21+
<button
22+
type="submit"
23+
class="newsletter-form-button"
24+
style="justify-content: {% if include.left %}flex-start{% elsif include.right %}flex-end{% else %}center{% endif %}"
25+
>
26+
subscribe
27+
</button>
28+
29+
<button
30+
type="button"
31+
class="newsletter-loading-button"
32+
style="justify-content: {% if include.left %}flex-start{% elsif include.right %}flex-end{% else %}center{% endif %}"
33+
>
34+
Please wait...
35+
</button>
36+
</form>
37+
38+
<div
39+
class="newsletter-success"
40+
style="justify-content: {% if include.left %}flex-start{% elsif include.right %}flex-end{% else %}center{% endif %}"
41+
>
42+
<p class="newsletter-success-message">You're subscribed!</p>
43+
</div>
44+
45+
<div
46+
class="newsletter-error"
47+
style="justify-content: {% if include.left %}flex-start{% elsif include.right %}flex-end{% else %}center{% endif %}"
48+
>
49+
<p class="newsletter-error-message">Oops! Something went wrong, please try again</p>
50+
</div>
51+
52+
<button
53+
class="newsletter-back-button"
54+
type="button"
55+
onmouseout='this.style.textDecoration="none"'
56+
onmouseover='this.style.textDecoration="underline"'
57+
>
58+
&larr; Back
59+
</button>
60+
</div>
61+
62+
<script>
63+
function submitHandler(event) {
64+
event.preventDefault();
65+
var container = event.target.parentNode;
66+
var form = container.querySelector('.newsletter-form');
67+
var formInput = container.querySelector('.newsletter-form-input');
68+
var success = container.querySelector('.newsletter-success');
69+
var errorContainer = container.querySelector('.newsletter-error');
70+
var errorMessage = container.querySelector('.newsletter-error-message');
71+
var backButton = container.querySelector('.newsletter-back-button');
72+
var submitButton = container.querySelector('.newsletter-form-button');
73+
var loadingButton = container.querySelector('.newsletter-loading-button');
74+
75+
const rateLimit = () => {
76+
errorContainer.style.display = 'flex';
77+
errorMessage.innerText = 'Too many signups, please try again in a little while';
78+
submitButton.style.display = 'none';
79+
formInput.style.display = 'none';
80+
backButton.style.display = 'block';
81+
};
82+
83+
// Compare current time with time of previous sign up
84+
var time = new Date();
85+
var timestamp = time.valueOf();
86+
var previousTimestamp = localStorage.getItem('loops-form-timestamp');
87+
88+
// If last sign up was less than a minute ago
89+
// display error
90+
if (previousTimestamp && Number(previousTimestamp) + 60000 > timestamp) {
91+
rateLimit();
92+
return;
93+
}
94+
localStorage.setItem('loops-form-timestamp', timestamp);
95+
96+
submitButton.style.display = 'none';
97+
loadingButton.style.display = 'flex';
98+
99+
var formBody = 'userGroup=&email=' + encodeURIComponent(formInput.value);
100+
fetch(event.target.action, {
101+
method: 'POST',
102+
body: formBody,
103+
headers: {
104+
'Content-Type': 'application/x-www-form-urlencoded',
105+
},
106+
})
107+
.then((res) => [res.ok, res.json(), res])
108+
.then(([ok, dataPromise, res]) => {
109+
if (ok) {
110+
// If response successful
111+
// display success
112+
success.style.display = 'flex';
113+
form.reset();
114+
} else {
115+
// If response unsuccessful
116+
// display error message or response status
117+
dataPromise.then((data) => {
118+
errorContainer.style.display = 'flex';
119+
errorMessage.innerText = data.message ? data.message : res.statusText;
120+
});
121+
}
122+
})
123+
.catch((error) => {
124+
// check for cloudflare error
125+
if (error.message === 'Failed to fetch') {
126+
rateLimit();
127+
return;
128+
}
129+
// If error caught
130+
// display error message if available
131+
errorContainer.style.display = 'flex';
132+
if (error.message) errorMessage.innerText = error.message;
133+
localStorage.setItem('loops-form-timestamp', '');
134+
})
135+
.finally(() => {
136+
formInput.style.display = 'none';
137+
loadingButton.style.display = 'none';
138+
backButton.style.display = 'block';
139+
});
140+
}
141+
function resetFormHandler(event) {
142+
var container = event.target.parentNode;
143+
var formInput = container.querySelector('.newsletter-form-input');
144+
var success = container.querySelector('.newsletter-success');
145+
var errorContainer = container.querySelector('.newsletter-error');
146+
var errorMessage = container.querySelector('.newsletter-error-message');
147+
var backButton = container.querySelector('.newsletter-back-button');
148+
var submitButton = container.querySelector('.newsletter-form-button');
149+
150+
success.style.display = 'none';
151+
errorContainer.style.display = 'none';
152+
errorMessage.innerText = 'Oops! Something went wrong, please try again';
153+
backButton.style.display = 'none';
154+
formInput.style.display = 'flex';
155+
submitButton.style.display = 'flex';
156+
}
157+
158+
var formContainers = document.getElementsByClassName('newsletter-form-container');
159+
160+
for (var i = 0; i < formContainers.length; i++) {
161+
var formContainer = formContainers[i];
162+
var handlersAdded = formContainer.classList.contains('newsletter-handlers-added');
163+
if (handlersAdded) continue;
164+
formContainer.querySelector('.newsletter-form').addEventListener('submit', submitHandler);
165+
formContainer.querySelector('.newsletter-back-button').addEventListener('click', resetFormHandler);
166+
formContainer.classList.add('newsletter-handlers-added');
167+
}
168+
</script>
169+
170+
<noscript>
171+
<style>
172+
.newsletter-form-container {
173+
display: none;
174+
}
175+
</style>
176+
</noscript>

_layouts/about.liquid

+4
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,9 @@ layout: default
7272
<div class="contact-note">{{ site.contact_note }}</div>
7373
</div>
7474
{% endif %}
75+
76+
{% if site.newsletter.enabled and site.footer_fixed %}
77+
{% include scripts/newsletter.liquid center=true %}
78+
{% endif %}
7579
</article>
7680
</div>

_sass/_base.scss

+119
Original file line numberDiff line numberDiff line change
@@ -1128,13 +1128,127 @@ ninja-keys {
11281128
--ninja-modal-background: var(--global-bg-color);
11291129
--ninja-z-index: 1031;
11301130
}
1131+
11311132
ninja-keys::part(ninja-input) {
11321133
color: var(--ninja-selected-text-color);
11331134
}
1135+
11341136
ninja-keys::part(ninja-input-wrapper) {
11351137
background: var(--global-bg-color);
11361138
}
11371139

1140+
// newsletter
1141+
.newsletter-form-container {
1142+
margin-bottom: 20px;
1143+
}
1144+
1145+
.newsletter-form {
1146+
display: flex;
1147+
flex-direction: row;
1148+
align-items: center;
1149+
width: 100%;
1150+
}
1151+
1152+
.newsletter-form-input {
1153+
color: var(--global-newsletter-text-color);
1154+
background: var(--global-newsletter-bg-color);
1155+
border: 1px solid var(--global-newsletter-text-color);
1156+
outline: none;
1157+
margin: 0px 10px 0px 0px;
1158+
width: 100%;
1159+
max-width: 350px;
1160+
min-width: 100px;
1161+
box-sizing: border-box;
1162+
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px;
1163+
border-radius: 6px;
1164+
padding: 8px 12px;
1165+
}
1166+
1167+
.newsletter-form-input:focus {
1168+
border-color: var(--global-theme-color) !important;
1169+
}
1170+
1171+
.newsletter-form-button {
1172+
background: var(--global-theme-color);
1173+
color: var(--global-bg-color);
1174+
display: flex;
1175+
width: min-content;
1176+
max-width: 200px;
1177+
white-space: nowrap;
1178+
height: 38px;
1179+
align-items: center;
1180+
flex-direction: row;
1181+
padding: 9px 17px;
1182+
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px;
1183+
border-radius: 6px;
1184+
text-align: center;
1185+
font-style: normal;
1186+
font-weight: 500;
1187+
line-height: 20px;
1188+
border: none;
1189+
cursor: pointer;
1190+
}
1191+
1192+
.newsletter-loading-button {
1193+
background: var(--global-theme-color);
1194+
color: var(--global-bg-color);
1195+
display: none;
1196+
width: min-content;
1197+
max-width: 300px;
1198+
white-space: nowrap;
1199+
height: 38px;
1200+
align-items: center;
1201+
flex-direction: row;
1202+
padding: 9px 17px;
1203+
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px;
1204+
border-radius: 6px;
1205+
text-align: center;
1206+
font-style: normal;
1207+
font-weight: 500;
1208+
line-height: 20px;
1209+
border: none;
1210+
cursor: pointer;
1211+
margin-right: 20px;
1212+
}
1213+
1214+
.newsletter-success {
1215+
color: var(--global-text-color);
1216+
display: none;
1217+
align-items: center;
1218+
width: 100%;
1219+
}
1220+
1221+
.newsletter-error {
1222+
color: var(--global-theme-color);
1223+
display: none;
1224+
align-items: center;
1225+
width: 100%;
1226+
}
1227+
1228+
.newsletter-back-button {
1229+
color: var(--global-theme-color);
1230+
margin: 10px auto;
1231+
text-align: center;
1232+
display: none;
1233+
background: transparent;
1234+
border: none;
1235+
cursor: pointer;
1236+
}
1237+
1238+
@media (max-width: 575px) {
1239+
.newsletter-form-input,
1240+
.newsletter-form-button,
1241+
.newsletter-loading-button,
1242+
.newsletter-success,
1243+
.newsletter-error {
1244+
font-size: 16px !important;
1245+
}
1246+
.newsletter-form-container {
1247+
margin-right: 20px;
1248+
margin-left: 20px;
1249+
}
1250+
}
1251+
11381252
// popover is used for annotation in bib.liquid
11391253
.popover {
11401254
background-color: var(--global-bg-color);
@@ -1144,25 +1258,30 @@ ninja-keys::part(ninja-input-wrapper) {
11441258
color: var(--global-text-color); // Header text color
11451259
border-bottom: 1px solid var(--global-divider-color);
11461260
}
1261+
11471262
.popover-body {
11481263
color: var(--global-text-color); // Body text color
11491264
}
11501265
}
1266+
11511267
.bs-popover-top {
11521268
// arrow fill color
11531269
.arrow::after {
11541270
border-top-color: var(--global-bg-color);
11551271
}
1272+
11561273
// arrow border color
11571274
.arrow:before {
11581275
border-top-color: var(--global-divider-color);
11591276
}
11601277
}
1278+
11611279
.bs-popover-bottom {
11621280
// arrow fill color
11631281
.arrow::after {
11641282
border-bottom-color: var(--global-bg-color);
11651283
}
1284+
11661285
// arrow border color
11671286
.arrow:before {
11681287
border-bottom-color: var(--global-divider-color);

_sass/_themes.scss

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
--global-highlight-color: #{$red-color-dark};
2020
--global-back-to-top-bg-color: rgba(#{red($black-color)}, #{green($black-color)}, #{blue($black-color)}, 0.4);
2121
--global-back-to-top-text-color: #{$white-color};
22+
--global-newsletter-bg-color: #{$white-color};
23+
--global-newsletter-text-color: #{$black-color};
2224

2325
--global-tip-block: #42b983;
2426
--global-tip-block-bg: #e2f5ec;
@@ -81,6 +83,8 @@ html[data-theme="dark"] {
8183
--global-card-bg-color: #{$grey-900};
8284
--global-back-to-top-bg-color: rgba(#{red($white-color)}, #{green($white-color)}, #{blue($white-color)}, 0.5);
8385
--global-back-to-top-text-color: #{$black-color};
86+
--global-newsletter-bg-color: #{$grey-color-light};
87+
--global-newsletter-text-color: #{$grey-color-dark};
8488

8589
--global-tip-block: #42b983;
8690
--global-tip-block-bg: #e2f5ec;

0 commit comments

Comments
 (0)