Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

explore.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

explore.tsxBlame1164 lines · 1 contributor
c81ab7aClaude1/**
2 * Explore page — discover public repositories, search, trending.
48c9efcClaude3 *
4 * Visual polish (2026 pass): adopts the design language from the
5 * admin-hero (07f4b70), tile-grid (cf793f9), and build-agent-integration
6 * (4a80519) commits. Hero gets a larger clamp(40-72px) gradient title,
7 * a drifting orb, and a tighter eyebrow + sub layout. Repo cards now
8 * use a gradient hairline that lights up on hover, a polished
9 * star/fork/lang chip row with tabular-nums, and ARIA-aware focus
10 * rings. Filter pills sit in a glass capsule, pagination/limit hint
11 * is surfaced as a dashed footer card, and the empty state keeps the
12 * orbiting search orb. All CSS is scoped under `.explore-*`. Route
13 * handler, query semantics, sort/topic/q params, and ordering are
14 * unchanged from the pre-polish version.
c81ab7aClaude15 */
16
17import { Hono } from "hono";
283fbc2Claude18import { eq, desc, sql, and } from "drizzle-orm";
c81ab7aClaude19import { db } from "../db";
20import { repositories, users, repoTopics } from "../db/schema";
21import { Layout } from "../views/layout";
22import { softAuth } from "../middleware/auth";
23import type { AuthEnv } from "../middleware/auth";
24
25const explore = new Hono<AuthEnv>();
26
27explore.use("*", softAuth);
28
283fbc2Claude29const ExploreStyle = () => (
30 <style
31 dangerouslySetInnerHTML={{
32 __html: `
48c9efcClaude33 .explore-wrap { max-width: 1180px; margin: 0 auto; }
34
283fbc2Claude35 /* ─── Hero ─── */
36 .explore-hero {
37 position: relative;
48c9efcClaude38 margin: 4px 0 28px;
39 padding: 40px 36px 36px;
283fbc2Claude40 background: var(--bg-elevated);
41 border: 1px solid var(--border);
48c9efcClaude42 border-radius: 18px;
283fbc2Claude43 overflow: hidden;
44 }
45 .explore-hero::before {
46 content: '';
47 position: absolute;
48 top: 0; left: 0; right: 0;
49 height: 2px;
50 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
48c9efcClaude51 opacity: 0.75;
283fbc2Claude52 pointer-events: none;
53 }
54 .explore-hero-bg {
55 position: absolute;
48c9efcClaude56 inset: -35% -12% auto auto;
57 width: 460px;
58 height: 460px;
283fbc2Claude59 pointer-events: none;
60 z-index: 0;
61 }
62 .explore-hero-orb {
63 position: absolute;
64 inset: 0;
48c9efcClaude65 background: radial-gradient(circle, rgba(140,109,255,0.22), rgba(54,197,214,0.10) 45%, transparent 70%);
283fbc2Claude66 filter: blur(80px);
48c9efcClaude67 opacity: 0.75;
68 animation: exploreHeroOrb 16s ease-in-out infinite;
69 }
70 .explore-hero-orb-2 {
71 position: absolute;
72 inset: auto auto -25% -15%;
73 width: 320px; height: 320px;
74 background: radial-gradient(circle, rgba(54,197,214,0.16), rgba(140,109,255,0.06) 50%, transparent 75%);
75 filter: blur(70px);
76 opacity: 0.55;
77 pointer-events: none;
78 z-index: 0;
79 animation: exploreHeroOrb2 18s ease-in-out infinite;
283fbc2Claude80 }
81 @keyframes exploreHeroOrb {
48c9efcClaude82 0%, 100% { transform: scale(1) translate(0, 0); opacity: 0.65; }
83 50% { transform: scale(1.12) translate(-14px, 10px); opacity: 0.92; }
84 }
85 @keyframes exploreHeroOrb2 {
86 0%, 100% { transform: scale(1) translate(0, 0); opacity: 0.45; }
87 50% { transform: scale(1.08) translate(16px, -8px); opacity: 0.72; }
283fbc2Claude88 }
89 @media (prefers-reduced-motion: reduce) {
48c9efcClaude90 .explore-hero-orb, .explore-hero-orb-2 { animation: none; }
283fbc2Claude91 }
92 .explore-hero-inner {
93 position: relative;
94 z-index: 1;
95 display: flex;
96 flex-direction: column;
48c9efcClaude97 gap: 20px;
283fbc2Claude98 }
99 .explore-hero-text { flex: 1; min-width: 280px; }
100 .explore-hero-eyebrow {
48c9efcClaude101 display: inline-flex;
102 align-items: center;
103 gap: 8px;
104 font-size: 12px;
283fbc2Claude105 color: var(--text-muted);
48c9efcClaude106 margin-bottom: 12px;
107 letter-spacing: 0.10em;
283fbc2Claude108 text-transform: uppercase;
48c9efcClaude109 font-weight: 700;
110 }
111 .explore-hero-eyebrow-dot {
112 display: inline-block;
113 width: 6px; height: 6px;
114 border-radius: 9999px;
115 background: linear-gradient(135deg, #8c6dff, #36c5d6);
116 box-shadow: 0 0 0 3px rgba(140,109,255,0.18);
283fbc2Claude117 }
118 .explore-hero-title {
119 font-family: var(--font-display);
48c9efcClaude120 font-size: clamp(40px, 6.5vw, 72px);
283fbc2Claude121 font-weight: 800;
48c9efcClaude122 letter-spacing: -0.032em;
123 line-height: 1.02;
124 margin: 0 0 14px;
283fbc2Claude125 color: var(--text-strong);
126 }
48c9efcClaude127 .explore-hero-title-grad {
128 background-image: linear-gradient(135deg, #a48bff 0%, #8c6dff 45%, #36c5d6 100%);
129 -webkit-background-clip: text;
130 background-clip: text;
131 -webkit-text-fill-color: transparent;
132 color: transparent;
133 }
283fbc2Claude134 .explore-hero-sub {
48c9efcClaude135 font-size: 15.5px;
283fbc2Claude136 color: var(--text-muted);
137 margin: 0;
48c9efcClaude138 line-height: 1.55;
139 max-width: 640px;
283fbc2Claude140 }
141 .explore-hero-search {
142 display: flex;
143 gap: 10px;
144 align-items: stretch;
145 flex-wrap: wrap;
48c9efcClaude146 margin-top: 6px;
283fbc2Claude147 }
48c9efcClaude148 .explore-hero-search-field {
149 position: relative;
283fbc2Claude150 flex: 1;
151 min-width: 240px;
48c9efcClaude152 }
153 .explore-hero-search-icon {
154 position: absolute;
155 left: 14px;
156 top: 50%;
157 transform: translateY(-50%);
158 width: 16px; height: 16px;
159 color: var(--text-faint);
160 pointer-events: none;
161 z-index: 1;
162 }
163 .explore-hero-search input[type="search"] {
164 width: 100%;
165 padding: 12px 16px 12px 40px;
283fbc2Claude166 background: var(--bg);
167 border: 1px solid var(--border);
168 border-radius: 12px;
169 color: var(--text);
170 font-size: 14.5px;
171 font-family: inherit;
48c9efcClaude172 transition: border-color 140ms ease, box-shadow 140ms ease, background 140ms ease;
173 }
174 .explore-hero-search input[type="search"]::placeholder {
175 color: var(--text-faint);
283fbc2Claude176 }
177 .explore-hero-search input:focus {
178 outline: none;
48c9efcClaude179 border-color: rgba(140,109,255,0.65);
180 box-shadow: 0 0 0 4px rgba(140,109,255,0.14);
181 background: var(--bg-secondary);
283fbc2Claude182 }
48c9efcClaude183 .explore-hero-search .btn { padding: 12px 22px; border-radius: 12px; }
283fbc2Claude184 @media (max-width: 720px) {
48c9efcClaude185 .explore-hero { padding: 28px 22px 26px; }
283fbc2Claude186 .explore-hero-search .btn { flex: 1; min-width: 0; }
187 }
188
189 /* ─── Toolbar / Filter pills ─── */
190 .explore-toolbar {
191 display: flex;
192 align-items: center;
193 justify-content: space-between;
194 gap: 12px;
195 flex-wrap: wrap;
48c9efcClaude196 margin: 0 0 20px;
283fbc2Claude197 }
198 .explore-filters {
199 display: inline-flex;
200 background: var(--bg-elevated);
201 border: 1px solid var(--border);
202 border-radius: 9999px;
203 padding: 4px;
204 gap: 2px;
48c9efcClaude205 box-shadow: 0 1px 0 rgba(255,255,255,0.02) inset;
283fbc2Claude206 }
207 .explore-filter {
208 display: inline-flex;
209 align-items: center;
210 gap: 6px;
48c9efcClaude211 padding: 8px 16px;
283fbc2Claude212 border-radius: 9999px;
213 font-size: 13px;
214 font-weight: 500;
215 color: var(--text-muted);
216 text-decoration: none;
48c9efcClaude217 transition: color 120ms ease, background 120ms ease, transform 120ms ease;
283fbc2Claude218 line-height: 1.4;
219 }
220 .explore-filter:hover { color: var(--text-strong); text-decoration: none; }
221 .explore-filter.is-active {
48c9efcClaude222 background: linear-gradient(135deg, rgba(140,109,255,0.18), rgba(54,197,214,0.14));
283fbc2Claude223 color: var(--text-strong);
48c9efcClaude224 box-shadow: inset 0 0 0 1px rgba(140,109,255,0.30);
225 }
226 .explore-filter-icon {
227 width: 13px; height: 13px;
228 display: inline-block;
229 opacity: 0.9;
283fbc2Claude230 }
231 .explore-toolbar-meta {
232 font-size: 12.5px;
233 color: var(--text-muted);
234 font-variant-numeric: tabular-nums;
235 }
236 .explore-toolbar-meta strong { color: var(--text); font-weight: 600; }
237
238 /* ─── Topic chip ─── */
239 .explore-topic-row {
240 display: flex;
241 align-items: center;
242 gap: 10px;
243 margin: 0 0 16px;
244 font-size: 13px;
245 color: var(--text-muted);
48c9efcClaude246 flex-wrap: wrap;
283fbc2Claude247 }
248 .explore-topic-pill {
249 display: inline-flex;
250 align-items: center;
251 gap: 6px;
252 padding: 4px 12px;
253 border-radius: 9999px;
254 background: rgba(140,109,255,0.12);
255 color: var(--text-strong);
256 border: 1px solid rgba(140,109,255,0.30);
257 font-size: 12.5px;
258 font-weight: 600;
259 }
260 .explore-topic-clear {
261 color: var(--text-muted);
262 text-decoration: none;
263 transition: color 120ms ease;
264 }
265 .explore-topic-clear:hover { color: var(--accent); text-decoration: none; }
266
48c9efcClaude267 /* ─── Section header (results category) ─── */
268 .explore-section-head {
269 display: flex;
270 align-items: baseline;
271 justify-content: space-between;
272 gap: 12px;
273 margin: 28px 0 14px;
274 padding: 0 2px;
275 flex-wrap: wrap;
276 }
277 .explore-section-head:first-of-type { margin-top: 4px; }
278 .explore-section-title {
279 font-family: var(--font-display);
280 font-size: 18px;
281 font-weight: 700;
282 letter-spacing: -0.018em;
283 color: var(--text-strong);
284 margin: 0;
285 display: inline-flex;
286 align-items: center;
287 gap: 10px;
288 }
289 .explore-section-title-bar {
290 display: inline-block;
291 width: 3px;
292 height: 16px;
293 border-radius: 2px;
294 background: linear-gradient(180deg, #8c6dff, #36c5d6);
295 }
296 .explore-section-sub {
297 font-size: 12.5px;
298 color: var(--text-faint);
299 font-variant-numeric: tabular-nums;
300 }
301
283fbc2Claude302 /* ─── Repo cards grid ─── */
303 .explore-grid {
304 display: grid;
305 grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
306 gap: 14px;
307 }
308 .explore-card {
309 position: relative;
310 display: flex;
311 flex-direction: column;
312 gap: 12px;
313 padding: 18px 20px;
314 background: var(--bg-elevated);
315 border: 1px solid var(--border);
316 border-radius: 14px;
317 text-decoration: none;
318 color: inherit;
48c9efcClaude319 overflow: hidden;
320 transition: transform 160ms ease, border-color 160ms ease, box-shadow 160ms ease;
321 }
322 /* Gradient hairline on hover */
323 .explore-card::before {
324 content: '';
325 position: absolute;
326 top: 0; left: 0; right: 0;
327 height: 2px;
328 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
329 opacity: 0;
330 transition: opacity 180ms ease;
331 pointer-events: none;
283fbc2Claude332 }
333 .explore-card:hover {
48c9efcClaude334 transform: translateY(-3px);
283fbc2Claude335 border-color: rgba(140,109,255,0.45);
48c9efcClaude336 box-shadow: 0 14px 30px -16px rgba(0,0,0,0.55), 0 0 24px -8px rgba(140,109,255,0.22);
283fbc2Claude337 text-decoration: none;
338 }
48c9efcClaude339 .explore-card:hover::before { opacity: 0.85; }
340 .explore-card:focus-visible {
341 outline: none;
342 border-color: rgba(140,109,255,0.65);
343 box-shadow: 0 0 0 3px rgba(140,109,255,0.22);
344 }
283fbc2Claude345 .explore-card-head {
346 display: flex;
347 align-items: center;
348 gap: 12px;
349 min-width: 0;
350 }
351 .explore-card-avatar {
352 width: 36px;
353 height: 36px;
354 border-radius: 9px;
355 display: inline-flex;
356 align-items: center;
357 justify-content: center;
358 font-family: var(--font-display);
359 font-weight: 700;
360 font-size: 14px;
361 color: #fff;
362 background: linear-gradient(135deg, #8c6dff 0%, #36c5d6 100%);
363 flex-shrink: 0;
364 letter-spacing: -0.01em;
48c9efcClaude365 box-shadow: 0 4px 12px -4px rgba(140,109,255,0.45);
283fbc2Claude366 }
367 .explore-card-name {
368 display: flex;
369 flex-direction: column;
370 min-width: 0;
371 line-height: 1.25;
372 }
373 .explore-card-owner {
374 font-size: 12px;
375 color: var(--text-muted);
376 overflow: hidden;
377 text-overflow: ellipsis;
378 white-space: nowrap;
379 }
380 .explore-card-repo {
381 font-family: var(--font-display);
382 font-size: 16px;
383 font-weight: 700;
384 color: var(--text-strong);
385 letter-spacing: -0.018em;
386 overflow: hidden;
387 text-overflow: ellipsis;
388 white-space: nowrap;
389 transition: color 120ms ease;
390 }
48c9efcClaude391 .explore-card:hover .explore-card-repo { color: var(--accent-hover); }
283fbc2Claude392 .explore-card-desc {
393 font-size: 13.5px;
394 color: var(--text-muted);
395 margin: 0;
396 line-height: 1.5;
397 display: -webkit-box;
398 -webkit-line-clamp: 2;
399 -webkit-box-orient: vertical;
400 overflow: hidden;
401 }
48c9efcClaude402 .explore-card-desc-empty {
403 font-size: 13px;
404 color: var(--text-faint);
405 margin: 0;
406 font-style: italic;
407 }
283fbc2Claude408 .explore-card-meta {
409 display: flex;
410 align-items: center;
411 gap: 14px;
412 margin-top: auto;
48c9efcClaude413 padding-top: 4px;
283fbc2Claude414 font-size: 12px;
415 color: var(--text-muted);
416 flex-wrap: wrap;
48c9efcClaude417 font-variant-numeric: tabular-nums;
283fbc2Claude418 }
419 .explore-card-meta-item {
420 display: inline-flex;
421 align-items: center;
422 gap: 5px;
423 font-variant-numeric: tabular-nums;
424 }
48c9efcClaude425 .explore-card-meta-icon {
426 width: 12px; height: 12px;
427 color: var(--text-faint);
428 display: inline-block;
429 }
430 .explore-card:hover .explore-card-meta-icon { color: var(--accent); }
283fbc2Claude431 .explore-card-badges {
432 display: flex;
433 gap: 6px;
434 flex-wrap: wrap;
435 }
436 .explore-pill {
437 display: inline-flex;
438 align-items: center;
439 gap: 5px;
440 padding: 2px 10px;
441 border-radius: 9999px;
442 font-size: 11.5px;
443 font-weight: 600;
444 line-height: 1.5;
445 letter-spacing: 0.005em;
446 }
447 .explore-pill-private {
448 background: rgba(255,180,94,0.10);
449 color: #ffb45e;
450 border: 1px solid rgba(255,180,94,0.28);
451 }
452 .explore-pill-fork {
453 background: rgba(54,197,214,0.10);
454 color: #36c5d6;
455 border: 1px solid rgba(54,197,214,0.28);
456 }
457 .explore-pill-archived {
458 background: rgba(255,255,255,0.04);
459 color: var(--text-muted);
460 border: 1px solid var(--border);
461 }
462 .explore-pill-template {
463 background: rgba(140,109,255,0.12);
464 color: var(--text-strong);
465 border: 1px solid rgba(140,109,255,0.30);
466 }
48c9efcClaude467
468 /* ─── Loading skeleton (used when JS later swaps in async fragments) ─── */
469 .explore-skel-grid {
470 display: grid;
471 grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
472 gap: 14px;
473 }
474 .explore-skel {
475 padding: 18px 20px;
476 background: var(--bg-elevated);
477 border: 1px solid var(--border);
478 border-radius: 14px;
479 display: flex;
480 flex-direction: column;
481 gap: 12px;
482 min-height: 140px;
483 }
484 .explore-skel-row {
485 height: 12px;
486 border-radius: 6px;
487 background: linear-gradient(90deg, rgba(255,255,255,0.04) 0%, rgba(255,255,255,0.08) 50%, rgba(255,255,255,0.04) 100%);
488 background-size: 200% 100%;
489 animation: exploreSkelPulse 1.4s ease-in-out infinite;
490 }
491 .explore-skel-row.is-title { height: 16px; width: 60%; }
492 .explore-skel-row.is-line { width: 100%; }
493 .explore-skel-row.is-short { width: 40%; }
494 @keyframes exploreSkelPulse {
495 0% { background-position: 200% 0; opacity: 0.7; }
496 100% { background-position: -200% 0; opacity: 1; }
497 }
498 @media (prefers-reduced-motion: reduce) {
499 .explore-skel-row { animation: none; }
500 }
283fbc2Claude501
502 /* ─── Empty state ─── */
503 .explore-empty {
504 margin: 0;
48c9efcClaude505 padding: 64px 32px;
283fbc2Claude506 background: var(--bg-elevated);
48c9efcClaude507 border: 1px dashed var(--border-strong);
508 border-radius: 18px;
283fbc2Claude509 text-align: center;
510 position: relative;
511 overflow: hidden;
512 }
513 .explore-empty::before {
514 content: '';
515 position: absolute;
48c9efcClaude516 inset: -40% -20% auto auto;
517 width: 320px; height: 320px;
518 background: radial-gradient(circle, rgba(140,109,255,0.18), rgba(54,197,214,0.08) 50%, transparent 75%);
519 filter: blur(70px);
283fbc2Claude520 opacity: 0.55;
521 pointer-events: none;
48c9efcClaude522 animation: exploreHeroOrb 16s ease-in-out infinite;
283fbc2Claude523 }
524 .explore-empty-art {
525 width: 88px;
526 height: 88px;
527 margin: 0 auto 18px;
528 display: block;
48c9efcClaude529 opacity: 0.92;
530 position: relative;
531 z-index: 1;
283fbc2Claude532 }
533 .explore-empty-title {
534 font-family: var(--font-display);
48c9efcClaude535 font-size: 24px;
283fbc2Claude536 font-weight: 700;
48c9efcClaude537 letter-spacing: -0.022em;
283fbc2Claude538 color: var(--text-strong);
539 margin: 0 0 8px;
48c9efcClaude540 position: relative;
541 z-index: 1;
283fbc2Claude542 }
543 .explore-empty-sub {
544 font-size: 14.5px;
545 color: var(--text-muted);
546 line-height: 1.55;
547 margin: 0 auto 22px;
48c9efcClaude548 max-width: 480px;
549 position: relative;
550 z-index: 1;
283fbc2Claude551 }
552 .explore-empty-cta {
553 display: inline-flex;
554 gap: 10px;
555 flex-wrap: wrap;
556 justify-content: center;
48c9efcClaude557 position: relative;
558 z-index: 1;
559 }
560
561 /* ─── Footer hint (limit + suggest search) ─── */
562 .explore-foot {
563 margin-top: 28px;
564 padding: 18px 22px;
565 background: transparent;
566 border: 1px dashed var(--border);
567 border-radius: 14px;
568 display: flex;
569 align-items: center;
570 justify-content: space-between;
571 gap: 14px;
572 flex-wrap: wrap;
573 }
574 .explore-foot-text {
575 font-size: 13px;
576 color: var(--text-muted);
577 line-height: 1.5;
578 margin: 0;
579 flex: 1;
580 min-width: 200px;
581 }
582 .explore-foot-text strong {
583 color: var(--text);
584 font-weight: 600;
585 font-variant-numeric: tabular-nums;
586 }
587 .explore-foot-actions {
588 display: flex;
589 gap: 8px;
590 flex-wrap: wrap;
283fbc2Claude591 }
f1dc7c7Claude592
593 /* ─── Mobile (≤720px) ─── */
594 @media (max-width: 720px) {
595 .explore-wrap { padding: 0 2px; }
596 .explore-hero-search-field { min-width: 0; width: 100%; }
597 .explore-hero-search input[type="search"] { padding: 14px 14px 14px 38px; min-height: 44px; }
598 .explore-toolbar { flex-direction: column; align-items: stretch; }
599 .explore-filters { width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }
600 .explore-filter { min-height: 40px; padding: 10px 14px; }
601 .explore-grid { grid-template-columns: 1fr; }
602 .explore-skel-grid { grid-template-columns: 1fr; }
603 .explore-card { padding: 14px 16px; }
604 .explore-empty { padding: 40px 20px; }
605 .explore-foot { flex-direction: column; align-items: stretch; padding: 16px 18px; }
606 .explore-foot-actions { width: 100%; }
607 .explore-foot-actions .btn { flex: 1; min-width: 0; }
608 }
283fbc2Claude609 `,
610 }}
611 />
612);
613
614function initials(name: string): string {
615 if (!name) return "·";
616 const clean = name.replace(/[^a-zA-Z0-9]+/g, " ").trim();
617 if (!clean) return name.charAt(0).toUpperCase();
618 const parts = clean.split(/\s+/);
619 if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
620 return (parts[0][0] + parts[1][0]).toUpperCase();
621}
622
623function formatRelative(dateStr: string | Date): string {
624 const date = typeof dateStr === "string" ? new Date(dateStr) : dateStr;
625 const diffMs = Date.now() - date.getTime();
626 const diffMins = Math.floor(diffMs / 60000);
627 if (diffMins < 1) return "just now";
628 if (diffMins < 60) return `${diffMins}m ago`;
629 const diffHours = Math.floor(diffMins / 60);
630 if (diffHours < 24) return `${diffHours}h ago`;
631 const diffDays = Math.floor(diffHours / 24);
632 if (diffDays < 30) return `${diffDays}d ago`;
633 const diffMonths = Math.floor(diffDays / 30);
634 if (diffMonths < 12) return `${diffMonths}mo ago`;
635 return `${Math.floor(diffMonths / 12)}y ago`;
636}
637
c81ab7aClaude638explore.get("/explore", async (c) => {
639 const user = c.get("user");
640 const q = c.req.query("q") || "";
641 const sort = c.req.query("sort") || "recent";
642 const topic = c.req.query("topic") || "";
643
f1dc7c7Claude644 // ── Loading skeleton (flag-gated) ──
645 // Renders the explore shell + card-grid skeleton when `?skeleton=1`
646 // is set. Re-uses the existing `.explore-skel-*` styles from
647 // ExploreStyle. Behind a flag so we don't flash empty placeholders
648 // before the real list lands.
649 if (c.req.query("skeleton") === "1") {
650 return c.html(
651 <Layout title="Explore" user={user}>
652 <ExploreStyle />
653 <div class="explore-wrap">
654 <section class="explore-hero" aria-hidden="true">
655 <div class="explore-hero-bg">
656 <div class="explore-hero-orb" />
657 </div>
658 <div class="explore-hero-orb-2" />
659 <div class="explore-hero-inner">
660 <div class="explore-hero-text">
661 <div class="explore-hero-eyebrow">
662 <span class="explore-hero-eyebrow-dot" />
663 Discover
664 </div>
665 <h1 class="explore-hero-title">
666 What&rsquo;s{" "}
667 <span class="explore-hero-title-grad">shipping</span>.
668 </h1>
669 <p class="explore-hero-sub">
670 Loading public repositories…
671 </p>
672 </div>
673 </div>
674 </section>
675 <div class="explore-skel-grid" aria-hidden="true">
676 {Array.from({ length: 9 }).map(() => (
677 <div class="explore-skel">
678 <div class="explore-skel-row is-title" />
679 <div class="explore-skel-row is-line" />
680 <div class="explore-skel-row is-line" />
681 <div class="explore-skel-row is-short" />
682 </div>
683 ))}
684 </div>
685 <span style="position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0" role="status" aria-live="polite">
686 Loading public repositories…
687 </span>
688 </div>
689 </Layout>
690 );
691 }
692
c81ab7aClaude693 let repoList: Array<{
694 repo: typeof repositories.$inferSelect;
695 ownerName: string;
696 }> = [];
697
698 if (q.trim()) {
699 // Search repos
700 const results = await db
701 .select({
702 repo: repositories,
703 ownerName: users.username,
704 })
705 .from(repositories)
706 .innerJoin(users, eq(repositories.ownerId, users.id))
707 .where(
708 and(
709 eq(repositories.isPrivate, false),
710 sql`(${repositories.name} ILIKE ${'%' + q + '%'} OR ${repositories.description} ILIKE ${'%' + q + '%'})`
711 )
712 )
713 .orderBy(desc(repositories.starCount))
714 .limit(50);
715
716 repoList = results.map((r) => ({
717 repo: r.repo,
718 ownerName: r.ownerName,
719 }));
720 } else if (topic) {
721 // Filter by topic
722 const results = await db
723 .select({
724 repo: repositories,
725 ownerName: users.username,
726 })
727 .from(repositories)
728 .innerJoin(users, eq(repositories.ownerId, users.id))
729 .innerJoin(repoTopics, eq(repoTopics.repositoryId, repositories.id))
730 .where(
731 and(
732 eq(repositories.isPrivate, false),
733 eq(repoTopics.topic, topic.toLowerCase())
734 )
735 )
736 .orderBy(desc(repositories.starCount))
737 .limit(50);
738
739 repoList = results.map((r) => ({
740 repo: r.repo,
741 ownerName: r.ownerName,
742 }));
743 } else {
744 // Default: recent or popular
745 const orderBy =
746 sort === "stars"
747 ? desc(repositories.starCount)
748 : sort === "forks"
749 ? desc(repositories.forkCount)
750 : desc(repositories.createdAt);
751
752 const results = await db
753 .select({
754 repo: repositories,
755 ownerName: users.username,
756 })
757 .from(repositories)
758 .innerJoin(users, eq(repositories.ownerId, users.id))
759 .where(eq(repositories.isPrivate, false))
760 .orderBy(orderBy)
761 .limit(50);
762
763 repoList = results.map((r) => ({
764 repo: r.repo,
765 ownerName: r.ownerName,
766 }));
767 }
768
283fbc2Claude769 const sortLabel =
770 sort === "stars"
771 ? "most-starred"
772 : sort === "forks"
773 ? "most-forked"
774 : "most recent";
775
48c9efcClaude776 const sectionTitle = q
777 ? `Search results`
778 : topic
779 ? `Tagged #${topic}`
780 : sort === "stars"
781 ? `Trending repositories`
782 : sort === "forks"
783 ? `Most-forked repositories`
784 : `Recently active`;
785
786 const sectionSub = q
787 ? `Public repositories matching "${q}"`
788 : topic
789 ? `Public repositories tagged with this topic`
790 : sort === "stars"
791 ? `Sorted by stars across the platform`
792 : sort === "forks"
793 ? `Sorted by fork count across the platform`
794 : `Sorted by repository creation date`;
795
c81ab7aClaude796 return c.html(
797 <Layout title="Explore" user={user}>
283fbc2Claude798 <ExploreStyle />
48c9efcClaude799 <div class="explore-wrap">
800 <section class="explore-hero">
801 <div class="explore-hero-bg" aria-hidden="true">
802 <div class="explore-hero-orb" />
283fbc2Claude803 </div>
48c9efcClaude804 <div class="explore-hero-orb-2" aria-hidden="true" />
805 <div class="explore-hero-inner">
806 <div class="explore-hero-text">
807 <div class="explore-hero-eyebrow">
808 <span class="explore-hero-eyebrow-dot" aria-hidden="true" />
809 Discover
810 </div>
811 <h1 class="explore-hero-title">
812 What&rsquo;s{" "}
813 <span class="explore-hero-title-grad">shipping</span>.
814 </h1>
815 <p class="explore-hero-sub">
816 {q
817 ? `Searching public repos for "${q}".`
818 : topic
819 ? `Public repos tagged with #${topic}.`
820 : "Browse public repositories built on Gluecron — AI-reviewed code, gate-checked pushes, and the people shipping them."}
821 </p>
822 </div>
823 <form
824 method="get"
825 action="/explore"
826 class="explore-hero-search"
827 role="search"
828 >
829 <div class="explore-hero-search-field">
830 <svg
831 class="explore-hero-search-icon"
832 viewBox="0 0 16 16"
833 fill="none"
834 xmlns="http://www.w3.org/2000/svg"
835 aria-hidden="true"
836 >
837 <circle
838 cx="7"
839 cy="7"
840 r="5"
841 stroke="currentColor"
842 stroke-width="1.6"
843 />
844 <path
845 d="M11 11L14 14"
846 stroke="currentColor"
847 stroke-width="1.6"
848 stroke-linecap="round"
849 />
850 </svg>
851 <input
852 type="search"
853 name="q"
854 value={q}
855 placeholder="Search repos by name or description…"
856 aria-label="Search repositories"
857 autocomplete="off"
858 />
859 </div>
860 <button type="submit" class="btn btn-primary">
861 Search
862 </button>
863 </form>
864 </div>
865 </section>
283fbc2Claude866
48c9efcClaude867 {topic && (
868 <div class="explore-topic-row">
869 <span>Filtering by topic:</span>
870 <span class="explore-topic-pill">
871 <span aria-hidden="true">#</span>
872 {topic}
283fbc2Claude873 </span>
48c9efcClaude874 <a href="/explore" class="explore-topic-clear">
875 Clear filter
876 </a>
877 </div>
878 )}
283fbc2Claude879
48c9efcClaude880 <div class="explore-toolbar">
881 <div
882 class="explore-filters"
883 role="tablist"
884 aria-label="Sort repositories"
283fbc2Claude885 >
48c9efcClaude886 <a
887 class={`explore-filter${sort === "stars" ? " is-active" : ""}`}
888 href="/explore?sort=stars"
889 role="tab"
890 aria-selected={sort === "stars" ? "true" : "false"}
891 >
892 <svg
893 class="explore-filter-icon"
894 viewBox="0 0 16 16"
895 fill="currentColor"
896 aria-hidden="true"
897 >
898 <path d="M8 1.5l1.95 4.02 4.43.65-3.21 3.12.76 4.41L8 11.62l-3.93 2.08.76-4.41L1.62 6.17l4.43-.65L8 1.5z" />
899 </svg>
900 Trending
901 </a>
902 <a
903 class={`explore-filter${sort === "forks" ? " is-active" : ""}`}
904 href="/explore?sort=forks"
905 role="tab"
906 aria-selected={sort === "forks" ? "true" : "false"}
907 >
908 <svg
909 class="explore-filter-icon"
910 viewBox="0 0 16 16"
911 fill="none"
912 stroke="currentColor"
913 stroke-width="1.6"
914 aria-hidden="true"
915 >
916 <circle cx="4" cy="3" r="1.6" />
917 <circle cx="12" cy="3" r="1.6" />
918 <circle cx="8" cy="13" r="1.6" />
919 <path d="M4 4.5v2.5a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2V4.5M8 9v2.5" stroke-linecap="round" />
920 </svg>
921 Most-forked
922 </a>
923 <a
924 class={`explore-filter${sort === "recent" ? " is-active" : ""}`}
925 href="/explore?sort=recent"
926 role="tab"
927 aria-selected={sort === "recent" ? "true" : "false"}
928 >
929 <svg
930 class="explore-filter-icon"
931 viewBox="0 0 16 16"
932 fill="none"
933 stroke="currentColor"
934 stroke-width="1.6"
935 aria-hidden="true"
936 >
937 <circle cx="8" cy="8" r="6" />
938 <path d="M8 4.5V8l2.5 1.5" stroke-linecap="round" />
939 </svg>
940 Recently active
283fbc2Claude941 </a>
48c9efcClaude942 </div>
943 <div class="explore-toolbar-meta">
944 {repoList.length > 0 ? (
945 <span>
946 <strong>{repoList.length}</strong> repo
947 {repoList.length === 1 ? "" : "s"}
948 {q ? <> matching "{q}"</> : <> · sorted by {sortLabel}</>}
949 </span>
950 ) : null}
283fbc2Claude951 </div>
952 </div>
48c9efcClaude953
954 {repoList.length === 0 ? (
955 <div class="explore-empty">
956 <svg
957 class="explore-empty-art"
958 viewBox="0 0 96 96"
959 fill="none"
960 xmlns="http://www.w3.org/2000/svg"
961 aria-hidden="true"
283fbc2Claude962 >
48c9efcClaude963 <defs>
964 <linearGradient
965 id="exploreEmptyG"
966 x1="0"
967 y1="0"
968 x2="96"
969 y2="96"
970 gradientUnits="userSpaceOnUse"
971 >
972 <stop stop-color="#8c6dff" />
973 <stop offset="1" stop-color="#36c5d6" />
974 </linearGradient>
975 </defs>
976 <circle
977 cx="42"
978 cy="42"
979 r="22"
980 stroke="url(#exploreEmptyG)"
981 stroke-width="4"
982 />
983 <path
984 d="M58 58L78 78"
985 stroke="url(#exploreEmptyG)"
986 stroke-width="4"
987 stroke-linecap="round"
988 />
989 <circle
990 cx="42"
991 cy="42"
992 r="8"
993 stroke="url(#exploreEmptyG)"
994 stroke-width="3"
995 opacity="0.55"
996 />
997 </svg>
998 <h2 class="explore-empty-title">
999 {q || topic ? "No matching repos" : "Nothing public yet"}
1000 </h2>
1001 <p class="explore-empty-sub">
1002 {q
1003 ? `We couldn't find any public repositories matching "${q}". Try a broader search or browse all repos.`
1004 : topic
1005 ? `No public repositories are tagged with #${topic}. Try a different topic or browse everything.`
1006 : "When public repositories are created they'll show up here. Be the first to ship something."}
1007 </p>
1008 <div class="explore-empty-cta">
1009 <a href="/explore" class="btn btn-primary">
1010 Reset filters
1011 </a>
1012 {user ? (
1013 <a href="/new" class="btn">
1014 New repository
1015 </a>
1016 ) : (
1017 <a href="/register" class="btn">
1018 Get started
1019 </a>
283fbc2Claude1020 )}
48c9efcClaude1021 </div>
1022 </div>
1023 ) : (
1024 <>
1025 <div class="explore-section-head">
1026 <h2 class="explore-section-title">
1027 <span
1028 class="explore-section-title-bar"
1029 aria-hidden="true"
1030 />
1031 {sectionTitle}
1032 </h2>
1033 <span class="explore-section-sub">{sectionSub}</span>
1034 </div>
1035 <div class="explore-grid">
1036 {repoList.map(({ repo, ownerName }) => (
1037 <a
1038 class="explore-card"
1039 href={`/${ownerName}/${repo.name}`}
1040 aria-label={`${ownerName}/${repo.name}`}
1041 >
1042 <div class="explore-card-head">
1043 <div class="explore-card-avatar" aria-hidden="true">
1044 {initials(ownerName)}
1045 </div>
1046 <div class="explore-card-name">
1047 <span class="explore-card-owner">{ownerName}/</span>
1048 <span class="explore-card-repo">{repo.name}</span>
1049 </div>
1050 </div>
1051 {repo.description ? (
1052 <p class="explore-card-desc">{repo.description}</p>
1053 ) : (
1054 <p class="explore-card-desc-empty">No description yet.</p>
283fbc2Claude1055 )}
48c9efcClaude1056 {(repo.isPrivate ||
1057 repo.forkedFromId ||
1058 repo.isArchived ||
1059 repo.isTemplate) && (
1060 <div class="explore-card-badges">
1061 {repo.isPrivate && (
1062 <span class="explore-pill explore-pill-private">
1063 Private
1064 </span>
1065 )}
1066 {repo.forkedFromId && (
1067 <span class="explore-pill explore-pill-fork">
1068 <span aria-hidden="true">{"⑂"}</span> Fork
1069 </span>
1070 )}
1071 {repo.isTemplate && (
1072 <span class="explore-pill explore-pill-template">
1073 Template
1074 </span>
1075 )}
1076 {repo.isArchived && (
1077 <span class="explore-pill explore-pill-archived">
1078 Archived
1079 </span>
1080 )}
1081 </div>
283fbc2Claude1082 )}
48c9efcClaude1083 <div class="explore-card-meta">
1084 <span class="explore-card-meta-item" title="Stars">
1085 <svg
1086 class="explore-card-meta-icon"
1087 viewBox="0 0 16 16"
1088 fill="currentColor"
1089 aria-hidden="true"
1090 >
1091 <path d="M8 1.5l1.95 4.02 4.43.65-3.21 3.12.76 4.41L8 11.62l-3.93 2.08.76-4.41L1.62 6.17l4.43-.65L8 1.5z" />
1092 </svg>
1093 {repo.starCount}
283fbc2Claude1094 </span>
48c9efcClaude1095 <span class="explore-card-meta-item" title="Forks">
1096 <svg
1097 class="explore-card-meta-icon"
1098 viewBox="0 0 16 16"
1099 fill="none"
1100 stroke="currentColor"
1101 stroke-width="1.6"
1102 aria-hidden="true"
1103 >
1104 <circle cx="4" cy="3" r="1.4" />
1105 <circle cx="12" cy="3" r="1.4" />
1106 <circle cx="8" cy="13" r="1.4" />
1107 <path
1108 d="M4 4.5v2.5a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2V4.5M8 9v2.5"
1109 stroke-linecap="round"
1110 />
1111 </svg>
1112 {repo.forkCount}
283fbc2Claude1113 </span>
48c9efcClaude1114 {repo.pushedAt && (
1115 <span class="explore-card-meta-item" title="Last push">
1116 <svg
1117 class="explore-card-meta-icon"
1118 viewBox="0 0 16 16"
1119 fill="none"
1120 stroke="currentColor"
1121 stroke-width="1.6"
1122 aria-hidden="true"
1123 >
1124 <circle cx="8" cy="8" r="6" />
1125 <path d="M8 4.5V8l2.5 1.5" stroke-linecap="round" />
1126 </svg>
1127 {formatRelative(repo.pushedAt.toString())}
1128 </span>
1129 )}
1130 </div>
1131 </a>
1132 ))}
1133 </div>
1134
1135 {repoList.length >= 50 && (
1136 <div class="explore-foot">
1137 <p class="explore-foot-text">
1138 Showing the first <strong>50</strong> matches. Refine with
1139 search or a topic filter to surface more repositories.
1140 </p>
1141 <div class="explore-foot-actions">
1142 <a href="/explore" class="btn">
1143 Reset
1144 </a>
1145 {user ? (
1146 <a href="/new" class="btn btn-primary">
1147 New repository
1148 </a>
1149 ) : (
1150 <a href="/register" class="btn btn-primary">
1151 Sign up
1152 </a>
283fbc2Claude1153 )}
1154 </div>
1155 </div>
48c9efcClaude1156 )}
1157 </>
1158 )}
1159 </div>
c81ab7aClaude1160 </Layout>
1161 );
1162});
1163
1164export default explore;