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.tsxBlame1248 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: `
eed4684Claude33 .explore-wrap { max-width: 1680px; margin: 0 auto; }
48c9efcClaude34
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; }
adf5e18Claude608 .explore-gh-callout { flex-direction: column; gap: 12px; }
609 .explore-gh-callout-cta { width: 100%; justify-content: center; }
f1dc7c7Claude610 }
adf5e18Claude611
612 /* "Coming from GitHub?" callout — bottom-of-page migration prompt */
613 .explore-gh-callout {
614 display: flex;
615 align-items: center;
616 gap: 16px;
617 margin-top: 32px;
618 padding: 20px 24px;
619 background: var(--bg-elevated);
620 border: 1px solid var(--border);
621 border-radius: 14px;
622 position: relative;
623 overflow: hidden;
624 }
625 .explore-gh-callout::before {
626 content: '';
627 position: absolute;
628 top: 0; left: 0; right: 0;
629 height: 2px;
630 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
631 opacity: 0.7;
632 pointer-events: none;
633 }
634 .explore-gh-callout-icon {
635 width: 40px; height: 40px;
636 border-radius: 10px;
637 background: rgba(140,109,255,0.10);
638 border: 1px solid rgba(140,109,255,0.20);
639 display: flex; align-items: center; justify-content: center;
640 color: var(--accent);
641 flex-shrink: 0;
642 }
643 .explore-gh-callout-body { flex: 1; min-width: 0; }
644 .explore-gh-callout-title {
645 font-size: 15px;
646 font-weight: 700;
647 color: var(--text-strong);
648 margin: 0 0 3px;
649 letter-spacing: -0.012em;
650 }
651 .explore-gh-callout-sub {
652 font-size: 13.5px;
653 color: var(--text-muted);
654 margin: 0;
655 line-height: 1.5;
656 }
657 .explore-gh-callout-cta { flex-shrink: 0; white-space: nowrap; }
283fbc2Claude658 `,
659 }}
660 />
661);
662
663function initials(name: string): string {
664 if (!name) return "·";
665 const clean = name.replace(/[^a-zA-Z0-9]+/g, " ").trim();
666 if (!clean) return name.charAt(0).toUpperCase();
667 const parts = clean.split(/\s+/);
668 if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
669 return (parts[0][0] + parts[1][0]).toUpperCase();
670}
671
672function formatRelative(dateStr: string | Date): string {
673 const date = typeof dateStr === "string" ? new Date(dateStr) : dateStr;
674 const diffMs = Date.now() - date.getTime();
675 const diffMins = Math.floor(diffMs / 60000);
676 if (diffMins < 1) return "just now";
677 if (diffMins < 60) return `${diffMins}m ago`;
678 const diffHours = Math.floor(diffMins / 60);
679 if (diffHours < 24) return `${diffHours}h ago`;
680 const diffDays = Math.floor(diffHours / 24);
681 if (diffDays < 30) return `${diffDays}d ago`;
682 const diffMonths = Math.floor(diffDays / 30);
683 if (diffMonths < 12) return `${diffMonths}mo ago`;
684 return `${Math.floor(diffMonths / 12)}y ago`;
685}
686
c81ab7aClaude687explore.get("/explore", async (c) => {
688 const user = c.get("user");
689 const q = c.req.query("q") || "";
690 const sort = c.req.query("sort") || "recent";
691 const topic = c.req.query("topic") || "";
692
f1dc7c7Claude693 // ── Loading skeleton (flag-gated) ──
694 // Renders the explore shell + card-grid skeleton when `?skeleton=1`
695 // is set. Re-uses the existing `.explore-skel-*` styles from
696 // ExploreStyle. Behind a flag so we don't flash empty placeholders
697 // before the real list lands.
698 if (c.req.query("skeleton") === "1") {
699 return c.html(
5acce80Claude700 <Layout
701 title="Explore"
702 user={user}
703 description="Discover open-source projects hosted on Gluecron — the AI-native GitHub alternative."
704 ogTitle="Explore — Gluecron"
705 ogDescription="Discover open-source projects hosted on Gluecron — the AI-native GitHub alternative."
706 twitterCard="summary"
707 >
f1dc7c7Claude708 <ExploreStyle />
709 <div class="explore-wrap">
710 <section class="explore-hero" aria-hidden="true">
711 <div class="explore-hero-bg">
712 <div class="explore-hero-orb" />
713 </div>
714 <div class="explore-hero-orb-2" />
715 <div class="explore-hero-inner">
716 <div class="explore-hero-text">
717 <div class="explore-hero-eyebrow">
718 <span class="explore-hero-eyebrow-dot" />
719 Discover
720 </div>
721 <h1 class="explore-hero-title">
722 What&rsquo;s{" "}
723 <span class="explore-hero-title-grad">shipping</span>.
724 </h1>
725 <p class="explore-hero-sub">
726 Loading public repositories…
727 </p>
728 </div>
729 </div>
730 </section>
731 <div class="explore-skel-grid" aria-hidden="true">
732 {Array.from({ length: 9 }).map(() => (
733 <div class="explore-skel">
734 <div class="explore-skel-row is-title" />
735 <div class="explore-skel-row is-line" />
736 <div class="explore-skel-row is-line" />
737 <div class="explore-skel-row is-short" />
738 </div>
739 ))}
740 </div>
741 <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">
742 Loading public repositories…
743 </span>
744 </div>
745 </Layout>
746 );
747 }
748
c81ab7aClaude749 let repoList: Array<{
750 repo: typeof repositories.$inferSelect;
751 ownerName: string;
752 }> = [];
753
754 if (q.trim()) {
755 // Search repos
756 const results = await db
757 .select({
758 repo: repositories,
759 ownerName: users.username,
760 })
761 .from(repositories)
762 .innerJoin(users, eq(repositories.ownerId, users.id))
763 .where(
764 and(
765 eq(repositories.isPrivate, false),
766 sql`(${repositories.name} ILIKE ${'%' + q + '%'} OR ${repositories.description} ILIKE ${'%' + q + '%'})`
767 )
768 )
769 .orderBy(desc(repositories.starCount))
770 .limit(50);
771
772 repoList = results.map((r) => ({
773 repo: r.repo,
774 ownerName: r.ownerName,
775 }));
776 } else if (topic) {
777 // Filter by topic
778 const results = await db
779 .select({
780 repo: repositories,
781 ownerName: users.username,
782 })
783 .from(repositories)
784 .innerJoin(users, eq(repositories.ownerId, users.id))
785 .innerJoin(repoTopics, eq(repoTopics.repositoryId, repositories.id))
786 .where(
787 and(
788 eq(repositories.isPrivate, false),
789 eq(repoTopics.topic, topic.toLowerCase())
790 )
791 )
792 .orderBy(desc(repositories.starCount))
793 .limit(50);
794
795 repoList = results.map((r) => ({
796 repo: r.repo,
797 ownerName: r.ownerName,
798 }));
799 } else {
800 // Default: recent or popular
801 const orderBy =
802 sort === "stars"
803 ? desc(repositories.starCount)
804 : sort === "forks"
805 ? desc(repositories.forkCount)
806 : desc(repositories.createdAt);
807
808 const results = await db
809 .select({
810 repo: repositories,
811 ownerName: users.username,
812 })
813 .from(repositories)
814 .innerJoin(users, eq(repositories.ownerId, users.id))
815 .where(eq(repositories.isPrivate, false))
816 .orderBy(orderBy)
817 .limit(50);
818
819 repoList = results.map((r) => ({
820 repo: r.repo,
821 ownerName: r.ownerName,
822 }));
823 }
824
283fbc2Claude825 const sortLabel =
826 sort === "stars"
827 ? "most-starred"
828 : sort === "forks"
829 ? "most-forked"
830 : "most recent";
831
48c9efcClaude832 const sectionTitle = q
833 ? `Search results`
834 : topic
835 ? `Tagged #${topic}`
836 : sort === "stars"
837 ? `Trending repositories`
838 : sort === "forks"
839 ? `Most-forked repositories`
840 : `Recently active`;
841
842 const sectionSub = q
843 ? `Public repositories matching "${q}"`
844 : topic
845 ? `Public repositories tagged with this topic`
846 : sort === "stars"
847 ? `Sorted by stars across the platform`
848 : sort === "forks"
849 ? `Sorted by fork count across the platform`
850 : `Sorted by repository creation date`;
851
c81ab7aClaude852 return c.html(
5acce80Claude853 <Layout
854 title="Explore"
855 user={user}
856 description="Discover open-source projects hosted on Gluecron — the AI-native GitHub alternative."
857 ogTitle="Explore — Gluecron"
858 ogDescription="Discover open-source projects hosted on Gluecron — the AI-native GitHub alternative."
859 twitterCard="summary"
860 >
283fbc2Claude861 <ExploreStyle />
48c9efcClaude862 <div class="explore-wrap">
863 <section class="explore-hero">
864 <div class="explore-hero-bg" aria-hidden="true">
865 <div class="explore-hero-orb" />
283fbc2Claude866 </div>
48c9efcClaude867 <div class="explore-hero-orb-2" aria-hidden="true" />
868 <div class="explore-hero-inner">
869 <div class="explore-hero-text">
870 <div class="explore-hero-eyebrow">
871 <span class="explore-hero-eyebrow-dot" aria-hidden="true" />
872 Discover
873 </div>
874 <h1 class="explore-hero-title">
875 What&rsquo;s{" "}
876 <span class="explore-hero-title-grad">shipping</span>.
877 </h1>
878 <p class="explore-hero-sub">
879 {q
880 ? `Searching public repos for "${q}".`
881 : topic
882 ? `Public repos tagged with #${topic}.`
883 : "Browse public repositories built on Gluecron — AI-reviewed code, gate-checked pushes, and the people shipping them."}
884 </p>
885 </div>
886 <form
887 method="get"
888 action="/explore"
889 class="explore-hero-search"
890 role="search"
891 >
892 <div class="explore-hero-search-field">
893 <svg
894 class="explore-hero-search-icon"
895 viewBox="0 0 16 16"
896 fill="none"
897 xmlns="http://www.w3.org/2000/svg"
898 aria-hidden="true"
899 >
900 <circle
901 cx="7"
902 cy="7"
903 r="5"
904 stroke="currentColor"
905 stroke-width="1.6"
906 />
907 <path
908 d="M11 11L14 14"
909 stroke="currentColor"
910 stroke-width="1.6"
911 stroke-linecap="round"
912 />
913 </svg>
914 <input
915 type="search"
916 name="q"
917 value={q}
918 placeholder="Search repos by name or description…"
919 aria-label="Search repositories"
920 autocomplete="off"
921 />
922 </div>
923 <button type="submit" class="btn btn-primary">
924 Search
925 </button>
926 </form>
927 </div>
928 </section>
283fbc2Claude929
48c9efcClaude930 {topic && (
931 <div class="explore-topic-row">
932 <span>Filtering by topic:</span>
933 <span class="explore-topic-pill">
934 <span aria-hidden="true">#</span>
935 {topic}
283fbc2Claude936 </span>
48c9efcClaude937 <a href="/explore" class="explore-topic-clear">
938 Clear filter
939 </a>
940 </div>
941 )}
283fbc2Claude942
48c9efcClaude943 <div class="explore-toolbar">
944 <div
945 class="explore-filters"
946 role="tablist"
947 aria-label="Sort repositories"
283fbc2Claude948 >
48c9efcClaude949 <a
950 class={`explore-filter${sort === "stars" ? " is-active" : ""}`}
951 href="/explore?sort=stars"
952 role="tab"
953 aria-selected={sort === "stars" ? "true" : "false"}
954 >
955 <svg
956 class="explore-filter-icon"
957 viewBox="0 0 16 16"
958 fill="currentColor"
959 aria-hidden="true"
960 >
961 <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" />
962 </svg>
963 Trending
964 </a>
965 <a
966 class={`explore-filter${sort === "forks" ? " is-active" : ""}`}
967 href="/explore?sort=forks"
968 role="tab"
969 aria-selected={sort === "forks" ? "true" : "false"}
970 >
971 <svg
972 class="explore-filter-icon"
973 viewBox="0 0 16 16"
974 fill="none"
975 stroke="currentColor"
976 stroke-width="1.6"
977 aria-hidden="true"
978 >
979 <circle cx="4" cy="3" r="1.6" />
980 <circle cx="12" cy="3" r="1.6" />
981 <circle cx="8" cy="13" r="1.6" />
982 <path d="M4 4.5v2.5a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2V4.5M8 9v2.5" stroke-linecap="round" />
983 </svg>
984 Most-forked
985 </a>
986 <a
987 class={`explore-filter${sort === "recent" ? " is-active" : ""}`}
988 href="/explore?sort=recent"
989 role="tab"
990 aria-selected={sort === "recent" ? "true" : "false"}
991 >
992 <svg
993 class="explore-filter-icon"
994 viewBox="0 0 16 16"
995 fill="none"
996 stroke="currentColor"
997 stroke-width="1.6"
998 aria-hidden="true"
999 >
1000 <circle cx="8" cy="8" r="6" />
1001 <path d="M8 4.5V8l2.5 1.5" stroke-linecap="round" />
1002 </svg>
1003 Recently active
283fbc2Claude1004 </a>
48c9efcClaude1005 </div>
1006 <div class="explore-toolbar-meta">
1007 {repoList.length > 0 ? (
1008 <span>
1009 <strong>{repoList.length}</strong> repo
1010 {repoList.length === 1 ? "" : "s"}
1011 {q ? <> matching "{q}"</> : <> · sorted by {sortLabel}</>}
1012 </span>
1013 ) : null}
283fbc2Claude1014 </div>
1015 </div>
48c9efcClaude1016
1017 {repoList.length === 0 ? (
1018 <div class="explore-empty">
1019 <svg
1020 class="explore-empty-art"
1021 viewBox="0 0 96 96"
1022 fill="none"
1023 xmlns="http://www.w3.org/2000/svg"
1024 aria-hidden="true"
283fbc2Claude1025 >
48c9efcClaude1026 <defs>
1027 <linearGradient
1028 id="exploreEmptyG"
1029 x1="0"
1030 y1="0"
1031 x2="96"
1032 y2="96"
1033 gradientUnits="userSpaceOnUse"
1034 >
1035 <stop stop-color="#8c6dff" />
1036 <stop offset="1" stop-color="#36c5d6" />
1037 </linearGradient>
1038 </defs>
1039 <circle
1040 cx="42"
1041 cy="42"
1042 r="22"
1043 stroke="url(#exploreEmptyG)"
1044 stroke-width="4"
1045 />
1046 <path
1047 d="M58 58L78 78"
1048 stroke="url(#exploreEmptyG)"
1049 stroke-width="4"
1050 stroke-linecap="round"
1051 />
1052 <circle
1053 cx="42"
1054 cy="42"
1055 r="8"
1056 stroke="url(#exploreEmptyG)"
1057 stroke-width="3"
1058 opacity="0.55"
1059 />
1060 </svg>
1061 <h2 class="explore-empty-title">
1062 {q || topic ? "No matching repos" : "Nothing public yet"}
1063 </h2>
1064 <p class="explore-empty-sub">
1065 {q
1066 ? `We couldn't find any public repositories matching "${q}". Try a broader search or browse all repos.`
1067 : topic
1068 ? `No public repositories are tagged with #${topic}. Try a different topic or browse everything.`
1069 : "When public repositories are created they'll show up here. Be the first to ship something."}
1070 </p>
1071 <div class="explore-empty-cta">
1072 <a href="/explore" class="btn btn-primary">
1073 Reset filters
1074 </a>
1075 {user ? (
1076 <a href="/new" class="btn">
1077 New repository
1078 </a>
1079 ) : (
1080 <a href="/register" class="btn">
1081 Get started
1082 </a>
283fbc2Claude1083 )}
48c9efcClaude1084 </div>
1085 </div>
1086 ) : (
1087 <>
1088 <div class="explore-section-head">
1089 <h2 class="explore-section-title">
1090 <span
1091 class="explore-section-title-bar"
1092 aria-hidden="true"
1093 />
1094 {sectionTitle}
1095 </h2>
1096 <span class="explore-section-sub">{sectionSub}</span>
1097 </div>
1098 <div class="explore-grid">
1099 {repoList.map(({ repo, ownerName }) => (
1100 <a
1101 class="explore-card"
1102 href={`/${ownerName}/${repo.name}`}
1103 aria-label={`${ownerName}/${repo.name}`}
1104 >
1105 <div class="explore-card-head">
1106 <div class="explore-card-avatar" aria-hidden="true">
1107 {initials(ownerName)}
1108 </div>
1109 <div class="explore-card-name">
1110 <span class="explore-card-owner">{ownerName}/</span>
1111 <span class="explore-card-repo">{repo.name}</span>
1112 </div>
1113 </div>
1114 {repo.description ? (
1115 <p class="explore-card-desc">{repo.description}</p>
1116 ) : (
1117 <p class="explore-card-desc-empty">No description yet.</p>
283fbc2Claude1118 )}
48c9efcClaude1119 {(repo.isPrivate ||
1120 repo.forkedFromId ||
1121 repo.isArchived ||
1122 repo.isTemplate) && (
1123 <div class="explore-card-badges">
1124 {repo.isPrivate && (
1125 <span class="explore-pill explore-pill-private">
1126 Private
1127 </span>
1128 )}
1129 {repo.forkedFromId && (
1130 <span class="explore-pill explore-pill-fork">
1131 <span aria-hidden="true">{"⑂"}</span> Fork
1132 </span>
1133 )}
1134 {repo.isTemplate && (
1135 <span class="explore-pill explore-pill-template">
1136 Template
1137 </span>
1138 )}
1139 {repo.isArchived && (
1140 <span class="explore-pill explore-pill-archived">
1141 Archived
1142 </span>
1143 )}
1144 </div>
283fbc2Claude1145 )}
48c9efcClaude1146 <div class="explore-card-meta">
1147 <span class="explore-card-meta-item" title="Stars">
1148 <svg
1149 class="explore-card-meta-icon"
1150 viewBox="0 0 16 16"
1151 fill="currentColor"
1152 aria-hidden="true"
1153 >
1154 <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" />
1155 </svg>
1156 {repo.starCount}
283fbc2Claude1157 </span>
48c9efcClaude1158 <span class="explore-card-meta-item" title="Forks">
1159 <svg
1160 class="explore-card-meta-icon"
1161 viewBox="0 0 16 16"
1162 fill="none"
1163 stroke="currentColor"
1164 stroke-width="1.6"
1165 aria-hidden="true"
1166 >
1167 <circle cx="4" cy="3" r="1.4" />
1168 <circle cx="12" cy="3" r="1.4" />
1169 <circle cx="8" cy="13" r="1.4" />
1170 <path
1171 d="M4 4.5v2.5a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2V4.5M8 9v2.5"
1172 stroke-linecap="round"
1173 />
1174 </svg>
1175 {repo.forkCount}
283fbc2Claude1176 </span>
48c9efcClaude1177 {repo.pushedAt && (
1178 <span class="explore-card-meta-item" title="Last push">
1179 <svg
1180 class="explore-card-meta-icon"
1181 viewBox="0 0 16 16"
1182 fill="none"
1183 stroke="currentColor"
1184 stroke-width="1.6"
1185 aria-hidden="true"
1186 >
1187 <circle cx="8" cy="8" r="6" />
1188 <path d="M8 4.5V8l2.5 1.5" stroke-linecap="round" />
1189 </svg>
1190 {formatRelative(repo.pushedAt.toString())}
1191 </span>
1192 )}
1193 </div>
1194 </a>
1195 ))}
1196 </div>
1197
1198 {repoList.length >= 50 && (
1199 <div class="explore-foot">
1200 <p class="explore-foot-text">
1201 Showing the first <strong>50</strong> matches. Refine with
1202 search or a topic filter to surface more repositories.
1203 </p>
1204 <div class="explore-foot-actions">
1205 <a href="/explore" class="btn">
1206 Reset
1207 </a>
1208 {user ? (
1209 <a href="/new" class="btn btn-primary">
1210 New repository
1211 </a>
1212 ) : (
1213 <a href="/register" class="btn btn-primary">
1214 Sign up
1215 </a>
283fbc2Claude1216 )}
1217 </div>
1218 </div>
48c9efcClaude1219 )}
1220 </>
1221 )}
adf5e18Claude1222 {/* "Coming from GitHub?" callout — shown to all visitors since
1223 Explore is the discovery surface most likely visited by evaluators
1224 comparing Gluecron to GitHub. Non-intrusive card at the bottom. */}
1225 <div class="explore-gh-callout" aria-label="Coming from GitHub?">
1226 <div class="explore-gh-callout-icon" aria-hidden="true">
1227 <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
1228 <polyline points="4 17 10 11 4 5" />
1229 <line x1="12" y1="19" x2="20" y2="19" />
1230 </svg>
1231 </div>
1232 <div class="explore-gh-callout-body">
1233 <p class="explore-gh-callout-title">Coming from GitHub?</p>
1234 <p class="explore-gh-callout-sub">
1235 Migrate your repos, issues, and full history in 60 seconds — no
1236 manual setup, no SaaS migration project required.
1237 </p>
1238 </div>
1239 <a href="/import" class="btn btn-primary explore-gh-callout-cta">
1240 Migrate from GitHub →
1241 </a>
1242 </div>
48c9efcClaude1243 </div>
c81ab7aClaude1244 </Layout>
1245 );
1246});
1247
1248export default explore;