wiki-plugin-salon 0.0.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.
@@ -0,0 +1,198 @@
1
+ // wiki-plugin-salon — client-side plugin
2
+ // Renders salon items in the fedwiki journal.
3
+ // Item data: { type: 'salon', text } — text is the display label for the salon link.
4
+
5
+ window.plugins = window.plugins || {};
6
+
7
+ window.plugins.salon = (function() {
8
+
9
+ const color = {
10
+ gray: '#ccc',
11
+ red: '#f55',
12
+ yellow: '#fb0',
13
+ green: '#0e0'
14
+ };
15
+
16
+ // Mode labels shown in the status badge
17
+ const MODE_LABEL = {
18
+ open: 'Open',
19
+ grant: 'By Approval',
20
+ closed: 'Closed'
21
+ };
22
+
23
+ const MODE_COLOR = {
24
+ open: '#10b981', // green
25
+ grant: '#f59e0b', // amber
26
+ closed: '#6b7280' // gray
27
+ };
28
+
29
+ // ── Emit: render the salon item ──────────────────────────────────────────────
30
+ function emit($item, item) {
31
+ const label = item.text || 'The Salon';
32
+
33
+ // Fetch public config to show mode + member count (credentials for owner check)
34
+ fetch('/plugin/salon/config', { credentials: 'include' })
35
+ .then(r => r.json())
36
+ .then(data => {
37
+ const mode = data.registrationMode || 'open';
38
+ const title = data.title || 'The Salon';
39
+ const description = data.description || '';
40
+ const memberCount = data.activeCount || 0;
41
+ const modeColor = MODE_COLOR[mode] || MODE_COLOR.open;
42
+ const modeLabel = MODE_LABEL[mode] || mode;
43
+
44
+ const registerHtml = mode === 'closed'
45
+ ? `<span style="color:#6b7280;font-size:0.8rem;">Registration closed</span>`
46
+ : `<a href="/plugin/salon/register" target="_blank" style="
47
+ color:#c4b5fd;font-size:0.8rem;text-decoration:none;
48
+ border:1px solid rgba(196,181,253,0.3);border-radius:4px;
49
+ padding:3px 8px;
50
+ ">Join</a>`;
51
+
52
+ const $card = $(`
53
+ <div class="salon-item" style="
54
+ background: linear-gradient(135deg, rgba(124,58,237,0.08) 0%, rgba(109,40,217,0.04) 100%);
55
+ border: 1px solid rgba(196,181,253,0.2);
56
+ border-radius: 10px;
57
+ padding: 14px 16px;
58
+ font-family: system-ui;
59
+ ">
60
+ <div style="display:flex;align-items:center;gap:10px;margin-bottom:8px;">
61
+ <span style="font-size:1.3rem;">🏛️</span>
62
+ <a href="/plugin/salon" target="_blank" style="
63
+ color:#c4b5fd;font-weight:600;font-size:1rem;text-decoration:none;
64
+ ">${escapeHtml(title)}</a>
65
+ <span style="
66
+ background:${modeColor}22;
67
+ color:${modeColor};
68
+ border:1px solid ${modeColor}44;
69
+ border-radius:4px;
70
+ font-size:0.7rem;
71
+ padding:2px 6px;
72
+ font-weight:600;
73
+ letter-spacing:0.03em;
74
+ ">${modeLabel}</span>
75
+ </div>
76
+ ${description ? `<div style="font-size:0.82rem;color:#a0a0a0;margin-bottom:8px;line-height:1.4;">${escapeHtml(description)}</div>` : ''}
77
+ <div style="display:flex;align-items:center;justify-content:space-between;">
78
+ <span style="font-size:0.8rem;color:#9ca3af;">
79
+ ${memberCount} ${memberCount === 1 ? 'member' : 'members'}
80
+ </span>
81
+ ${registerHtml}
82
+ </div>
83
+ </div>
84
+ `);
85
+ $item.append($card);
86
+
87
+ if (data.isOwner) {
88
+ const configDiv = document.createElement('div');
89
+ configDiv.style.cssText = 'border:1px solid rgba(196,181,253,0.2);border-radius:8px;padding:12px;margin-top:8px;background:rgba(124,58,237,0.06);font-family:system-ui;';
90
+ configDiv.innerHTML = `
91
+ <div style="font-size:0.8rem;color:#c4b5fd;margin-bottom:8px;font-weight:600;">Allyabase connection (owner only)</div>
92
+ <div style="display:flex;gap:6px;align-items:center;">
93
+ <input id="salon-url-input" value="${escapeHtml(data.allyabaseUrl || '')}" placeholder="https://dev.allyabase.com"
94
+ style="flex:1;background:#0d001a;border:1px solid rgba(196,181,253,0.3);border-radius:4px;padding:6px 8px;color:#e0d0ff;font-size:0.8rem;">
95
+ <button id="salon-url-btn"
96
+ style="background:#7c3aed;border:none;border-radius:4px;padding:6px 12px;color:white;cursor:pointer;font-size:0.8rem;white-space:nowrap;">
97
+ Save
98
+ </button>
99
+ </div>
100
+ <div id="salon-url-status" style="margin-top:6px;font-size:0.75rem;"></div>`;
101
+ $item.append(configDiv);
102
+ configDiv.querySelector('#salon-url-btn').addEventListener('click', function() {
103
+ const input = configDiv.querySelector('#salon-url-input');
104
+ const btn = configDiv.querySelector('#salon-url-btn');
105
+ const status = configDiv.querySelector('#salon-url-status');
106
+ const base = (input.value || '').trim().replace(/\/$/, '');
107
+ if (!base) { status.textContent = 'Enter an allyabase URL first'; return; }
108
+ btn.disabled = true; btn.textContent = 'Saving…';
109
+ fetch('/plugin/salon/config', {
110
+ method: 'POST',
111
+ credentials: 'include',
112
+ headers: { 'Content-Type': 'application/json' },
113
+ body: JSON.stringify({ allyabaseUrl: base })
114
+ })
115
+ .then(r => r.json())
116
+ .then(d => {
117
+ if (d.error) { status.innerHTML = '<span style="color:#f55;">' + escapeHtml(d.error) + '</span>'; return; }
118
+ status.innerHTML = '<span style="color:#0e0;">Saved</span>';
119
+ })
120
+ .catch(e => { status.innerHTML = '<span style="color:#f55;">' + escapeHtml(e.message) + '</span>'; })
121
+ .finally(() => { btn.disabled = false; btn.textContent = 'Save'; });
122
+ });
123
+ }
124
+ })
125
+ .catch(() => {
126
+ // Salon not configured or unreachable
127
+ $item.append(`
128
+ <div style="
129
+ background: rgba(124,58,237,0.06);
130
+ border: 2px dashed rgba(196,181,253,0.25);
131
+ border-radius: 10px;
132
+ padding: 16px;
133
+ text-align: center;
134
+ color: #c4b5fd;
135
+ font-family: system-ui;
136
+ ">
137
+ <div style="font-size:1.8rem;margin-bottom:6px;">🏛️</div>
138
+ <div style="font-weight:600;">${escapeHtml(label)}</div>
139
+ <div style="font-size:0.82rem;margin-top:6px;opacity:0.6;">Salon not available</div>
140
+ </div>
141
+ `);
142
+ });
143
+ }
144
+
145
+ // ── Bind: owner controls ──────────────────────────────────────────────────────
146
+ function bind($item, item) {
147
+ // Status indicator (traffic-light ◉ — consistent with lucille)
148
+ const $status = $('<span>').text('◉ ').css({ color: color.gray, cursor: 'pointer' })
149
+ .attr('title', 'Salon status');
150
+
151
+ $item.find('.item-buttons').prepend($status);
152
+
153
+ fetch('/plugin/salon/config')
154
+ .then(r => r.json())
155
+ .then(data => {
156
+ const mode = data.registrationMode || 'open';
157
+ const pendingCount = data.pendingCount || 0;
158
+
159
+ if (mode === 'closed') {
160
+ $status.css('color', color.red).attr('title', 'Salon: registration closed');
161
+ } else if (mode === 'grant' && pendingCount > 0) {
162
+ $status.css('color', color.yellow)
163
+ .attr('title', `Salon: ${pendingCount} pending approval`);
164
+ } else {
165
+ $status.css('color', color.green)
166
+ .attr('title', `Salon: ${MODE_LABEL[mode] || mode}`);
167
+ }
168
+
169
+ // Clicking the indicator opens the admin panel
170
+ $status.on('click', () => {
171
+ window.open('/plugin/salon/admin', '_blank');
172
+ });
173
+
174
+ // Pending badge next to indicator
175
+ if (pendingCount > 0) {
176
+ const $badge = $(`<span style="
177
+ background:#7c3aed;color:#fff;
178
+ border-radius:9px;font-size:0.7rem;
179
+ padding:1px 6px;margin-left:2px;
180
+ cursor:pointer;
181
+ ">${pendingCount}</span>`).attr('title', 'Open admin to review pending members')
182
+ .on('click', () => window.open('/plugin/salon/admin', '_blank'));
183
+ $item.find('.item-buttons').prepend($badge);
184
+ }
185
+ })
186
+ .catch(() => {
187
+ $status.css('color', color.red).attr('title', 'Salon: could not connect');
188
+ });
189
+ }
190
+
191
+ // ── Helpers ──────────────────────────────────────────────────────────────────
192
+ function escapeHtml(str) {
193
+ return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
194
+ }
195
+
196
+ return { emit, bind };
197
+
198
+ }());
package/factory.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "salon",
3
+ "creator": "Planet Nine",
4
+ "category": "community",
5
+ "type": "salon",
6
+ "description": "A community gathering space on your wiki. Users can register as members and receive updates from agora, lucille, and other freyja plugins."
7
+ }
package/index.js ADDED
@@ -0,0 +1,4 @@
1
+ (function() {
2
+ const { startServer } = require('./server/server.js');
3
+ module.exports = { startServer };
4
+ }).call(this);
package/package.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "wiki-plugin-salon",
3
+ "version": "0.0.1",
4
+ "description": "Federated Wiki plugin — community gathering space with self-registration and plugin event feeds",
5
+ "keywords": ["wiki", "plugin", "salon", "community", "planet-nine"],
6
+ "author": "Planet Nine",
7
+ "license": "MIT",
8
+ "type": "commonjs",
9
+ "main": "index.js",
10
+ "dependencies": {
11
+ "nodemailer": "^6.9.0"
12
+ }
13
+ }