al_newsletter 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b91a5ec7be8efd68015e56d997f7495c8f9cbb03e0f6d20637a8621e866db2eb
4
+ data.tar.gz: bdaaf9639c284628c107e12095e0ef471e4c8d29708d51c2489fa818406fa11d
5
+ SHA512:
6
+ metadata.gz: 70b41ee8378ca4d30fafd6d5e63c904dec1129bf2a708e6e04c4f7aad3a417048d0f84d2e679142b6df9716f4c45243f2c939dd100364a77202af034f5a3f699
7
+ data.tar.gz: 73c895e6150c77837c2de451370bf180f702d1dbb727cbd6b6bcdbadf5f40fb3538d6befc8946424434fc4ea1fe41a40158e0522fe2513cb99ae249a558d7aee
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-02-07
4
+ - Initial gem release.
5
+ - Added standalone newsletter form and script tags for Jekyll/al-folio sites.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Maruan Al-Shedivat.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # Al-Newsletter
2
+
3
+ A Jekyll plugin that provides a reusable newsletter form and JS handlers.
4
+
5
+ ## Installation
6
+
7
+ ```ruby
8
+ gem 'al_newsletter'
9
+ ```
10
+
11
+ Enable in `_config.yml`:
12
+
13
+ ```yaml
14
+ plugins:
15
+ - al_newsletter
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```liquid
21
+ {% al_newsletter_form align=center margin=true %}
22
+ ```
23
+
24
+ ```liquid
25
+ {% al_newsletter_scripts %}
26
+ ```
@@ -0,0 +1,99 @@
1
+ require 'jekyll'
2
+
3
+ module AlNewsletter
4
+ PLUGIN_NAME = 'al_newsletter'
5
+ ASSETS_DIR = 'assets'
6
+ JS_DIR = 'js'
7
+
8
+ class PluginStaticFile < Jekyll::StaticFile; end
9
+
10
+ class AssetsGenerator < Jekyll::Generator
11
+ safe true
12
+ priority :low
13
+
14
+ def generate(site)
15
+ plugin_lib_path = File.expand_path('.', __dir__)
16
+ source_path = File.join(plugin_lib_path, ASSETS_DIR, PLUGIN_NAME, JS_DIR, 'newsletter.js')
17
+ return unless File.exist?(source_path)
18
+
19
+ site.static_files << PluginStaticFile.new(site, plugin_lib_path, File.join(ASSETS_DIR, PLUGIN_NAME, JS_DIR), 'newsletter.js')
20
+ end
21
+ end
22
+
23
+ class NewsletterFormTag < Liquid::Tag
24
+ def initialize(tag_name, markup, tokens)
25
+ super
26
+ @opts = parse_options(markup)
27
+ end
28
+
29
+ def render(context)
30
+ site = context.registers[:site]
31
+ return '' unless site
32
+ return '' unless site.config['newsletter'].is_a?(Hash)
33
+
34
+ endpoint = site.config.dig('newsletter', 'endpoint').to_s
35
+ align = @opts['align'] || infer_align(@opts)
36
+ justify = case align
37
+ when 'left' then 'flex-start'
38
+ when 'right' then 'flex-end'
39
+ else 'center'
40
+ end
41
+ margin = @opts['margin'] == 'true' ? ' style="margin: 20px"' : ''
42
+
43
+ <<~HTML
44
+ <div class="newsletter-form-container"#{margin}>
45
+ <form class="newsletter-form" action="https://app.loops.so/api/newsletter-form/#{endpoint}" method="POST" style="justify-content: #{justify}">
46
+ <input class="newsletter-form-input" name="newsletter-form-input" type="email" placeholder="user@example.com" required="">
47
+ <button type="submit" class="newsletter-form-button" style="justify-content: #{justify}">subscribe</button>
48
+ <button type="button" class="newsletter-loading-button" style="justify-content: #{justify}">Please wait...</button>
49
+ </form>
50
+
51
+ <div class="newsletter-success" style="justify-content: #{justify}">
52
+ <p class="newsletter-success-message">You're subscribed!</p>
53
+ </div>
54
+
55
+ <div class="newsletter-error" style="justify-content: #{justify}">
56
+ <p class="newsletter-error-message">Oops! Something went wrong, please try again</p>
57
+ </div>
58
+
59
+ <button class="newsletter-back-button" type="button" onmouseout='this.style.textDecoration="none"' onmouseover='this.style.textDecoration="underline"'>
60
+ &larr; Back
61
+ </button>
62
+ </div>
63
+
64
+ <noscript>
65
+ <style>
66
+ .newsletter-form-container {
67
+ display: none;
68
+ }
69
+ </style>
70
+ </noscript>
71
+ HTML
72
+ end
73
+
74
+ private
75
+
76
+ def parse_options(markup)
77
+ opts = {}
78
+ markup.to_s.scan(/(\w+)=(\w+)/) { |k, v| opts[k] = v }
79
+ opts
80
+ end
81
+
82
+ def infer_align(opts)
83
+ return 'left' if opts['left'] == 'true'
84
+ return 'right' if opts['right'] == 'true'
85
+ 'center'
86
+ end
87
+ end
88
+
89
+ class NewsletterScriptsTag < Liquid::Tag
90
+ def render(context)
91
+ site = context['site'] || {}
92
+ baseurl = site['baseurl'] || ''
93
+ %(<script defer src="#{baseurl}/assets/al_newsletter/js/newsletter.js"></script>)
94
+ end
95
+ end
96
+ end
97
+
98
+ Liquid::Template.register_tag('al_newsletter_form', AlNewsletter::NewsletterFormTag)
99
+ Liquid::Template.register_tag('al_newsletter_scripts', AlNewsletter::NewsletterScriptsTag)
@@ -0,0 +1,105 @@
1
+ function submitHandler(event) {
2
+ event.preventDefault();
3
+ var container = event.target.parentNode;
4
+ var form = container.querySelector(".newsletter-form");
5
+ var formInput = container.querySelector(".newsletter-form-input");
6
+ var success = container.querySelector(".newsletter-success");
7
+ var errorContainer = container.querySelector(".newsletter-error");
8
+ var errorMessage = container.querySelector(".newsletter-error-message");
9
+ var backButton = container.querySelector(".newsletter-back-button");
10
+ var submitButton = container.querySelector(".newsletter-form-button");
11
+ var loadingButton = container.querySelector(".newsletter-loading-button");
12
+
13
+ const rateLimit = () => {
14
+ errorContainer.style.display = "flex";
15
+ errorMessage.innerText = "Too many signups, please try again in a little while";
16
+ submitButton.style.display = "none";
17
+ formInput.style.display = "none";
18
+ backButton.style.display = "block";
19
+ };
20
+
21
+ // Compare current time with time of previous sign up
22
+ var time = new Date();
23
+ var timestamp = time.valueOf();
24
+ var previousTimestamp = localStorage.getItem("loops-form-timestamp");
25
+
26
+ // If last sign up was less than a minute ago
27
+ // display error
28
+ if (previousTimestamp && Number(previousTimestamp) + 60000 > timestamp) {
29
+ rateLimit();
30
+ return;
31
+ }
32
+ localStorage.setItem("loops-form-timestamp", timestamp);
33
+
34
+ submitButton.style.display = "none";
35
+ loadingButton.style.display = "flex";
36
+
37
+ var formBody = "userGroup=&email=" + encodeURIComponent(formInput.value);
38
+ fetch(event.target.action, {
39
+ method: "POST",
40
+ body: formBody,
41
+ headers: {
42
+ "Content-Type": "application/x-www-form-urlencoded",
43
+ },
44
+ })
45
+ .then((res) => [res.ok, res.json(), res])
46
+ .then(([ok, dataPromise, res]) => {
47
+ if (ok) {
48
+ // If response successful
49
+ // display success
50
+ success.style.display = "flex";
51
+ form.reset();
52
+ } else {
53
+ // If response unsuccessful
54
+ // display error message or response status
55
+ dataPromise.then((data) => {
56
+ errorContainer.style.display = "flex";
57
+ errorMessage.innerText = data.message ? data.message : res.statusText;
58
+ });
59
+ }
60
+ })
61
+ .catch((error) => {
62
+ // check for cloudflare error
63
+ if (error.message === "Failed to fetch") {
64
+ rateLimit();
65
+ return;
66
+ }
67
+ // If error caught
68
+ // display error message if available
69
+ errorContainer.style.display = "flex";
70
+ if (error.message) errorMessage.innerText = error.message;
71
+ localStorage.setItem("loops-form-timestamp", "");
72
+ })
73
+ .finally(() => {
74
+ formInput.style.display = "none";
75
+ loadingButton.style.display = "none";
76
+ backButton.style.display = "block";
77
+ });
78
+ }
79
+ function resetFormHandler(event) {
80
+ var container = event.target.parentNode;
81
+ var formInput = container.querySelector(".newsletter-form-input");
82
+ var success = container.querySelector(".newsletter-success");
83
+ var errorContainer = container.querySelector(".newsletter-error");
84
+ var errorMessage = container.querySelector(".newsletter-error-message");
85
+ var backButton = container.querySelector(".newsletter-back-button");
86
+ var submitButton = container.querySelector(".newsletter-form-button");
87
+
88
+ success.style.display = "none";
89
+ errorContainer.style.display = "none";
90
+ errorMessage.innerText = "Oops! Something went wrong, please try again";
91
+ backButton.style.display = "none";
92
+ formInput.style.display = "flex";
93
+ submitButton.style.display = "flex";
94
+ }
95
+
96
+ var formContainers = document.getElementsByClassName("newsletter-form-container");
97
+
98
+ for (var i = 0; i < formContainers.length; i++) {
99
+ var formContainer = formContainers[i];
100
+ var handlersAdded = formContainer.classList.contains("newsletter-handlers-added");
101
+ if (handlersAdded) continue;
102
+ formContainer.querySelector(".newsletter-form").addEventListener("submit", submitHandler);
103
+ formContainer.querySelector(".newsletter-back-button").addEventListener("click", resetFormHandler);
104
+ formContainer.classList.add("newsletter-handlers-added");
105
+ }
metadata ADDED
@@ -0,0 +1,126 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: al_newsletter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - al-org
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-02-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: jekyll
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.9'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '5.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '3.9'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '5.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: liquid
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '4.0'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '6.0'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '4.0'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '6.0'
53
+ - !ruby/object:Gem::Dependency
54
+ name: bundler
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '2.0'
60
+ - - "<"
61
+ - !ruby/object:Gem::Version
62
+ version: '3.0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '2.0'
70
+ - - "<"
71
+ - !ruby/object:Gem::Version
72
+ version: '3.0'
73
+ - !ruby/object:Gem::Dependency
74
+ name: rake
75
+ requirement: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - "~>"
78
+ - !ruby/object:Gem::Version
79
+ version: '13.0'
80
+ type: :development
81
+ prerelease: false
82
+ version_requirements: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - "~>"
85
+ - !ruby/object:Gem::Version
86
+ version: '13.0'
87
+ description: Jekyll plugin extracted from al-folio that renders configurable newsletter
88
+ forms and ships frontend JavaScript handlers.
89
+ email:
90
+ - dev@al-org.dev
91
+ executables: []
92
+ extensions: []
93
+ extra_rdoc_files: []
94
+ files:
95
+ - CHANGELOG.md
96
+ - LICENSE
97
+ - README.md
98
+ - lib/al_newsletter.rb
99
+ - lib/assets/al_newsletter/js/newsletter.js
100
+ homepage: https://github.com/al-org-dev/al-newsletter
101
+ licenses:
102
+ - MIT
103
+ metadata:
104
+ allowed_push_host: https://rubygems.org
105
+ homepage_uri: https://github.com/al-org-dev/al-newsletter
106
+ source_code_uri: https://github.com/al-org-dev/al-newsletter
107
+ post_install_message:
108
+ rdoc_options: []
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '2.7'
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ requirements: []
122
+ rubygems_version: 3.0.3.1
123
+ signing_key:
124
+ specification_version: 4
125
+ summary: Newsletter form tag and assets for Jekyll
126
+ test_files: []