@rmdes/indiekit-endpoint-donation 0.1.0-alpha.1

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.
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@rmdes/indiekit-endpoint-donation",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "Stripe-backed donation endpoint for Indiekit. Treats Stripe Products as campaigns, PaymentIntents/Checkout Sessions as donations, exposes admin UI + public stats API + rebuild trigger.",
5
+ "keywords": [
6
+ "indiekit",
7
+ "indiekit-plugin",
8
+ "indieweb",
9
+ "stripe",
10
+ "donations",
11
+ "fundraising"
12
+ ],
13
+ "homepage": "https://github.com/rmdes/indiekit-endpoint-donation",
14
+ "author": {
15
+ "name": "Ricardo Mendes",
16
+ "url": "https://rmendes.net"
17
+ },
18
+ "license": "MIT",
19
+ "engines": {
20
+ "node": ">=20"
21
+ },
22
+ "type": "module",
23
+ "main": "index.js",
24
+ "files": [
25
+ "lib",
26
+ "views",
27
+ "locales",
28
+ "assets",
29
+ "index.js"
30
+ ],
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/rmdes/indiekit-endpoint-donation.git"
34
+ },
35
+ "dependencies": {
36
+ "@indiekit/error": "^1.0.0-beta.27",
37
+ "@indiekit/frontend": "^1.0.0-beta.27",
38
+ "@rmdes/indiekit-startup-gate": "^1.0.0",
39
+ "express": "^5.0.0",
40
+ "stripe": "^17.0.0"
41
+ },
42
+ "peerDependencies": {
43
+ "@indiekit/indiekit": ">=1.0.0-beta.25"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ }
48
+ }
@@ -0,0 +1,33 @@
1
+ {% extends "layouts/donation.njk" %}
2
+
3
+ {% block donation %}
4
+ {% call section({ title: campaign.title }) %}
5
+ <dl>
6
+ <dt>Stripe Product ID</dt><dd><code>{{ campaign.stripe_product_id }}</code></dd>
7
+ <dt>Sous-titre</dt><dd>{{ campaign.subtitle }}</dd>
8
+ <dt>{{ __("donation.campaigns.goal") }}</dt><dd>{% if campaign.goal_cents %}{{ (campaign.goal_cents / 100) | round(0) }} €{% else %}—{% endif %}</dd>
9
+ <dt>{{ __("donation.campaigns.raised") }}</dt><dd><strong>{{ (campaign.raised_cents / 100) | round(0) }} €</strong></dd>
10
+ <dt>{{ __("donation.campaigns.donors") }}</dt><dd>{{ campaign.donor_count }}</dd>
11
+ <dt>{{ __("donation.stats.last_sync") }}</dt><dd>{{ campaign.last_synced_at | date("PPpp") }}</dd>
12
+ </dl>
13
+ {% endcall %}
14
+
15
+ {% call section({ title: "Réglages locaux" }) %}
16
+ <p class="hint">
17
+ Ces réglages restent locaux à Indiekit (non envoyés à Stripe).
18
+ Pour modifier le titre, l'objectif ou les dates, éditer le Produit
19
+ directement dans Stripe Dashboard puis cliquer « Synchroniser ».
20
+ </p>
21
+ <form method="post" action="{{ baseUrl }}/campaigns/{{ campaign.stripe_product_id }}/edit">
22
+ <label>
23
+ <input name="hidden_on_public" type="checkbox" {% if campaign.hidden_on_public %}checked{% endif %}>
24
+ {{ __("donation.campaigns.hidden_on_public") }}
25
+ </label>
26
+ <label>
27
+ Ordre d'affichage
28
+ <input name="display_order" type="number" value="{{ campaign.display_order or 100 }}">
29
+ </label>
30
+ {{ button({ type: "submit", text: __("donation.save") }) }}
31
+ </form>
32
+ {% endcall %}
33
+ {% endblock %}
@@ -0,0 +1,24 @@
1
+ {% extends "layouts/donation.njk" %}
2
+
3
+ {% block donation %}
4
+ {% call section({ title: title }) %}
5
+ <p class="hint">{{ __("donation.campaigns.synced_from_stripe") }}.</p>
6
+ <table class="donation-table">
7
+ <thead>
8
+ <tr><th>Titre</th><th>État</th><th>{{ __("donation.campaigns.raised") }}</th><th>{{ __("donation.campaigns.goal") }}</th><th>{{ __("donation.campaigns.donors") }}</th><th></th></tr>
9
+ </thead>
10
+ <tbody>
11
+ {% for c in campaigns %}
12
+ <tr>
13
+ <td><strong>{{ c.title }}</strong></td>
14
+ <td>{% if c.active %}{{ __("donation.campaigns.active") }}{% else %}{{ __("donation.campaigns.archived") }}{% endif %}</td>
15
+ <td>{{ (c.raised_cents / 100) | round(0) }} €</td>
16
+ <td>{% if c.goal_cents %}{{ (c.goal_cents / 100) | round(0) }} €{% else %}—{% endif %}</td>
17
+ <td>{{ c.donor_count }}</td>
18
+ <td>{{ button({ href: baseUrl + "/campaigns/" + c.stripe_product_id, text: __("donation.edit"), classes: "button--small" }) }}</td>
19
+ </tr>
20
+ {% endfor %}
21
+ </tbody>
22
+ </table>
23
+ {% endcall %}
24
+ {% endblock %}
@@ -0,0 +1,100 @@
1
+ {% extends "layouts/donation.njk" %}
2
+
3
+ {% block donation %}
4
+
5
+ {% call section({ title: __("donation.stats.title") }) %}
6
+ <dl class="donation-stats">
7
+ <div class="donation-stat">
8
+ <dt>{{ __("donation.stats.lifetime_total") }}</dt>
9
+ <dd><strong>{{ (stats.lifetime_total_cents / 100) | round(0) }} €</strong></dd>
10
+ </div>
11
+ <div class="donation-stat">
12
+ <dt>{{ __("donation.stats.donation_count") }}</dt>
13
+ <dd>{{ stats.donation_count }}</dd>
14
+ </div>
15
+ <div class="donation-stat">
16
+ <dt>{{ __("donation.stats.donor_count") }}</dt>
17
+ <dd>{{ stats.donor_count }}</dd>
18
+ </div>
19
+ <div class="donation-stat">
20
+ <dt>{{ __("donation.stats.active_campaigns") }}</dt>
21
+ <dd>{{ stats.active_campaign_count }}</dd>
22
+ </div>
23
+ </dl>
24
+
25
+ <div class="donation-actions">
26
+ {{ button({ href: baseUrl + "/donations", text: __("donation.donations.title"), classes: "button--secondary" }) }}
27
+ {{ button({ href: baseUrl + "/campaigns", text: __("donation.campaigns.title"), classes: "button--secondary" }) }}
28
+ {{ button({ href: baseUrl + "/manual", text: __("donation.manual.action") }) }}
29
+ </div>
30
+ {% endcall %}
31
+
32
+ {% call section({ title: __("donation.actions.title") }) %}
33
+ <div class="donation-actions">
34
+ <form method="post" action="{{ baseUrl }}/sync" style="display: inline;">
35
+ {{ button({ type: "submit", text: __("donation.actions.sync_stripe"), classes: "button--secondary" }) }}
36
+ </form>
37
+ <form method="post" action="{{ baseUrl }}/rebuild" style="display: inline;">
38
+ {{ button({ type: "submit", text: __("donation.actions.trigger_rebuild"), classes: "button--secondary" }) }}
39
+ </form>
40
+ </div>
41
+ {% endcall %}
42
+
43
+ {% if activeCampaigns.length > 0 %}
44
+ {% call section({ title: __("donation.campaigns.active") }) %}
45
+ <ul class="donation-campaign-list">
46
+ {% for c in activeCampaigns %}
47
+ <li class="donation-campaign-row">
48
+ <a href="{{ baseUrl }}/campaigns/{{ c.stripe_product_id }}">
49
+ <strong>{{ c.title }}</strong>
50
+ </a>
51
+ <span class="donation-campaign-row__totals">
52
+ {{ (c.raised_cents / 100) | round(0) }} € / {{ (c.goal_cents / 100) | round(0) }} €
53
+ &middot; {{ c.donor_count }} {{ __("donation.campaigns.donors") | lower }}
54
+ </span>
55
+ </li>
56
+ {% endfor %}
57
+ </ul>
58
+ {% endcall %}
59
+ {% endif %}
60
+
61
+ {% call section({ title: __("donation.donations.title") + " — récents" }) %}
62
+ {% if recent.length == 0 %}
63
+ <p>{{ __("donation.donations.empty") }}</p>
64
+ {% else %}
65
+ <table class="donation-table">
66
+ <thead>
67
+ <tr>
68
+ <th>Date</th>
69
+ <th>Montant</th>
70
+ <th>Donateur</th>
71
+ <th>Campagne</th>
72
+ <th>Source</th>
73
+ <th></th>
74
+ </tr>
75
+ </thead>
76
+ <tbody>
77
+ {% for d in recent %}
78
+ <tr>
79
+ <td>{{ d.date | date("PPpp") }}</td>
80
+ <td>{{ (d.amount_cents / 100) | round(0) }} {{ d.currency }}</td>
81
+ <td>
82
+ {% if d.donor.consent_public and d.donor.display %}
83
+ {{ d.donor.display }}
84
+ {% elif d.donor.consent_public %}
85
+ <em>Anonyme</em>
86
+ {% else %}
87
+ <em>Masqué</em>
88
+ {% endif %}
89
+ </td>
90
+ <td>{{ d.campaign_id }}</td>
91
+ <td><code>{{ d.source }}</code></td>
92
+ <td>{{ button({ href: baseUrl + "/donations/" + d.stripe_id, text: __("donation.edit"), classes: "button--small button--secondary" }) }}</td>
93
+ </tr>
94
+ {% endfor %}
95
+ </tbody>
96
+ </table>
97
+ {% endif %}
98
+ {% endcall %}
99
+
100
+ {% endblock %}
@@ -0,0 +1,37 @@
1
+ {% extends "layouts/donation.njk" %}
2
+
3
+ {% block donation %}
4
+ {% call section({ title: donation.stripe_id }) %}
5
+ <dl>
6
+ <dt>Date</dt><dd>{{ donation.date | date("PPpp") }}</dd>
7
+ <dt>Montant</dt><dd>{{ (donation.amount_cents / 100) | round(0) }} {{ donation.currency }}</dd>
8
+ <dt>Campagne</dt><dd>{{ donation.campaign_id }}</dd>
9
+ <dt>Source</dt><dd><code>{{ donation.source }}</code></dd>
10
+ </dl>
11
+ {% endcall %}
12
+
13
+ {% call section({ title: "Donateur" }) %}
14
+ <form method="post" action="{{ baseUrl }}/donations/{{ donation.stripe_id }}/edit">
15
+ <label>
16
+ Nom affiché
17
+ <input name="display" type="text" value="{{ donation.donor.display or '' }}">
18
+ </label>
19
+ <label>
20
+ Message
21
+ <textarea name="message" rows="3">{{ donation.donor.message or '' }}</textarea>
22
+ </label>
23
+ <label>
24
+ <input name="consent_public" type="checkbox" {% if donation.donor.consent_public %}checked{% endif %}>
25
+ Afficher publiquement
26
+ </label>
27
+ {{ button({ type: "submit", text: __("donation.save") }) }}
28
+ </form>
29
+ {% endcall %}
30
+
31
+ {% call section({ title: "Actions" }) %}
32
+ <form method="post" action="{{ baseUrl }}/donations/{{ donation.stripe_id }}/delete"
33
+ onsubmit="return confirm('{{ __("donation.confirm") }}');" style="display: inline;">
34
+ {{ button({ type: "submit", text: __("donation.delete"), classes: "button--warning" }) }}
35
+ </form>
36
+ {% endcall %}
37
+ {% endblock %}
@@ -0,0 +1,29 @@
1
+ {% extends "layouts/donation.njk" %}
2
+
3
+ {% block donation %}
4
+ {% call section({ title: title }) %}
5
+ {# TODO: filter controls (campaign, source, date range) #}
6
+ <table class="donation-table">
7
+ <thead>
8
+ <tr>
9
+ <th>Date</th><th>Montant</th><th>Donateur</th><th>Campagne</th><th>Source</th><th>Consentement</th><th></th>
10
+ </tr>
11
+ </thead>
12
+ <tbody>
13
+ {% for d in donations %}
14
+ <tr>
15
+ <td>{{ d.date | date("yyyy-MM-dd HH:mm") }}</td>
16
+ <td>{{ (d.amount_cents / 100) | round(0) }} {{ d.currency }}</td>
17
+ <td>
18
+ {% if d.donor.display %}{{ d.donor.display }}{% else %}<em>Anonyme</em>{% endif %}
19
+ </td>
20
+ <td>{{ d.campaign_id }}</td>
21
+ <td><code>{{ d.source }}</code></td>
22
+ <td>{% if d.donor.consent_public %}{{ __("donation.consent.public") }}{% else %}{{ __("donation.consent.hidden") }}{% endif %}</td>
23
+ <td>{{ button({ href: baseUrl + "/donations/" + d.stripe_id, text: __("donation.edit"), classes: "button--small" }) }}</td>
24
+ </tr>
25
+ {% endfor %}
26
+ </tbody>
27
+ </table>
28
+ {% endcall %}
29
+ {% endblock %}
@@ -0,0 +1,8 @@
1
+ {% extends "layouts/donation.njk" %}
2
+
3
+ {% block donation %}
4
+ {% call section({ title: title }) %}
5
+ <p class="error">{{ message }}</p>
6
+ {{ button({ href: baseUrl + "/manual", text: __("donation.cancel"), classes: "button--secondary" }) }}
7
+ {% endcall %}
8
+ {% endblock %}
@@ -0,0 +1,45 @@
1
+ {% extends "layouts/donation.njk" %}
2
+
3
+ {% block donation %}
4
+ {% call section({ title: __("donation.manual.title") }) %}
5
+ <p class="hint">
6
+ Pour les dons reçus hors Stripe (virement bancaire, espèces lors d'un évènement, etc.).
7
+ Ces dons s'ajoutent aux statistiques publiques mais portent la source <code>manual</code>.
8
+ </p>
9
+ <form method="post" action="{{ baseUrl }}/manual">
10
+ <label>
11
+ {{ __("donation.manual.fields.amount") }}
12
+ <input name="amount" type="number" step="0.01" min="0.01" required>
13
+ </label>
14
+ <label>
15
+ {{ __("donation.manual.fields.campaign") }}
16
+ <select name="campaign_id" required>
17
+ {% for c in campaigns %}
18
+ <option value="{{ c.stripe_product_id }}">{{ c.title }}</option>
19
+ {% endfor %}
20
+ </select>
21
+ </label>
22
+ <label>
23
+ {{ __("donation.manual.fields.date") }}
24
+ <input name="date" type="date" required>
25
+ </label>
26
+ <label>
27
+ {{ __("donation.manual.fields.donor_display") }}
28
+ <input name="donor_display" type="text">
29
+ </label>
30
+ <label>
31
+ {{ __("donation.manual.fields.message") }}
32
+ <textarea name="message" rows="2"></textarea>
33
+ </label>
34
+ <label>
35
+ <input name="consent_public" type="checkbox" checked>
36
+ {{ __("donation.manual.fields.consent_public") }}
37
+ </label>
38
+ <label>
39
+ {{ __("donation.manual.fields.note") }}
40
+ <input name="note" type="text">
41
+ </label>
42
+ {{ button({ type: "submit", text: __("donation.manual.action") }) }}
43
+ </form>
44
+ {% endcall %}
45
+ {% endblock %}
@@ -0,0 +1,8 @@
1
+ {% extends "layouts/donation.njk" %}
2
+
3
+ {% block donation %}
4
+ {% call section({ title: "Introuvable" }) %}
5
+ <p>Cette entrée n'existe pas ou a été supprimée.</p>
6
+ {{ button({ href: baseUrl, text: "Retour au tableau de bord", classes: "button--secondary" }) }}
7
+ {% endcall %}
8
+ {% endblock %}
@@ -0,0 +1,14 @@
1
+ {# Shared layout for all donation admin pages.
2
+ Extends indiekit's global admin layout — inheriting nav, auth chrome,
3
+ notification banners. The {% block donation %} child fills the body.
4
+ #}
5
+ {% extends "layouts/application.njk" %}
6
+
7
+ {% block content %}
8
+ <header class="page-header">
9
+ <h1 class="page-header__heading">{{ title }}</h1>
10
+ {% if subtitle %}<p class="page-header__description">{{ subtitle }}</p>{% endif %}
11
+ </header>
12
+
13
+ {% block donation %}{% endblock %}
14
+ {% endblock %}