cc-achievements 1.0.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.
Files changed (3) hide show
  1. package/README.md +47 -0
  2. package/index.html +462 -0
  3. package/package.json +13 -0
package/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # cc-achievements
2
+
3
+ Auto-detect your Claude Code milestones. 20 achievements, all detected from your `~/.claude` folder.
4
+
5
+ ## Usage
6
+
7
+ **Browser only — no install needed:**
8
+
9
+ 👉 [https://yurukusa.github.io/cc-achievements/](https://yurukusa.github.io/cc-achievements/)
10
+
11
+ 1. Open the link in Chrome or Edge
12
+ 2. Click "Browse My Claude Folder"
13
+ 3. Select your `~/.claude` folder
14
+ 4. See which achievements you've unlocked
15
+
16
+ ## Achievements (20 total)
17
+
18
+ | Category | Achievement | Unlock Condition |
19
+ |----------|-------------|-----------------|
20
+ | Volume | 🌱 First Steps | Any sessions detected |
21
+ | Volume | ⏱ Double Digits | 10+ total hours |
22
+ | Volume | 📦 Fifty Hours | 50+ total hours |
23
+ | Volume | 💎 Century | 100+ total hours |
24
+ | Volume | 🔥 Half Grand | 500+ total hours |
25
+ | Consistency | 📅 Hat Trick | 3+ day streak |
26
+ | Consistency | 🗓 Week Strong | 7+ day streak |
27
+ | Consistency | 🏅 Month Strong | 30+ day streak |
28
+ | Consistency | 📆 Veteran | 30+ active calendar days |
29
+ | Ghost Days | 👻 First Ghost | 1 ghost day (AI ran solo) |
30
+ | Ghost Days | 🌙 Ghost Collector | 5+ ghost days |
31
+ | Ghost Days | 🌌 Phantom | 15+ ghost days |
32
+ | Patterns | 🦉 Night Owl | 5+ late-night sessions |
33
+ | Patterns | 💪 Marathon | Single session 4h+ |
34
+ | Patterns | 🏋 Ultramarathon | Single session 6h+ |
35
+ | Patterns | ⚡ Power Day | 5+ sessions in one day |
36
+ | Projects | 🗂 Juggler | 3+ different projects |
37
+ | Projects | 🌍 Explorer | 7+ different projects |
38
+ | Autonomy | 🤖 AI Partners | AI hours ≥ your hours |
39
+ | Autonomy | 🚀 AI Forward | AI hours ≥ 2× your hours |
40
+
41
+ ## Privacy
42
+
43
+ Everything runs in your browser. No data is sent anywhere. Zero dependencies.
44
+
45
+ ## Part of cc-toolkit
46
+
47
+ [cc-toolkit](https://yurukusa.github.io/cc-toolkit/) — 36 free tools for Claude Code users.
package/index.html ADDED
@@ -0,0 +1,462 @@
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>cc-achievements — Unlock Your Claude Code Milestones</title>
7
+ <meta name="description" content="Auto-detect your Claude Code milestones. Select your ~/.claude folder and see which achievements you've unlocked. Zero installs, no data leaves your machine.">
8
+ <meta property="og:title" content="cc-achievements — Your Claude Code Milestones">
9
+ <meta property="og:description" content="How many milestones have you hit? Select your ~/.claude folder to find out. 20 achievements, all auto-detected.">
10
+ <meta property="og:url" content="https://yurukusa.github.io/cc-achievements/">
11
+ <meta name="twitter:card" content="summary">
12
+ <meta name="twitter:site" content="@yurukusa_dev">
13
+ <meta name="twitter:title" content="cc-achievements — Your Claude Code Milestones">
14
+ <meta name="twitter:description" content="Auto-detect your Claude Code milestones from your ~/.claude folder. 20 achievements. Zero installs.">
15
+ <style>
16
+ * { margin: 0; padding: 0; box-sizing: border-box; }
17
+ :root {
18
+ --bg: #0d0d14;
19
+ --surface: #14141e;
20
+ --surface-2: #1c1c2a;
21
+ --gold: #f59e0b;
22
+ --gold-dim: rgba(245,158,11,0.15);
23
+ --gold-glow: rgba(245,158,11,0.3);
24
+ --green: #10b981;
25
+ --purple: #8b5cf6;
26
+ --blue: #3b82f6;
27
+ --text: #e8e8f0;
28
+ --text-dim: #7878a0;
29
+ --text-faint: #44445a;
30
+ --border: #252535;
31
+ --border-locked: #1e1e2e;
32
+ }
33
+ body { background: var(--bg); color: var(--text); font-family: system-ui, -apple-system, sans-serif; min-height: 100vh; }
34
+
35
+ /* Header */
36
+ .header { text-align: center; padding: 2.5rem 1rem 1.5rem; }
37
+ .header h1 { font-size: 2rem; font-weight: 700; letter-spacing: -0.02em; }
38
+ .header h1 span { color: var(--gold); }
39
+ .header p { color: var(--text-dim); margin-top: 0.4rem; font-size: 0.95rem; }
40
+
41
+ /* Pick box */
42
+ .pick-box { max-width: 480px; margin: 0 auto 2rem; text-align: center; padding: 0 1rem; }
43
+ .pick-label { color: var(--text-dim); margin-bottom: 1.2rem; font-size: 0.95rem; line-height: 1.5; }
44
+ .pick-label code { color: var(--text); background: var(--surface-2); padding: 0.1em 0.4em; border-radius: 4px; font-size: 0.9em; }
45
+ .btn-pick {
46
+ display: inline-block; background: var(--gold); color: #000; font-weight: 700;
47
+ padding: 0.8rem 2rem; border-radius: 8px; cursor: pointer; font-size: 1rem;
48
+ transition: opacity 0.15s; user-select: none;
49
+ }
50
+ .btn-pick:hover { opacity: 0.85; }
51
+ .status { margin-top: 1rem; font-size: 0.9rem; color: var(--text-dim); min-height: 1.4em; }
52
+ .status.ok { color: var(--green); }
53
+ .status.err { color: #f87171; }
54
+
55
+ /* Results header */
56
+ .results-header { max-width: 860px; margin: 0 auto 1.5rem; padding: 0 1.2rem; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 0.8rem; }
57
+ .unlocked-count { font-size: 1.1rem; }
58
+ .unlocked-count strong { color: var(--gold); font-size: 1.4rem; }
59
+ .btn-share {
60
+ background: var(--surface-2); border: 1px solid var(--border); color: var(--text);
61
+ padding: 0.5rem 1.2rem; border-radius: 6px; cursor: pointer; font-size: 0.9rem;
62
+ transition: border-color 0.15s;
63
+ }
64
+ .btn-share:hover { border-color: var(--gold); color: var(--gold); }
65
+ .btn-again { background: none; border: 1px solid var(--border); color: var(--text-dim); padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-size: 0.85rem; }
66
+ .btn-again:hover { color: var(--text); border-color: var(--text-dim); }
67
+
68
+ /* Category section */
69
+ .category { max-width: 860px; margin: 0 auto 2rem; padding: 0 1.2rem; }
70
+ .cat-title { font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-dim); margin-bottom: 0.8rem; padding-bottom: 0.4rem; border-bottom: 1px solid var(--border); }
71
+
72
+ /* Achievement grid */
73
+ .ach-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 0.7rem; }
74
+
75
+ /* Achievement card */
76
+ .ach-card {
77
+ background: var(--surface); border: 1px solid var(--border);
78
+ border-radius: 10px; padding: 0.9rem 1rem; display: flex; align-items: center; gap: 0.75rem;
79
+ transition: border-color 0.2s, box-shadow 0.2s;
80
+ }
81
+ .ach-card.unlocked { border-color: var(--gold-glow); background: var(--gold-dim); box-shadow: 0 0 20px var(--gold-glow); }
82
+ .ach-card.locked { opacity: 0.45; }
83
+
84
+ .ach-icon { font-size: 1.6rem; line-height: 1; flex-shrink: 0; width: 2.2rem; text-align: center; }
85
+ .ach-icon.locked-icon { filter: grayscale(1); opacity: 0.4; }
86
+
87
+ .ach-info { min-width: 0; }
88
+ .ach-name { font-size: 0.9rem; font-weight: 700; }
89
+ .ach-card.unlocked .ach-name { color: var(--gold); }
90
+ .ach-desc { font-size: 0.78rem; color: var(--text-dim); margin-top: 0.15rem; line-height: 1.35; }
91
+ .ach-card.locked .ach-desc { color: var(--text-faint); }
92
+
93
+ /* Share popup */
94
+ .share-popup {
95
+ position: fixed; inset: 0; background: rgba(0,0,0,0.7); display: flex; align-items: center; justify-content: center;
96
+ z-index: 100; padding: 1rem;
97
+ }
98
+ .share-box { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 1.5rem; max-width: 480px; width: 100%; }
99
+ .share-box h3 { margin-bottom: 1rem; font-size: 1rem; color: var(--gold); }
100
+ .share-text { background: var(--surface-2); border: 1px solid var(--border); border-radius: 6px; padding: 0.75rem; font-size: 0.85rem; color: var(--text-dim); white-space: pre-wrap; word-break: break-word; min-height: 80px; }
101
+ .share-actions { display: flex; gap: 0.6rem; margin-top: 0.9rem; }
102
+ .btn-copy-text { flex: 1; background: var(--gold); color: #000; font-weight: 700; padding: 0.6rem; border-radius: 6px; border: none; cursor: pointer; font-size: 0.9rem; }
103
+ .btn-copy-text:hover { opacity: 0.85; }
104
+ .btn-close { background: var(--surface-2); color: var(--text-dim); padding: 0.6rem 1rem; border-radius: 6px; border: 1px solid var(--border); cursor: pointer; font-size: 0.9rem; }
105
+ .btn-close:hover { color: var(--text); }
106
+
107
+ /* Footer */
108
+ .footer { text-align: center; padding: 2rem 1rem 3rem; color: var(--text-faint); font-size: 0.8rem; }
109
+ .footer a { color: var(--text-dim); text-decoration: none; }
110
+ .footer a:hover { color: var(--text); }
111
+
112
+ /* Hidden */
113
+ #results-view { display: none; }
114
+ </style>
115
+ </head>
116
+ <body>
117
+
118
+ <div class="header">
119
+ <h1>cc-<span>achievements</span></h1>
120
+ <p>Auto-detect your Claude Code milestones — select your folder, see what you've unlocked.</p>
121
+ </div>
122
+
123
+ <div class="pick-box" id="pick-box">
124
+ <p class="pick-label">Select your <code>~/.claude</code> folder to unlock achievements.<br>Everything runs locally. No data leaves your machine.</p>
125
+ <label class="btn-pick">
126
+ Browse My Claude Folder
127
+ <input type="file" id="dir-input" webkitdirectory style="display:none" onchange="processDir(this.files)">
128
+ </label>
129
+ <div class="status" id="status"></div>
130
+ </div>
131
+
132
+ <div id="results-view">
133
+ <div class="results-header">
134
+ <div class="unlocked-count"><strong id="unlock-n">0</strong> / 20 achievements unlocked</div>
135
+ <div style="display:flex;gap:0.6rem;">
136
+ <button class="btn-share" onclick="showShare()">Share Results</button>
137
+ <button class="btn-again" onclick="reset()">Try Another Folder</button>
138
+ </div>
139
+ </div>
140
+ <div id="ach-categories"></div>
141
+ </div>
142
+
143
+ <div class="share-popup" id="share-popup" style="display:none" onclick="if(event.target===this)closeShare()">
144
+ <div class="share-box">
145
+ <h3>🏆 Share Your Achievements</h3>
146
+ <div class="share-text" id="share-text"></div>
147
+ <div class="share-actions">
148
+ <button class="btn-copy-text" onclick="copyShareText()">Copy to Clipboard</button>
149
+ <button class="btn-close" onclick="closeShare()">Close</button>
150
+ </div>
151
+ </div>
152
+ </div>
153
+
154
+ <div class="footer">
155
+ Part of <a href="https://yurukusa.github.io/cc-toolkit/">cc-toolkit</a> — 106 free tools for Claude Code users.
156
+ <a href="https://github.com/yurukusa/cc-achievements">GitHub</a>
157
+ </div>
158
+
159
+ <script>
160
+ // ── Achievement definitions ────────────────────────────────────
161
+
162
+ const ACHIEVEMENTS = [
163
+ // Volume
164
+ { id: 'first_steps', cat: 'Volume', icon: '🌱', name: 'First Steps', desc: 'Any Claude Code sessions detected.', check: d => d.totalHours > 0 },
165
+ { id: 'double_digits', cat: 'Volume', icon: '⏱', name: 'Double Digits', desc: '10+ total hours of sessions.', check: d => d.totalHours >= 10 },
166
+ { id: 'fifty_hours', cat: 'Volume', icon: '📦', name: 'Fifty Hours', desc: '50+ total hours of sessions.', check: d => d.totalHours >= 50 },
167
+ { id: 'century', cat: 'Volume', icon: '💎', name: 'Century', desc: '100+ total hours. You\'re in deep.', check: d => d.totalHours >= 100 },
168
+ { id: 'half_grand', cat: 'Volume', icon: '🔥', name: 'Half Grand', desc: '500+ total hours. Your hip flexors know.', check: d => d.totalHours >= 500 },
169
+
170
+ // Consistency
171
+ { id: 'hat_trick', cat: 'Consistency', icon: '📅', name: 'Hat Trick', desc: '3+ day active streak.', check: d => d.maxStreak >= 3 },
172
+ { id: 'week_strong', cat: 'Consistency', icon: '🗓', name: 'Week Strong', desc: '7+ day active streak.', check: d => d.maxStreak >= 7 },
173
+ { id: 'month_strong', cat: 'Consistency', icon: '🏅', name: 'Month Strong', desc: '30+ day active streak.', check: d => d.maxStreak >= 30 },
174
+ { id: 'veteran', cat: 'Consistency', icon: '📆', name: 'Veteran', desc: 'Active on 30+ different calendar days.', check: d => d.activeDayCount >= 30 },
175
+
176
+ // Ghost Days
177
+ { id: 'first_ghost', cat: 'Ghost Days', icon: '👻', name: 'First Ghost', desc: 'AI ran solo for a full day while you were away.', check: d => d.ghostDays >= 1 },
178
+ { id: 'ghost_col', cat: 'Ghost Days', icon: '🌙', name: 'Ghost Collector', desc: '5+ ghost days total.', check: d => d.ghostDays >= 5 },
179
+ { id: 'phantom', cat: 'Ghost Days', icon: '🌌', name: 'Phantom', desc: '15+ ghost days. The AI runs itself.', check: d => d.ghostDays >= 15 },
180
+
181
+ // Session Patterns
182
+ { id: 'night_owl', cat: 'Patterns', icon: '🦉', name: 'Night Owl', desc: '5+ sessions started between 11pm and 3am.', check: d => d.nightOwl >= 5 },
183
+ { id: 'marathon', cat: 'Patterns', icon: '💪', name: 'Marathon', desc: 'A single session lasting 4+ hours.', check: d => d.maxSessionHours >= 4 },
184
+ { id: 'ultra', cat: 'Patterns', icon: '🏋', name: 'Ultramarathon', desc: 'A single session lasting 6+ hours. Stretch.', check: d => d.maxSessionHours >= 6 },
185
+ { id: 'power_day', cat: 'Patterns', icon: '⚡', name: 'Power Day', desc: '5+ sessions in a single day.', check: d => d.maxSessionsPerDay >= 5 },
186
+
187
+ // Projects
188
+ { id: 'juggler', cat: 'Projects', icon: '🗂', name: 'Juggler', desc: '3+ different projects worked on.', check: d => d.projectCount >= 3 },
189
+ { id: 'explorer', cat: 'Projects', icon: '🌍', name: 'Explorer', desc: '7+ different projects worked on.', check: d => d.projectCount >= 7 },
190
+
191
+ // Autonomy
192
+ { id: 'ai_partners', cat: 'Autonomy', icon: '🤖', name: 'AI Partners', desc: 'AI session hours ≥ your session hours.', check: d => d.mainHours > 0 && d.subHours / d.mainHours >= 1.0 },
193
+ { id: 'ai_lead', cat: 'Autonomy', icon: '🚀', name: 'AI Forward', desc: 'AI session hours are 2× your session hours.', check: d => d.mainHours > 0 && d.subHours / d.mainHours >= 2.0 },
194
+ ];
195
+
196
+ const CATEGORIES = ['Volume', 'Consistency', 'Ghost Days', 'Patterns', 'Projects', 'Autonomy'];
197
+
198
+ // ── File reading utilities (from cc-score) ─────────────────────
199
+
200
+ function _parseTs(line) {
201
+ if (!line) return null;
202
+ try {
203
+ const obj = JSON.parse(line);
204
+ const ts = obj.timestamp;
205
+ if (!ts) return null;
206
+ const n = typeof ts === 'string' ? Date.parse(ts) : ts;
207
+ return isFinite(n) ? n : null;
208
+ } catch { return null; }
209
+ }
210
+
211
+ async function _readFirstLast(file) {
212
+ const readSize = 1024;
213
+ const firstChunk = await file.slice(0, 2048).text();
214
+ const firstLine = firstChunk.split('\n').find(l => l.trim().length > 0) || '';
215
+ const lastChunk = await file.slice(Math.max(0, file.size - readSize)).text();
216
+ const lines = lastChunk.split('\n').filter(l => l.trim().length > 0);
217
+ const lastLine = lines[lines.length - 1] || '';
218
+ return { firstLine, lastLine };
219
+ }
220
+
221
+ function tsToDate(ts) {
222
+ const d = new Date(ts);
223
+ return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
224
+ }
225
+
226
+ // ── Streak computation ──────────────────────────────────────────
227
+
228
+ function computeMaxStreak(byDate) {
229
+ const dates = Object.keys(byDate).sort();
230
+ if (dates.length === 0) return 0;
231
+ let max = 1, cur = 1;
232
+ for (let i = 1; i < dates.length; i++) {
233
+ const prev = new Date(dates[i-1] + 'T00:00:00');
234
+ const curr = new Date(dates[i] + 'T00:00:00');
235
+ const diff = (curr - prev) / 864e5;
236
+ if (diff === 1) { cur++; max = Math.max(max, cur); }
237
+ else cur = 1;
238
+ }
239
+ return max;
240
+ }
241
+
242
+ // ── Main processing ─────────────────────────────────────────────
243
+
244
+ let _shareText = '';
245
+
246
+ async function processDir(fileList) {
247
+ const statusEl = document.getElementById('status');
248
+ statusEl.textContent = 'Reading sessions…';
249
+ statusEl.className = 'status';
250
+
251
+ const byDate = {}; // {date: {main, sub, mainCount, subCount}}
252
+ const projects = new Set();
253
+ let mainHours = 0, subHours = 0;
254
+ let maxSessionHours = 0;
255
+ let nightOwl = 0;
256
+ let fileCount = 0;
257
+
258
+ for (const file of Array.from(fileList)) {
259
+ const path = file.webkitRelativePath;
260
+ const mainMatch = path.match(/projects\/([^/]+)\/[^/]+\.jsonl$/);
261
+ const subMatch = path.match(/projects\/([^/]+)\/subagents\/[^/]+\.jsonl$/);
262
+ if ((!mainMatch && !subMatch) || file.size < 50) continue;
263
+ const isMain = !!mainMatch && !subMatch;
264
+ const proj = (mainMatch || subMatch)[1];
265
+
266
+ try {
267
+ const { firstLine, lastLine } = await _readFirstLast(file);
268
+ const startTs = _parseTs(firstLine);
269
+ const endTs = _parseTs(lastLine);
270
+ if (!startTs || !endTs) continue;
271
+ const ms = endTs - startTs;
272
+ if (ms < 0 || ms > 7 * 864e5) continue;
273
+ const hours = ms / 36e5;
274
+ const date = tsToDate(startTs);
275
+
276
+ if (!byDate[date]) byDate[date] = { main: 0, sub: 0, mainCount: 0, subCount: 0 };
277
+ if (isMain) {
278
+ byDate[date].main += hours;
279
+ byDate[date].mainCount += 1;
280
+ mainHours += hours;
281
+ projects.add(proj);
282
+ } else {
283
+ byDate[date].sub += hours;
284
+ byDate[date].subCount += 1;
285
+ subHours += hours;
286
+ }
287
+
288
+ maxSessionHours = Math.max(maxSessionHours, hours);
289
+
290
+ // Night owl: session started between 23:00-03:00 local time
291
+ if (isMain) {
292
+ const h = new Date(startTs).getHours();
293
+ if (h >= 23 || h < 3) nightOwl++;
294
+ }
295
+
296
+ fileCount++;
297
+ } catch {}
298
+ }
299
+
300
+ if (fileCount === 0) {
301
+ statusEl.textContent = '⚠ No session data found — make sure you selected the .claude folder';
302
+ statusEl.className = 'status err';
303
+ return;
304
+ }
305
+
306
+ statusEl.textContent = `✓ ${fileCount} sessions read`;
307
+ statusEl.className = 'status ok';
308
+
309
+ // Compute aggregates
310
+ const activeDates = Object.keys(byDate).filter(d => byDate[d].main > 0 || byDate[d].sub > 0);
311
+ const ghostDays = activeDates.filter(d => byDate[d].main === 0 && byDate[d].sub > 0).length;
312
+ const totalHours = mainHours + subHours;
313
+ const maxStreak = computeMaxStreak(
314
+ Object.fromEntries(activeDates.map(d => [d, 1]))
315
+ );
316
+ const maxSessionsPerDay = Math.max(...activeDates.map(d => byDate[d].mainCount + byDate[d].subCount), 0);
317
+
318
+ const data = {
319
+ totalHours, mainHours, subHours,
320
+ activeDayCount: activeDates.length,
321
+ ghostDays, maxStreak,
322
+ projectCount: projects.size,
323
+ nightOwl, maxSessionHours, maxSessionsPerDay,
324
+ };
325
+
326
+ renderResults(data);
327
+ }
328
+
329
+ // ── Render ──────────────────────────────────────────────────────
330
+
331
+ function renderResults(data) {
332
+ const unlocked = ACHIEVEMENTS.filter(a => a.check(data));
333
+ document.getElementById('unlock-n').textContent = unlocked.length;
334
+
335
+ const container = document.getElementById('ach-categories');
336
+ container.innerHTML = '';
337
+
338
+ for (const cat of CATEGORIES) {
339
+ const achs = ACHIEVEMENTS.filter(a => a.cat === cat);
340
+ const catUnlocked = achs.filter(a => a.check(data));
341
+ const section = document.createElement('div');
342
+ section.className = 'category';
343
+ section.innerHTML = `<div class="cat-title">${cat} — ${catUnlocked.length}/${achs.length}</div>`;
344
+ const grid = document.createElement('div');
345
+ grid.className = 'ach-grid';
346
+ for (const ach of achs) {
347
+ const isUnlocked = ach.check(data);
348
+ const card = document.createElement('div');
349
+ card.className = `ach-card ${isUnlocked ? 'unlocked' : 'locked'}`;
350
+ card.innerHTML = `
351
+ <div class="ach-icon${isUnlocked ? '' : ' locked-icon'}">${ach.icon}</div>
352
+ <div class="ach-info">
353
+ <div class="ach-name">${ach.name}</div>
354
+ <div class="ach-desc">${ach.desc}</div>
355
+ </div>`;
356
+ grid.appendChild(card);
357
+ }
358
+ section.appendChild(grid);
359
+ container.appendChild(section);
360
+ }
361
+
362
+ // Build share text + URL
363
+ const unlockedIds = unlocked.map(a => a.id);
364
+ const shareUrl = `https://yurukusa.github.io/cc-achievements/#unlocked=${unlockedIds.join(',')}`;
365
+ const unlockedNames = unlocked.map(a => `${a.icon} ${a.name}`);
366
+ _shareText = `cc-achievements: ${unlocked.length}/20 unlocked\n\n` +
367
+ (unlockedNames.length > 0
368
+ ? unlockedNames.join('\n')
369
+ : '(none yet — get coding!)') +
370
+ `\n\n${shareUrl}\n#claudecode`;
371
+
372
+ // Update browser URL hash (no page reload)
373
+ history.replaceState(null, '', `#unlocked=${unlockedIds.join(',')}`);
374
+
375
+ document.getElementById('pick-box').style.display = 'none';
376
+ document.getElementById('results-view').style.display = '';
377
+ }
378
+
379
+ // ── Share ────────────────────────────────────────────────────────
380
+
381
+ function showShare() {
382
+ document.getElementById('share-text').textContent = _shareText;
383
+ document.getElementById('share-popup').style.display = 'flex';
384
+ }
385
+
386
+ function closeShare() {
387
+ document.getElementById('share-popup').style.display = 'none';
388
+ }
389
+
390
+ function copyShareText() {
391
+ navigator.clipboard.writeText(_shareText).then(() => {
392
+ const btn = document.querySelector('.btn-copy-text');
393
+ const orig = btn.textContent;
394
+ btn.textContent = 'Copied!';
395
+ setTimeout(() => { btn.textContent = orig; }, 2000);
396
+ });
397
+ }
398
+
399
+ function reset() {
400
+ history.replaceState(null, '', location.pathname);
401
+ document.getElementById('pick-box').style.display = '';
402
+ document.getElementById('results-view').style.display = 'none';
403
+ document.getElementById('status').textContent = '';
404
+ document.getElementById('status').className = 'status';
405
+ document.getElementById('dir-input').value = '';
406
+ }
407
+
408
+ // ── Shared view (load from URL hash) ────────────────────────────
409
+
410
+ function loadFromHash() {
411
+ const hash = location.hash;
412
+ if (!hash.startsWith('#unlocked=')) return;
413
+ const ids = hash.slice('#unlocked='.length).split(',').filter(Boolean);
414
+ if (ids.length === 0) return;
415
+
416
+ const idSet = new Set(ids);
417
+ const unlockedAchs = ACHIEVEMENTS.filter(a => idSet.has(a.id));
418
+
419
+ // Show shared banner
420
+ const banner = document.createElement('div');
421
+ banner.style.cssText = 'max-width:860px;margin:0 auto 1rem;padding:0.6rem 1.2rem;background:rgba(245,158,11,0.1);border:1px solid rgba(245,158,11,0.3);border-radius:8px;font-size:0.85rem;color:#f59e0b;text-align:center;';
422
+ banner.textContent = `Shared achievements view — ${unlockedAchs.length}/20 unlocked by someone else. Check your own: ↓`;
423
+ document.getElementById('results-view').prepend(banner);
424
+
425
+ // Render with shared IDs
426
+ document.getElementById('unlock-n').textContent = unlockedAchs.length;
427
+ const container = document.getElementById('ach-categories');
428
+ container.innerHTML = '';
429
+
430
+ for (const cat of CATEGORIES) {
431
+ const achs = ACHIEVEMENTS.filter(a => a.cat === cat);
432
+ const catUnlocked = achs.filter(a => idSet.has(a.id));
433
+ const section = document.createElement('div');
434
+ section.className = 'category';
435
+ section.innerHTML = `<div class="cat-title">${cat} — ${catUnlocked.length}/${achs.length}</div>`;
436
+ const grid = document.createElement('div');
437
+ grid.className = 'ach-grid';
438
+ for (const ach of achs) {
439
+ const isUnlocked = idSet.has(ach.id);
440
+ const card = document.createElement('div');
441
+ card.className = `ach-card ${isUnlocked ? 'unlocked' : 'locked'}`;
442
+ card.innerHTML = `
443
+ <div class="ach-icon${isUnlocked ? '' : ' locked-icon'}">${ach.icon}</div>
444
+ <div class="ach-info">
445
+ <div class="ach-name">${ach.name}</div>
446
+ <div class="ach-desc">${ach.desc}</div>
447
+ </div>`;
448
+ grid.appendChild(card);
449
+ }
450
+ section.appendChild(grid);
451
+ container.appendChild(section);
452
+ }
453
+
454
+ document.getElementById('pick-box').style.display = 'none';
455
+ document.getElementById('results-view').style.display = '';
456
+ }
457
+
458
+ // Run on page load
459
+ loadFromHash();
460
+ </script>
461
+ </body>
462
+ </html>
package/package.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "cc-achievements",
3
+ "version": "1.0.0",
4
+ "description": "Auto-detect your Claude Code milestones. 20 achievements from your ~/.claude folder. Browser only, zero installs.",
5
+ "keywords": ["claude-code", "claude", "ai", "productivity", "achievements", "milestones", "analytics"],
6
+ "homepage": "https://yurukusa.github.io/cc-achievements/",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/yurukusa/cc-achievements"
10
+ },
11
+ "author": "yurukusa",
12
+ "license": "MIT"
13
+ }