Skip to content

Commit 0f5a6ba

Browse files
committed
feat: enhance feed directory with Alpine.js integration
1 parent 919c47b commit 0f5a6ba

3 files changed

Lines changed: 112 additions & 187 deletions

File tree

assets/js/feed-directory/index.js

Lines changed: 42 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -1,171 +1,49 @@
1-
const App = {
2-
init() {
3-
this.instanceEl = document.querySelector('input[name="instance"]');
4-
if (!this.instanceEl) return;
5-
6-
this.searchEl = document.querySelector('input[name="search"]');
7-
this.configsEl = document.querySelector("#configs");
8-
this.cards = this.configsEl.querySelectorAll(".card");
9-
this.bindEvents();
10-
this.updateInstanceUrl();
11-
this.initDynamicForms();
12-
},
13-
14-
bindEvents() {
15-
this.searchEl.addEventListener("input", this.filterConfigs.bind(this));
16-
this.configsEl.addEventListener("click", (event) => {
17-
const { bindClick } = event.target.dataset;
18-
if (bindClick === "show") {
19-
this.handleShowClick(event);
20-
} else if (bindClick === "copy") {
21-
this.handleCopyClick(event);
1+
document.addEventListener("alpine:init", () => {
2+
Alpine.data("feedDirectory", () => ({
3+
instanceUrl: "https://html2rss.herokuapp.com/",
4+
searchQuery: "",
5+
configs: window.feedDirectoryData,
6+
7+
filterConfig(configName) {
8+
if (!this.searchQuery) {
9+
return true;
2210
}
23-
});
24-
25-
this.instanceEl.addEventListener("blur", this.updateInstanceUrl.bind(this));
26-
},
27-
28-
filterConfigs() {
29-
const query = this.searchEl.value.toLowerCase();
30-
this.cards.forEach((card) => {
31-
const title = card.querySelector(".card-title").innerText.toLowerCase();
32-
if (title.includes(query)) {
33-
card.style.display = "";
34-
} else {
35-
card.style.display = "none";
11+
return configName.toLowerCase().includes(this.searchQuery.toLowerCase());
12+
},
13+
14+
getFeedUrl(config, params = {}) {
15+
let url = this.instanceUrl.endsWith("/")
16+
? this.instanceUrl
17+
: `${this.instanceUrl}/`;
18+
url += `${config.domain}/${config.name}.rss`;
19+
20+
const queryParams = new URLSearchParams();
21+
if (Array.isArray(config.url_parameters)) {
22+
config.url_parameters.forEach((param) => {
23+
const key = param[0];
24+
if (params[key]) {
25+
queryParams.append(key, params[key]);
26+
}
27+
});
3628
}
37-
});
38-
},
39-
40-
getInstanceUrl() {
41-
const url = this.instanceEl.value;
42-
return url.endsWith("/") ? url : `${url}/`;
43-
},
4429

45-
updateInstanceUrl() {
46-
const url = this.getInstanceUrl();
47-
document.querySelectorAll(".instance").forEach((el) => {
48-
if (el instanceof HTMLElement) {
49-
el.innerText = url;
30+
const queryString = queryParams.toString();
31+
if (queryString) {
32+
url += `?${queryString}`;
5033
}
51-
});
52-
},
53-
54-
handleShowClick(event) {
55-
const { target } = event;
56-
const path = this.getPath(target);
57-
if (path) {
58-
target.href = `${this.getInstanceUrl()}${path}`;
59-
} else {
60-
event.preventDefault();
61-
}
62-
},
63-
64-
async handleCopyClick(event) {
65-
const { target } = event;
66-
const path = this.getPath(target);
67-
if (!path) return;
68-
69-
const href = `${this.getInstanceUrl()}${path}`;
70-
71-
try {
72-
await navigator.clipboard.writeText(href);
73-
this.showCopiedState(target);
74-
} catch (err) {
75-
console.error("Failed to copy: ", err);
76-
}
77-
},
78-
79-
showCopiedState(triggerEl) {
80-
triggerEl.classList.add("copied");
81-
setTimeout(() => {
82-
triggerEl.classList.remove("copied");
83-
triggerEl.blur();
84-
}, 1000);
85-
},
86-
87-
initDynamicForms() {
88-
document
89-
.querySelectorAll("[data-dynamic-form]")
90-
.forEach((formContainer) => {
91-
const form = formContainer.querySelector("form");
92-
const pathPreview = formContainer.querySelector("[data-path-preview]");
93-
const actionButtons = formContainer.querySelectorAll(
94-
"[data-path-template]",
95-
);
96-
97-
if (!form || !pathPreview || !(pathPreview instanceof HTMLElement))
98-
return;
99-
100-
const updateDynamicPath = () => {
101-
const formData = new FormData(form);
102-
const firstButton = actionButtons[0];
103-
if (!(firstButton instanceof HTMLElement)) return;
10434

105-
let pathTemplate = firstButton.dataset.pathTemplate || "";
106-
let pathPreviewTemplate = pathTemplate;
107-
108-
for (const [key, value] of formData.entries()) {
109-
if (typeof value === "string") {
110-
const placeholder = `{${key}}`;
111-
pathTemplate = pathTemplate.replace(
112-
placeholder,
113-
encodeURIComponent(value),
114-
);
115-
pathPreviewTemplate = pathPreviewTemplate.replace(
116-
placeholder,
117-
value || `{${key}}`,
118-
);
119-
}
120-
}
121-
122-
actionButtons.forEach((button) => {
123-
if (button instanceof HTMLElement) {
124-
button.dataset.path = pathTemplate;
125-
}
126-
});
127-
pathPreview.innerText = pathPreviewTemplate;
128-
};
129-
130-
form.addEventListener("input", updateDynamicPath);
131-
updateDynamicPath(); // Initial update
35+
return url;
36+
},
37+
38+
copyUrl(config, params = {}, event) {
39+
const url = this.getFeedUrl(config, params);
40+
navigator.clipboard.writeText(url).then(() => {
41+
const originalText = event.target.innerHTML;
42+
event.target.innerHTML = "Copied!";
43+
setTimeout(() => {
44+
event.target.innerHTML = originalText;
45+
}, 2000);
13246
});
133-
},
134-
135-
getPath(target) {
136-
if (!(target instanceof HTMLElement)) return null;
137-
138-
if (target.dataset.path) {
139-
return target.dataset.path;
140-
}
141-
142-
if (target.dataset.pathTemplate) {
143-
const formContainer = target.closest("[data-dynamic-form]");
144-
if (!formContainer) return null;
145-
146-
const form = formContainer.querySelector("form");
147-
if (!form) return null;
148-
149-
const formData = new FormData(form);
150-
let path = target.dataset.pathTemplate;
151-
let allParamsFilled = true;
152-
153-
for (const [key, value] of formData.entries()) {
154-
if (!value) {
155-
allParamsFilled = false;
156-
}
157-
if (typeof value === "string") {
158-
path = path.replace(`{${key}}`, encodeURIComponent(value));
159-
}
160-
}
161-
162-
return allParamsFilled ? path : null;
163-
}
164-
165-
return null;
166-
},
167-
};
168-
169-
document.addEventListener("DOMContentLoaded", () => {
170-
App.init();
47+
},
48+
}));
17149
});

feed-directory/index.html

Lines changed: 69 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,20 @@ <h2 class="fs-6">What is an Instance URL?</h2>
5050
</span>
5151
</noscript>
5252

53-
<div id="configs">
53+
<script>
54+
// Define global data for Alpine.js to consume, processed by Jekyll
55+
// The `site.data.configs` is already JSONified by Jekyll, so we just need to assign it.
56+
window.feedDirectoryData = {{ site.data.configs | jsonify }};
57+
</script>
58+
59+
<div id="configs" x-data="feedDirectory()">
5460
<fieldset class="mb-4 py-1 px-2">
5561
<legend class="fs-5">Instance URL</legend>
5662

5763
<input
5864
type="text"
5965
name="instance"
60-
value="https://1.h2r.workers.dev"
66+
x-model="instanceUrl"
6167
class="bg-grey-dk-100 p-2"
6268
/>
6369
</fieldset>
@@ -69,13 +75,17 @@ <h2 class="fs-6">What is an Instance URL?</h2>
6975
type="search"
7076
name="search"
7177
placeholder="e.g. heise"
78+
x-model="searchQuery"
7279
class="bg-grey-dk-100 p-2"
7380
/>
7481
</fieldset>
7582

7683
<div class="card-deck">
7784
{% for config in site.data.configs %}
78-
<div class="card mb-4">
85+
<div
86+
class="card mb-4"
87+
x-show="filterConfig('{{ config.domain }}/{{ config.name }}')"
88+
>
7989
<div class="card-header d-flex-justify-content-between">
8090
<h3 class="card-title mb-0 text-mono fs-5">
8191
{{ config.domain }}/{{ config.name }}
@@ -118,15 +128,14 @@ <h3 class="card-title mb-0 text-mono fs-5">
118128
<div class="mt-4 d-flex-justify-content-between">
119129
<a
120130
href="#"
121-
data-bind-click="show"
122-
data-path="{{ config.domain }}/{{ config.name }}.rss"
131+
x-bind:href="getFeedUrl(JSON.parse('{{ config | jsonify | escape }}'))"
123132
class="btn btn-primary"
124133
rel="noopener noreferrer"
134+
target="_blank"
125135
>Show RSS</a
126136
>
127137
<button
128-
data-bind-click="copy"
129-
data-path="{{ config.domain }}/{{ config.name }}.rss"
138+
x-on:click.prevent="copyUrl(JSON.parse('{{ config | jsonify | escape }}'), {}, $event)"
130139
class="btn btn-icon"
131140
rel="noopener noreferrer"
132141
title="Copy URL"
@@ -142,7 +151,40 @@ <h3 class="card-title mb-0 text-mono fs-5">
142151
</a>
143152
</div>
144153
{%- else -%}
145-
<div class="mt-4" data-dynamic-form>
154+
<div
155+
class="mt-4"
156+
x-data="{ params: {}, pathPreview: '' }"
157+
x-init="
158+
const currentConfig = JSON.parse('{{ config | jsonify | escape }}');
159+
console.log('{{ config | jsonify | escape }}');
160+
$watch('params', () => {
161+
let template = `${currentConfig.domain}/${currentConfig.name}.rss?`;
162+
let previewTemplate = template;
163+
if (Array.isArray(currentConfig.url_parameters)) {
164+
currentConfig.url_parameters.forEach(param => {
165+
const key = param[0];
166+
const value = params[key];
167+
template += `${key}=${encodeURIComponent(value || '')}&`;
168+
previewTemplate += `${key}=${value || `{${param[1]}}`}&`;
169+
});
170+
template = template.slice(0, -1); // Remove trailing '&'
171+
previewTemplate = previewTemplate.slice(0, -1); // Remove trailing '&'
172+
}
173+
pathPreview = previewTemplate;
174+
$refs.showBtn.href = getFeedUrl(currentConfig, params);
175+
$refs.copyBtn.dataset.url = getFeedUrl(currentConfig, params);
176+
});
177+
$nextTick(() => {
178+
let initialParams = {};
179+
if (Array.isArray(currentConfig.url_parameters)) {
180+
currentConfig.url_parameters.forEach(param => {
181+
initialParams[param[0]] = '';
182+
});
183+
}
184+
params = initialParams;
185+
});
186+
"
187+
>
146188
<form>
147189
{% for param in config.url_parameters %}
148190
<div class="form-group">
@@ -155,26 +197,27 @@ <h3 class="card-title mb-0 text-mono fs-5">
155197
name="{{ param[0] }}"
156198
class="form-control"
157199
placeholder="{{ param[1] }}"
158-
data-param
200+
x-model="params.{{ param[0] }}"
159201
/>
160202
</div>
161203
{% endfor %}
162204
</form>
163205
<div class="mt-2 d-flex-justify-content-between">
164206
<a
165207
href="#"
166-
data-bind-click="show"
167-
data-path-template="{{ config.domain }}/{{ config.name }}.rss?{% for param in config.url_parameters %}{{ param[0] }}={{ ob }}{{ param[0] }}{{ cb }}&{% endfor %}"
208+
x-bind:href="getFeedUrl(JSON.parse('{{ config | jsonify | escape }}'), params)"
168209
class="btn btn-primary"
169210
rel="noopener noreferrer"
211+
target="_blank"
212+
x-ref="showBtn"
170213
>Show RSS</a
171214
>
172215
<button
173-
data-bind-click="copy"
174-
data-path-template="{{ config.domain }}/{{ config.name }}.rss?{% for param in config.url_parameters %}{{ param[0] }}={{ ob }}{{ param[0] }}{{ cb }}&{% endfor %}"
216+
x-on:click.prevent="copyUrl(JSON.parse('{{ config | jsonify | escape }}'), params, $event)"
175217
class="btn btn-icon"
176218
rel="noopener noreferrer"
177219
title="Copy URL"
220+
x-ref="copyBtn"
178221
>
179222
<svg><use xlink:href="#copy-icon"></use></svg>
180223
</button>
@@ -188,12 +231,11 @@ <h3 class="card-title mb-0 text-mono fs-5">
188231
</div>
189232
<div class="mt-2">
190233
<code class="d-block w-100 p-2 bg-grey-dk-300 text-grey-dk-000">
191-
<span class="instance"></span
192-
><span data-path-preview
193-
>{{ config.domain }}/{{ config.name }}.rss?{% for param in
194-
config.url_parameters %}{{ param[0] }}={{ ob }}{{ param[1] }}{{
195-
cb }}&{% endfor %}</span
196-
>
234+
<span
235+
class="instance"
236+
x-text="instanceUrl.endsWith('/') ? instanceUrl : instanceUrl + '/'"
237+
></span
238+
><span x-text="pathPreview"></span>
197239
</code>
198240
</div>
199241
</div>
@@ -222,4 +264,11 @@ <h2 class="fs-6">Contribute to the Directory</h2>
222264
</a>
223265
</div>
224266

225-
<script src="/assets/js/feed-directory/index.js" async defer></script>
267+
<script
268+
src="{{ '/assets/js/feed-directory/index.js' | relative_url }}"
269+
defer
270+
></script>
271+
<script
272+
defer
273+
src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.1/dist/cdn.min.js"
274+
></script>

get-involved/index.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,10 @@ nav_order: 4
99

1010
Want to participate in the `html2rss` project? Explore the options below to engage with our community and contribute to the project's growth.
1111

12-
---
13-
12+
- [**The Project Roadmap**]({{ 'https://github.com/orgs/html2rss/projects/3/views/1' }}): See what's being worked on, planned, and needs attention across all `html2rss` components.
1413
- [**Report Bugs & Discuss Features**]({{ '/get-involved/issues-and-features' | relative_url }}): Found a bug or have a new feature idea? Learn where to report and discuss them.
1514
- [**Join Community Discussions**]({{ '/get-involved/discussions' | relative_url }}): Connect with other users and contributors, ask questions, and share ideas.
1615
- [**Contribute to html2rss**]({{ '/contributing' | relative_url }}): Ready to contribute code, documentation, or feed configurations? Find detailed guides here.
17-
- [**📊 View Project Roadmap & Tasks**]({{ 'https://github.com/orgs/html2rss/projects/3/views/1' }}): See what's being worked on, planned, and needs attention across all `html2rss` projects.
1816

1917
---
2018

0 commit comments

Comments
 (0)