linkitylink 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.
- package/CLAUDE.md +238 -0
- package/Dockerfile +20 -0
- package/Dockerfile.local +24 -0
- package/LICENSE +674 -0
- package/MANUAL-TESTING.md +399 -0
- package/README.md +119 -0
- package/TEMPLATE-FEDERATION-GO-LIVE.md +269 -0
- package/USER-TESTING-GUIDE.md +420 -0
- package/docker-compose.standalone.yml +14 -0
- package/docker-compose.yml +42 -0
- package/lib/app-handoff.js +315 -0
- package/lib/relevant-bdos-middleware.js +381 -0
- package/package.json +33 -0
- package/public/create.html +1468 -0
- package/public/index.html +117 -0
- package/public/moderate.html +465 -0
- package/public/my-tapestries.html +351 -0
- package/public/relevant-bdos.js +267 -0
- package/public/styles.css +1004 -0
- package/server.js +2914 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Glyphenge - You've Got Places to Go</title>
|
|
7
|
+
<link rel="stylesheet" href="/styles.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div class="landing-container">
|
|
11
|
+
<!-- Left Side: Example Card -->
|
|
12
|
+
<div class="left-panel">
|
|
13
|
+
<h1 class="tagline">You've got places to go.</h1>
|
|
14
|
+
<div class="example-card">
|
|
15
|
+
<svg viewBox="0 0 600 700" xmlns="http://www.w3.org/2000/svg">
|
|
16
|
+
<!-- Sunset gradient background -->
|
|
17
|
+
<defs>
|
|
18
|
+
<linearGradient id="sunsetGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
19
|
+
<stop offset="0%" style="stop-color:#ff6b6b;stop-opacity:1" />
|
|
20
|
+
<stop offset="50%" style="stop-color:#feca57;stop-opacity:1" />
|
|
21
|
+
<stop offset="100%" style="stop-color:#ee5a6f;stop-opacity:1" />
|
|
22
|
+
</linearGradient>
|
|
23
|
+
|
|
24
|
+
<!-- Link card gradients -->
|
|
25
|
+
<linearGradient id="linkGrad1" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
26
|
+
<stop offset="0%" style="stop-color:#10b981;stop-opacity:1" />
|
|
27
|
+
<stop offset="100%" style="stop-color:#059669;stop-opacity:1" />
|
|
28
|
+
</linearGradient>
|
|
29
|
+
<linearGradient id="linkGrad2" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
30
|
+
<stop offset="0%" style="stop-color:#3b82f6;stop-opacity:1" />
|
|
31
|
+
<stop offset="100%" style="stop-color:#2563eb;stop-opacity:1" />
|
|
32
|
+
</linearGradient>
|
|
33
|
+
<linearGradient id="linkGrad3" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
34
|
+
<stop offset="0%" style="stop-color:#8b5cf6;stop-opacity:1" />
|
|
35
|
+
<stop offset="100%" style="stop-color:#7c3aed;stop-opacity:1" />
|
|
36
|
+
</linearGradient>
|
|
37
|
+
<linearGradient id="linkGrad4" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
38
|
+
<stop offset="0%" style="stop-color:#ec4899;stop-opacity:1" />
|
|
39
|
+
<stop offset="100%" style="stop-color:#db2777;stop-opacity:1" />
|
|
40
|
+
</linearGradient>
|
|
41
|
+
</defs>
|
|
42
|
+
|
|
43
|
+
<!-- Background -->
|
|
44
|
+
<rect width="600" height="700" fill="url(#sunsetGradient)" rx="20"/>
|
|
45
|
+
|
|
46
|
+
<!-- Title -->
|
|
47
|
+
<text x="300" y="80" font-family="system-ui, -apple-system, sans-serif" font-size="32" font-weight="bold" fill="white" text-anchor="middle">
|
|
48
|
+
Planet Nine Awesome
|
|
49
|
+
</text>
|
|
50
|
+
|
|
51
|
+
<!-- Link Cards -->
|
|
52
|
+
<!-- Card 1: GitHub -->
|
|
53
|
+
<g transform="translate(50, 140)">
|
|
54
|
+
<rect width="500" height="80" fill="url(#linkGrad1)" rx="12" opacity="0.95"/>
|
|
55
|
+
<circle cx="40" cy="40" r="25" fill="white" opacity="0.3"/>
|
|
56
|
+
<text x="80" y="35" font-family="system-ui" font-size="16" font-weight="600" fill="white">GitHub</text>
|
|
57
|
+
<text x="80" y="55" font-family="system-ui" font-size="14" fill="white" opacity="0.9">github.com/planet-nine</text>
|
|
58
|
+
<text x="460" y="45" font-family="system-ui" font-size="24" fill="white" opacity="0.6">→</text>
|
|
59
|
+
</g>
|
|
60
|
+
|
|
61
|
+
<!-- Card 2: The Advancement -->
|
|
62
|
+
<g transform="translate(50, 240)">
|
|
63
|
+
<rect width="500" height="80" fill="url(#linkGrad2)" rx="12" opacity="0.95"/>
|
|
64
|
+
<circle cx="40" cy="40" r="25" fill="white" opacity="0.3"/>
|
|
65
|
+
<text x="80" y="35" font-family="system-ui" font-size="16" font-weight="600" fill="white">The Advancement</text>
|
|
66
|
+
<text x="80" y="55" font-family="system-ui" font-size="14" fill="white" opacity="0.9">Privacy-first browser extension</text>
|
|
67
|
+
<text x="460" y="45" font-family="system-ui" font-size="24" fill="white" opacity="0.6">→</text>
|
|
68
|
+
</g>
|
|
69
|
+
|
|
70
|
+
<!-- Card 3: The Nullary -->
|
|
71
|
+
<g transform="translate(50, 340)">
|
|
72
|
+
<rect width="500" height="80" fill="url(#linkGrad3)" rx="12" opacity="0.95"/>
|
|
73
|
+
<circle cx="40" cy="40" r="25" fill="white" opacity="0.3"/>
|
|
74
|
+
<text x="80" y="35" font-family="system-ui" font-size="16" font-weight="600" fill="white">The Nullary</text>
|
|
75
|
+
<text x="80" y="55" font-family="system-ui" font-size="14" fill="white" opacity="0.9">SVG-first social apps</text>
|
|
76
|
+
<text x="460" y="45" font-family="system-ui" font-size="24" fill="white" opacity="0.6">→</text>
|
|
77
|
+
</g>
|
|
78
|
+
|
|
79
|
+
<!-- Card 4: Glyphenge -->
|
|
80
|
+
<g transform="translate(50, 440)">
|
|
81
|
+
<rect width="500" height="80" fill="url(#linkGrad4)" rx="12" opacity="0.95"/>
|
|
82
|
+
<circle cx="40" cy="40" r="25" fill="white" opacity="0.3"/>
|
|
83
|
+
<text x="80" y="35" font-family="system-ui" font-size="16" font-weight="600" fill="white">Glyphenge</text>
|
|
84
|
+
<text x="80" y="55" font-family="system-ui" font-size="14" fill="white" opacity="0.9">Beautiful link tapestries</text>
|
|
85
|
+
<text x="460" y="45" font-family="system-ui" font-size="24" fill="white" opacity="0.6">→</text>
|
|
86
|
+
</g>
|
|
87
|
+
|
|
88
|
+
<!-- Footer emoji -->
|
|
89
|
+
<text x="300" y="650" font-size="48" text-anchor="middle" opacity="0.7">✨</text>
|
|
90
|
+
</svg>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<!-- Right Side: Call to Action -->
|
|
95
|
+
<div class="right-panel">
|
|
96
|
+
<h2 class="cta-text">Let us make them...</h2>
|
|
97
|
+
<div class="button-grid">
|
|
98
|
+
<button class="style-button stunning" onclick="selectStyle('stunning')">Stunning</button>
|
|
99
|
+
<button class="style-button dazzling" onclick="selectStyle('dazzling')">Dazzling</button>
|
|
100
|
+
<button class="style-button electric" onclick="selectStyle('electric')">Electric</button>
|
|
101
|
+
<button class="style-button polished" onclick="selectStyle('polished')">Polished</button>
|
|
102
|
+
<button class="style-button professional" onclick="selectStyle('professional')">Professional</button>
|
|
103
|
+
<button class="style-button captivating" onclick="selectStyle('captivating')">Captivating</button>
|
|
104
|
+
<button class="style-button delightful" onclick="selectStyle('delightful')">Delightful</button>
|
|
105
|
+
<button class="style-button magical" onclick="selectStyle('magical')">Magical</button>
|
|
106
|
+
<button class="style-button basic" onclick="selectStyle('basic')">Basic</button>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<script>
|
|
112
|
+
function selectStyle(style) {
|
|
113
|
+
window.location.href = `/create?style=${style}`;
|
|
114
|
+
}
|
|
115
|
+
</script>
|
|
116
|
+
</body>
|
|
117
|
+
</html>
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Moderate Templates - Linkitylink Admin</title>
|
|
7
|
+
<link rel="stylesheet" href="/styles.css">
|
|
8
|
+
<style>
|
|
9
|
+
body {
|
|
10
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
11
|
+
min-height: 100vh;
|
|
12
|
+
padding: 40px 20px;
|
|
13
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.admin-container {
|
|
17
|
+
max-width: 1200px;
|
|
18
|
+
margin: 0 auto;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.admin-header {
|
|
22
|
+
text-align: center;
|
|
23
|
+
margin-bottom: 40px;
|
|
24
|
+
color: white;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.admin-header h1 {
|
|
28
|
+
font-size: 2.5rem;
|
|
29
|
+
margin-bottom: 10px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.admin-header p {
|
|
33
|
+
color: #94a3b8;
|
|
34
|
+
font-size: 1.1rem;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.template-grid {
|
|
38
|
+
display: grid;
|
|
39
|
+
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
|
40
|
+
gap: 30px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.template-card {
|
|
44
|
+
background: white;
|
|
45
|
+
border-radius: 15px;
|
|
46
|
+
padding: 20px;
|
|
47
|
+
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.template-preview {
|
|
51
|
+
width: 100%;
|
|
52
|
+
height: 200px;
|
|
53
|
+
margin-bottom: 15px;
|
|
54
|
+
border-radius: 10px;
|
|
55
|
+
overflow: hidden;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.template-info h3 {
|
|
59
|
+
margin: 0 0 10px 0;
|
|
60
|
+
color: #333;
|
|
61
|
+
font-size: 1.3rem;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.template-meta {
|
|
65
|
+
display: flex;
|
|
66
|
+
flex-direction: column;
|
|
67
|
+
gap: 8px;
|
|
68
|
+
margin-bottom: 15px;
|
|
69
|
+
font-size: 0.9rem;
|
|
70
|
+
color: #666;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.template-meta-row {
|
|
74
|
+
display: flex;
|
|
75
|
+
justify-content: space-between;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.template-meta label {
|
|
79
|
+
font-weight: 600;
|
|
80
|
+
color: #333;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.color-swatches {
|
|
84
|
+
display: flex;
|
|
85
|
+
gap: 5px;
|
|
86
|
+
margin-top: 10px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.color-swatch {
|
|
90
|
+
width: 30px;
|
|
91
|
+
height: 30px;
|
|
92
|
+
border-radius: 5px;
|
|
93
|
+
border: 2px solid white;
|
|
94
|
+
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.template-actions {
|
|
98
|
+
display: flex;
|
|
99
|
+
gap: 10px;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.approve-btn, .reject-btn {
|
|
103
|
+
flex: 1;
|
|
104
|
+
padding: 12px;
|
|
105
|
+
border: none;
|
|
106
|
+
border-radius: 8px;
|
|
107
|
+
font-weight: 600;
|
|
108
|
+
font-size: 1rem;
|
|
109
|
+
cursor: pointer;
|
|
110
|
+
transition: all 0.2s;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.approve-btn {
|
|
114
|
+
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
|
115
|
+
color: white;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.approve-btn:hover {
|
|
119
|
+
transform: translateY(-2px);
|
|
120
|
+
box-shadow: 0 6px 20px rgba(16, 185, 129, 0.4);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.reject-btn {
|
|
124
|
+
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
|
125
|
+
color: white;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.reject-btn:hover {
|
|
129
|
+
transform: translateY(-2px);
|
|
130
|
+
box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.approve-btn:disabled, .reject-btn:disabled {
|
|
134
|
+
opacity: 0.5;
|
|
135
|
+
cursor: not-allowed;
|
|
136
|
+
transform: none;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.empty-state {
|
|
140
|
+
text-align: center;
|
|
141
|
+
padding: 60px 20px;
|
|
142
|
+
color: white;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.empty-state svg {
|
|
146
|
+
font-size: 4rem;
|
|
147
|
+
margin-bottom: 20px;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.empty-state h2 {
|
|
151
|
+
font-size: 1.8rem;
|
|
152
|
+
margin-bottom: 10px;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.empty-state p {
|
|
156
|
+
color: #94a3b8;
|
|
157
|
+
font-size: 1.1rem;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.error-message {
|
|
161
|
+
background: #fee2e2;
|
|
162
|
+
color: #991b1b;
|
|
163
|
+
padding: 15px;
|
|
164
|
+
border-radius: 10px;
|
|
165
|
+
margin-bottom: 20px;
|
|
166
|
+
text-align: center;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.loading {
|
|
170
|
+
text-align: center;
|
|
171
|
+
color: white;
|
|
172
|
+
font-size: 1.2rem;
|
|
173
|
+
padding: 40px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.pubkey-input {
|
|
177
|
+
max-width: 600px;
|
|
178
|
+
margin: 0 auto 40px;
|
|
179
|
+
background: white;
|
|
180
|
+
padding: 20px;
|
|
181
|
+
border-radius: 15px;
|
|
182
|
+
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.pubkey-input label {
|
|
186
|
+
display: block;
|
|
187
|
+
margin-bottom: 10px;
|
|
188
|
+
font-weight: 600;
|
|
189
|
+
color: #333;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.pubkey-input input {
|
|
193
|
+
width: 100%;
|
|
194
|
+
padding: 12px;
|
|
195
|
+
border: 2px solid #e5e7eb;
|
|
196
|
+
border-radius: 8px;
|
|
197
|
+
font-family: monospace;
|
|
198
|
+
font-size: 0.9rem;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.pubkey-input input:focus {
|
|
202
|
+
outline: none;
|
|
203
|
+
border-color: #10b981;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.pubkey-input button {
|
|
207
|
+
width: 100%;
|
|
208
|
+
margin-top: 10px;
|
|
209
|
+
padding: 12px;
|
|
210
|
+
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
|
211
|
+
color: white;
|
|
212
|
+
border: none;
|
|
213
|
+
border-radius: 8px;
|
|
214
|
+
font-weight: 600;
|
|
215
|
+
font-size: 1rem;
|
|
216
|
+
cursor: pointer;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.pubkey-input button:hover {
|
|
220
|
+
transform: translateY(-2px);
|
|
221
|
+
box-shadow: 0 6px 20px rgba(16, 185, 129, 0.4);
|
|
222
|
+
}
|
|
223
|
+
</style>
|
|
224
|
+
</head>
|
|
225
|
+
<body>
|
|
226
|
+
<div class="admin-container">
|
|
227
|
+
<div class="admin-header">
|
|
228
|
+
<h1>Template Moderation</h1>
|
|
229
|
+
<p>Review and approve user-submitted templates</p>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<div class="pubkey-input">
|
|
233
|
+
<label for="admin-pubkey">Admin Public Key</label>
|
|
234
|
+
<input
|
|
235
|
+
type="text"
|
|
236
|
+
id="admin-pubkey"
|
|
237
|
+
placeholder="Enter your public key to authenticate"
|
|
238
|
+
autocomplete="off"
|
|
239
|
+
>
|
|
240
|
+
<button onclick="loadPendingTemplates()">Load Pending Templates</button>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<div id="error-container"></div>
|
|
244
|
+
<div id="template-container"></div>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<script>
|
|
248
|
+
let adminPubKey = '';
|
|
249
|
+
|
|
250
|
+
// Generate template preview SVG
|
|
251
|
+
function generateTemplateSVG(template) {
|
|
252
|
+
const gradientStops = template.colors.map((color, index) => {
|
|
253
|
+
const offset = (index / (template.colors.length - 1)) * 100;
|
|
254
|
+
return `<stop offset="${offset}%" style="stop-color:${color};stop-opacity:1" />`;
|
|
255
|
+
}).join('');
|
|
256
|
+
|
|
257
|
+
const linkCards = template.linkColors.slice(0, 2).map((color, i) => {
|
|
258
|
+
const y = 140 + i * 90;
|
|
259
|
+
return `
|
|
260
|
+
<g>
|
|
261
|
+
<rect x="50" y="${y}" width="500" height="70" fill="${color}" rx="10" opacity="0.9"/>
|
|
262
|
+
<text x="80" y="${y + 30}" font-family="system-ui" font-size="14" font-weight="600" fill="white">Sample Link ${i + 1}</text>
|
|
263
|
+
<text x="80" y="${y + 50}" font-family="system-ui" font-size="12" fill="white" opacity="0.8">example.com</text>
|
|
264
|
+
</g>
|
|
265
|
+
`;
|
|
266
|
+
}).join('');
|
|
267
|
+
|
|
268
|
+
return `
|
|
269
|
+
<svg viewBox="0 0 600 400" xmlns="http://www.w3.org/2000/svg">
|
|
270
|
+
<defs>
|
|
271
|
+
<linearGradient id="bg-${template.emojicode}" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
272
|
+
${gradientStops}
|
|
273
|
+
</linearGradient>
|
|
274
|
+
</defs>
|
|
275
|
+
<rect width="600" height="400" fill="url(#bg-${template.emojicode})" rx="15"/>
|
|
276
|
+
<text x="300" y="80" font-family="system-ui" font-size="24" font-weight="bold" fill="white" text-anchor="middle">
|
|
277
|
+
${escapeXML(template.name)}
|
|
278
|
+
</text>
|
|
279
|
+
${linkCards}
|
|
280
|
+
</svg>
|
|
281
|
+
`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function escapeXML(str) {
|
|
285
|
+
return String(str)
|
|
286
|
+
.replace(/&/g, '&')
|
|
287
|
+
.replace(/</g, '<')
|
|
288
|
+
.replace(/>/g, '>')
|
|
289
|
+
.replace(/"/g, '"')
|
|
290
|
+
.replace(/'/g, ''');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function loadPendingTemplates() {
|
|
294
|
+
adminPubKey = document.getElementById('admin-pubkey').value.trim();
|
|
295
|
+
|
|
296
|
+
if (!adminPubKey) {
|
|
297
|
+
showError('Please enter your admin public key');
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const container = document.getElementById('template-container');
|
|
302
|
+
const errorContainer = document.getElementById('error-container');
|
|
303
|
+
errorContainer.innerHTML = '';
|
|
304
|
+
container.innerHTML = '<div class="loading">Loading pending templates...</div>';
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
const response = await fetch(`/templates/pending?pubKey=${encodeURIComponent(adminPubKey)}`);
|
|
308
|
+
|
|
309
|
+
if (response.status === 403) {
|
|
310
|
+
showError('Unauthorized - You do not have admin nineum');
|
|
311
|
+
container.innerHTML = '';
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (!response.ok) {
|
|
316
|
+
throw new Error(`HTTP ${response.status}`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const data = await response.json();
|
|
320
|
+
|
|
321
|
+
if (!data.success) {
|
|
322
|
+
throw new Error(data.error || 'Failed to load templates');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (data.count === 0) {
|
|
326
|
+
container.innerHTML = `
|
|
327
|
+
<div class="empty-state">
|
|
328
|
+
<div>✅</div>
|
|
329
|
+
<h2>All Clear!</h2>
|
|
330
|
+
<p>No pending templates to review</p>
|
|
331
|
+
</div>
|
|
332
|
+
`;
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
renderTemplates(data.templates);
|
|
337
|
+
|
|
338
|
+
} catch (error) {
|
|
339
|
+
console.error('Error loading templates:', error);
|
|
340
|
+
showError(`Error: ${error.message}`);
|
|
341
|
+
container.innerHTML = '';
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function renderTemplates(templates) {
|
|
346
|
+
const container = document.getElementById('template-container');
|
|
347
|
+
|
|
348
|
+
const templateCards = templates.map(template => `
|
|
349
|
+
<div class="template-card" id="template-${template.emojicode}">
|
|
350
|
+
<div class="template-preview">
|
|
351
|
+
${generateTemplateSVG(template)}
|
|
352
|
+
</div>
|
|
353
|
+
<div class="template-info">
|
|
354
|
+
<h3>${escapeXML(template.name)}</h3>
|
|
355
|
+
<div class="template-meta">
|
|
356
|
+
<div class="template-meta-row">
|
|
357
|
+
<label>Submitted:</label>
|
|
358
|
+
<span>${new Date(template.submittedAt).toLocaleDateString()}</span>
|
|
359
|
+
</div>
|
|
360
|
+
<div class="template-meta-row">
|
|
361
|
+
<label>Creator:</label>
|
|
362
|
+
<span style="font-family: monospace; font-size: 0.8rem;">${template.creatorPubKey.substring(0, 16)}...</span>
|
|
363
|
+
</div>
|
|
364
|
+
<div class="template-meta-row">
|
|
365
|
+
<label>Emojicode:</label>
|
|
366
|
+
<span>${template.emojicode}</span>
|
|
367
|
+
</div>
|
|
368
|
+
<div>
|
|
369
|
+
<label>Background Colors:</label>
|
|
370
|
+
<div class="color-swatches">
|
|
371
|
+
${template.colors.map(color => `<div class="color-swatch" style="background: ${color};" title="${color}"></div>`).join('')}
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
<div>
|
|
375
|
+
<label>Link Colors:</label>
|
|
376
|
+
<div class="color-swatches">
|
|
377
|
+
${template.linkColors.map(color => `<div class="color-swatch" style="background: ${color};" title="${color}"></div>`).join('')}
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
<div class="template-actions">
|
|
382
|
+
<button class="approve-btn" onclick="moderateTemplate('${template.emojicode}', 'approve')">
|
|
383
|
+
Approve
|
|
384
|
+
</button>
|
|
385
|
+
<button class="reject-btn" onclick="moderateTemplate('${template.emojicode}', 'reject')">
|
|
386
|
+
Reject
|
|
387
|
+
</button>
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
</div>
|
|
391
|
+
`).join('');
|
|
392
|
+
|
|
393
|
+
container.innerHTML = `<div class="template-grid">${templateCards}</div>`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function moderateTemplate(emojicode, action) {
|
|
397
|
+
const card = document.getElementById(`template-${emojicode}`);
|
|
398
|
+
const buttons = card.querySelectorAll('button');
|
|
399
|
+
|
|
400
|
+
// Disable buttons
|
|
401
|
+
buttons.forEach(btn => btn.disabled = true);
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
const response = await fetch(`/template/${encodeURIComponent(emojicode)}/moderate`, {
|
|
405
|
+
method: 'PUT',
|
|
406
|
+
headers: { 'Content-Type': 'application/json' },
|
|
407
|
+
body: JSON.stringify({
|
|
408
|
+
pubKey: adminPubKey,
|
|
409
|
+
action: action
|
|
410
|
+
})
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
if (!response.ok) {
|
|
414
|
+
const data = await response.json();
|
|
415
|
+
throw new Error(data.error || `HTTP ${response.status}`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const data = await response.json();
|
|
419
|
+
|
|
420
|
+
if (!data.success) {
|
|
421
|
+
throw new Error(data.error || 'Moderation failed');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Remove card with animation
|
|
425
|
+
card.style.transition = 'opacity 0.3s, transform 0.3s';
|
|
426
|
+
card.style.opacity = '0';
|
|
427
|
+
card.style.transform = 'scale(0.9)';
|
|
428
|
+
|
|
429
|
+
setTimeout(() => {
|
|
430
|
+
card.remove();
|
|
431
|
+
|
|
432
|
+
// Check if no more templates
|
|
433
|
+
const remaining = document.querySelectorAll('.template-card').length;
|
|
434
|
+
if (remaining === 0) {
|
|
435
|
+
document.getElementById('template-container').innerHTML = `
|
|
436
|
+
<div class="empty-state">
|
|
437
|
+
<div>✅</div>
|
|
438
|
+
<h2>All Done!</h2>
|
|
439
|
+
<p>No more pending templates to review</p>
|
|
440
|
+
</div>
|
|
441
|
+
`;
|
|
442
|
+
}
|
|
443
|
+
}, 300);
|
|
444
|
+
|
|
445
|
+
console.log(`✅ Template ${action}d:`, data);
|
|
446
|
+
|
|
447
|
+
} catch (error) {
|
|
448
|
+
console.error(`Error ${action}ing template:`, error);
|
|
449
|
+
showError(`Failed to ${action} template: ${error.message}`);
|
|
450
|
+
buttons.forEach(btn => btn.disabled = false);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function showError(message) {
|
|
455
|
+
const errorContainer = document.getElementById('error-container');
|
|
456
|
+
errorContainer.innerHTML = `<div class="error-message">${escapeXML(message)}</div>`;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Auto-focus pubkey input on load
|
|
460
|
+
window.addEventListener('DOMContentLoaded', () => {
|
|
461
|
+
document.getElementById('admin-pubkey').focus();
|
|
462
|
+
});
|
|
463
|
+
</script>
|
|
464
|
+
</body>
|
|
465
|
+
</html>
|