groove-dev 0.15.0 → 0.15.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.
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>GROOVE</title>
7
- <script type="module" crossorigin src="/assets/index-CNCSwHwH.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-8Kqi_LVo.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-Gfb8Zxy9.css">
9
9
  </head>
10
10
  <body>
@@ -89,10 +89,55 @@ function sortSkills(skills, sortBy) {
89
89
  }
90
90
  }
91
91
 
92
+ // ── Interactive Star Rating ──────────────────────────────────────────
93
+ function StarRating({ current, onRate, disabled }) {
94
+ const [hover, setHover] = useState(0);
95
+
96
+ return (
97
+ <div style={{ display: 'flex', gap: 2, alignItems: 'center' }}>
98
+ {[1, 2, 3, 4, 5].map((star) => {
99
+ const filled = hover > 0 ? star <= hover : star <= (current || 0);
100
+ return (
101
+ <span
102
+ key={star}
103
+ onClick={() => !disabled && onRate(star)}
104
+ onMouseEnter={() => !disabled && setHover(star)}
105
+ onMouseLeave={() => setHover(0)}
106
+ style={{
107
+ fontSize: 18,
108
+ cursor: disabled ? 'default' : 'pointer',
109
+ color: filled ? 'var(--amber)' : 'var(--bg-active)',
110
+ transition: 'color 0.1s, transform 0.1s',
111
+ transform: hover === star ? 'scale(1.2)' : 'none',
112
+ userSelect: 'none',
113
+ }}
114
+ >
115
+ {'\u2605'}
116
+ </span>
117
+ );
118
+ })}
119
+ </div>
120
+ );
121
+ }
122
+
92
123
  // ── Skill Detail Modal ──────────────────────────────────────────────
93
- function SkillDetailModal({ skill, content, installing, onInstall, onUninstall, onClose }) {
124
+ function SkillDetailModal({ skill, content, installing, onInstall, onUninstall, onRate, onClose }) {
125
+ const [userRating, setUserRating] = useState(0);
126
+ const [ratingSubmitted, setRatingSubmitted] = useState(false);
127
+ const [submittingRating, setSubmittingRating] = useState(false);
128
+
94
129
  if (!skill) return null;
95
130
 
131
+ async function handleRate(stars) {
132
+ setSubmittingRating(true);
133
+ try {
134
+ await onRate(skill.id, stars);
135
+ setUserRating(stars);
136
+ setRatingSubmitted(true);
137
+ } catch { /* ignore */ }
138
+ setSubmittingRating(false);
139
+ }
140
+
96
141
  return (
97
142
  <div style={modal.overlay} onClick={onClose}>
98
143
  <div style={modal.container} onClick={(e) => e.stopPropagation()}>
@@ -162,6 +207,31 @@ function SkillDetailModal({ skill, content, installing, onInstall, onUninstall,
162
207
  {skill.price === 0 && !skill.installed && <span style={modal.freeLabel}>Free</span>}
163
208
  </div>
164
209
 
210
+ {/* Rating */}
211
+ <div style={modal.section}>
212
+ <div style={modal.sectionTitle}>Rating</div>
213
+ <div style={modal.ratingRow}>
214
+ <div style={modal.ratingLeft}>
215
+ <div style={modal.ratingBig}>{skill.rating || '-'}</div>
216
+ <div style={{ color: 'var(--amber)', fontSize: 13 }}>{renderStars(skill.rating)}</div>
217
+ <div style={modal.ratingCount}>{skill.ratingCount || 0} ratings</div>
218
+ </div>
219
+ <div style={modal.ratingRight}>
220
+ <div style={modal.rateLabel}>
221
+ {ratingSubmitted ? 'Thanks for rating!' : 'Rate this skill'}
222
+ </div>
223
+ <StarRating
224
+ current={userRating}
225
+ onRate={handleRate}
226
+ disabled={submittingRating || ratingSubmitted}
227
+ />
228
+ {submittingRating && (
229
+ <div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 4 }}>Submitting...</div>
230
+ )}
231
+ </div>
232
+ </div>
233
+ </div>
234
+
165
235
  {/* Description */}
166
236
  <div style={modal.section}>
167
237
  <div style={modal.sectionTitle}>About</div>
@@ -413,6 +483,23 @@ export default function SkillsMarketplace() {
413
483
  setInstalling(null);
414
484
  }
415
485
 
486
+ async function handleRate(id, rating) {
487
+ const res = await fetch(`/api/skills/${id}/rate`, {
488
+ method: 'POST',
489
+ headers: { 'Content-Type': 'application/json' },
490
+ body: JSON.stringify({ rating }),
491
+ });
492
+ if (!res.ok) throw new Error('Rating failed');
493
+ const data = await res.json();
494
+ // Update the skill in local state with new rating
495
+ setSkills((prev) => prev.map((s) =>
496
+ s.id === id ? { ...s, rating: data.rating, ratingCount: data.rating_count ?? data.ratingCount } : s
497
+ ));
498
+ if (selectedSkill?.id === id) {
499
+ setSelectedSkill((prev) => prev ? { ...prev, rating: data.rating, ratingCount: data.rating_count ?? data.ratingCount } : null);
500
+ }
501
+ }
502
+
416
503
  async function handleSelect(skill) {
417
504
  setSelectedSkill(skill);
418
505
  setSkillContent(null);
@@ -561,6 +648,7 @@ export default function SkillsMarketplace() {
561
648
  installing={installing}
562
649
  onInstall={handleInstall}
563
650
  onUninstall={handleUninstall}
651
+ onRate={handleRate}
564
652
  onClose={() => { setSelectedSkill(null); setSkillContent(null); }}
565
653
  />
566
654
  </div>
@@ -876,6 +964,29 @@ const modal = {
876
964
  section: {
877
965
  marginBottom: 16,
878
966
  },
967
+ ratingRow: {
968
+ display: 'flex', gap: 20, alignItems: 'center',
969
+ padding: '12px 14px',
970
+ background: 'var(--bg-surface)', border: '1px solid var(--border)',
971
+ borderRadius: 6,
972
+ },
973
+ ratingLeft: {
974
+ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2,
975
+ minWidth: 70,
976
+ },
977
+ ratingBig: {
978
+ fontSize: 24, fontWeight: 700, color: 'var(--text-bright)',
979
+ lineHeight: 1,
980
+ },
981
+ ratingCount: {
982
+ fontSize: 9, color: 'var(--text-muted)', marginTop: 2,
983
+ },
984
+ ratingRight: {
985
+ flex: 1,
986
+ },
987
+ rateLabel: {
988
+ fontSize: 11, color: 'var(--text-dim)', marginBottom: 6,
989
+ },
879
990
  sectionTitle: {
880
991
  fontSize: 10, fontWeight: 600, color: 'var(--text-muted)',
881
992
  textTransform: 'uppercase', letterSpacing: 0.8, marginBottom: 8,