Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
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.tsxBlame1178 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; }
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(
5acce80Claude651 <Layout
652 title="Explore"
653 user={user}
654 description="Discover open-source projects hosted on Gluecron — the AI-native GitHub alternative."
655 ogTitle="Explore — Gluecron"
656 ogDescription="Discover open-source projects hosted on Gluecron — the AI-native GitHub alternative."
657 twitterCard="summary"
658 >
f1dc7c7Claude659 <ExploreStyle />
660 <div class="explore-wrap">
661 <section class="explore-hero" aria-hidden="true">
662 <div class="explore-hero-bg">
663 <div class="explore-hero-orb" />
664 </div>
665 <div class="explore-hero-orb-2" />
666 <div class="explore-hero-inner">
667 <div class="explore-hero-text">
668 <div class="explore-hero-eyebrow">
669 <span class="explore-hero-eyebrow-dot" />
670 Discover
671 </div>
672 <h1 class="explore-hero-title">
673 What&rsquo;s{" "}
674 <span class="explore-hero-title-grad">shipping</span>.
675 </h1>
676 <p class="explore-hero-sub">
677 Loading public repositories…
678 </p>
679 </div>
680 </div>
681 </section>
682 <div class="explore-skel-grid" aria-hidden="true">
683 {Array.from({ length: 9 }).map(() => (
684 <div class="explore-skel">
685 <div class="explore-skel-row is-title" />
686 <div class="explore-skel-row is-line" />
687 <div class="explore-skel-row is-line" />
688 <div class="explore-skel-row is-short" />
689 </div>
690 ))}
691 </div>
692 <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">
693 Loading public repositories…
694 </span>
695 </div>
696 </Layout>
697 );
698 }
699
c81ab7aClaude700 let repoList: Array<{
701 repo: typeof repositories.$inferSelect;
702 ownerName: string;
703 }> = [];
704
705 if (q.trim()) {
706 // Search repos
707 const results = await db
708 .select({
709 repo: repositories,
710 ownerName: users.username,
711 })
712 .from(repositories)
713 .innerJoin(users, eq(repositories.ownerId, users.id))
714 .where(
715 and(
716 eq(repositories.isPrivate, false),
717 sql`(${repositories.name} ILIKE ${'%' + q + '%'} OR ${repositories.description} ILIKE ${'%' + q + '%'})`
718 )
719 )
720 .orderBy(desc(repositories.starCount))
721 .limit(50);
722
723 repoList = results.map((r) => ({
724 repo: r.repo,
725 ownerName: r.ownerName,
726 }));
727 } else if (topic) {
728 // Filter by topic
729 const results = await db
730 .select({
731 repo: repositories,
732 ownerName: users.username,
733 })
734 .from(repositories)
735 .innerJoin(users, eq(repositories.ownerId, users.id))
736 .innerJoin(repoTopics, eq(repoTopics.repositoryId, repositories.id))
737 .where(
738 and(
739 eq(repositories.isPrivate, false),
740 eq(repoTopics.topic, topic.toLowerCase())
741 )
742 )
743 .orderBy(desc(repositories.starCount))
744 .limit(50);
745
746 repoList = results.map((r) => ({
747 repo: r.repo,
748 ownerName: r.ownerName,
749 }));
750 } else {
751 // Default: recent or popular
752 const orderBy =
753 sort === "stars"
754 ? desc(repositories.starCount)
755 : sort === "forks"
756 ? desc(repositories.forkCount)
757 : desc(repositories.createdAt);
758
759 const results = await db
760 .select({
761 repo: repositories,
762 ownerName: users.username,
763 })
764 .from(repositories)
765 .innerJoin(users, eq(repositories.ownerId, users.id))
766 .where(eq(repositories.isPrivate, false))
767 .orderBy(orderBy)
768 .limit(50);
769
770 repoList = results.map((r) => ({
771 repo: r.repo,
772 ownerName: r.ownerName,
773 }));
774 }
775
283fbc2Claude776 const sortLabel =
777 sort === "stars"
778 ? "most-starred"
779 : sort === "forks"
780 ? "most-forked"
781 : "most recent";
782
48c9efcClaude783 const sectionTitle = q
784 ? `Search results`
785 : topic
786 ? `Tagged #${topic}`
787 : sort === "stars"
788 ? `Trending repositories`
789 : sort === "forks"
790 ? `Most-forked repositories`
791 : `Recently active`;
792
793 const sectionSub = q
794 ? `Public repositories matching "${q}"`
795 : topic
796 ? `Public repositories tagged with this topic`
797 : sort === "stars"
798 ? `Sorted by stars across the platform`
799 : sort === "forks"
800 ? `Sorted by fork count across the platform`
801 : `Sorted by repository creation date`;
802
c81ab7aClaude803 return c.html(
5acce80Claude804 <Layout
805 title="Explore"
806 user={user}
807 description="Discover open-source projects hosted on Gluecron — the AI-native GitHub alternative."
808 ogTitle="Explore — Gluecron"
809 ogDescription="Discover open-source projects hosted on Gluecron — the AI-native GitHub alternative."
810 twitterCard="summary"
811 >
283fbc2Claude812 <ExploreStyle />
48c9efcClaude813 <div class="explore-wrap">
814 <section class="explore-hero">
815 <div class="explore-hero-bg" aria-hidden="true">
816 <div class="explore-hero-orb" />
283fbc2Claude817 </div>
48c9efcClaude818 <div class="explore-hero-orb-2" aria-hidden="true" />
819 <div class="explore-hero-inner">
820 <div class="explore-hero-text">
821 <div class="explore-hero-eyebrow">
822 <span class="explore-hero-eyebrow-dot" aria-hidden="true" />
823 Discover
824 </div>
825 <h1 class="explore-hero-title">
826 What&rsquo;s{" "}
827 <span class="explore-hero-title-grad">shipping</span>.
828 </h1>
829 <p class="explore-hero-sub">
830 {q
831 ? `Searching public repos for "${q}".`
832 : topic
833 ? `Public repos tagged with #${topic}.`
834 : "Browse public repositories built on Gluecron — AI-reviewed code, gate-checked pushes, and the people shipping them."}
835 </p>
836 </div>
837 <form
838 method="get"
839 action="/explore"
840 class="explore-hero-search"
841 role="search"
842 >
843 <div class="explore-hero-search-field">
844 <svg
845 class="explore-hero-search-icon"
846 viewBox="0 0 16 16"
847 fill="none"
848 xmlns="http://www.w3.org/2000/svg"
849 aria-hidden="true"
850 >
851 <circle
852 cx="7"
853 cy="7"
854 r="5"
855 stroke="currentColor"
856 stroke-width="1.6"
857 />
858 <path
859 d="M11 11L14 14"
860 stroke="currentColor"
861 stroke-width="1.6"
862 stroke-linecap="round"
863 />
864 </svg>
865 <input
866 type="search"
867 name="q"
868 value={q}
869 placeholder="Search repos by name or description…"
870 aria-label="Search repositories"
871 autocomplete="off"
872 />
873 </div>
874 <button type="submit" class="btn btn-primary">
875 Search
876 </button>
877 </form>
878 </div>
879 </section>
283fbc2Claude880
48c9efcClaude881 {topic && (
882 <div class="explore-topic-row">
883 <span>Filtering by topic:</span>
884 <span class="explore-topic-pill">
885 <span aria-hidden="true">#</span>
886 {topic}
283fbc2Claude887 </span>
48c9efcClaude888 <a href="/explore" class="explore-topic-clear">
889 Clear filter
890 </a>
891 </div>
892 )}
283fbc2Claude893
48c9efcClaude894 <div class="explore-toolbar">
895 <div
896 class="explore-filters"
897 role="tablist"
898 aria-label="Sort repositories"
283fbc2Claude899 >
48c9efcClaude900 <a
901 class={`explore-filter${sort === "stars" ? " is-active" : ""}`}
902 href="/explore?sort=stars"
903 role="tab"
904 aria-selected={sort === "stars" ? "true" : "false"}
905 >
906 <svg
907 class="explore-filter-icon"
908 viewBox="0 0 16 16"
909 fill="currentColor"
910 aria-hidden="true"
911 >
912 <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" />
913 </svg>
914 Trending
915 </a>
916 <a
917 class={`explore-filter${sort === "forks" ? " is-active" : ""}`}
918 href="/explore?sort=forks"
919 role="tab"
920 aria-selected={sort === "forks" ? "true" : "false"}
921 >
922 <svg
923 class="explore-filter-icon"
924 viewBox="0 0 16 16"
925 fill="none"
926 stroke="currentColor"
927 stroke-width="1.6"
928 aria-hidden="true"
929 >
930 <circle cx="4" cy="3" r="1.6" />
931 <circle cx="12" cy="3" r="1.6" />
932 <circle cx="8" cy="13" r="1.6" />
933 <path d="M4 4.5v2.5a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2V4.5M8 9v2.5" stroke-linecap="round" />
934 </svg>
935 Most-forked
936 </a>
937 <a
938 class={`explore-filter${sort === "recent" ? " is-active" : ""}`}
939 href="/explore?sort=recent"
940 role="tab"
941 aria-selected={sort === "recent" ? "true" : "false"}
942 >
943 <svg
944 class="explore-filter-icon"
945 viewBox="0 0 16 16"
946 fill="none"
947 stroke="currentColor"
948 stroke-width="1.6"
949 aria-hidden="true"
950 >
951 <circle cx="8" cy="8" r="6" />
952 <path d="M8 4.5V8l2.5 1.5" stroke-linecap="round" />
953 </svg>
954 Recently active
283fbc2Claude955 </a>
48c9efcClaude956 </div>
957 <div class="explore-toolbar-meta">
958 {repoList.length > 0 ? (
959 <span>
960 <strong>{repoList.length}</strong> repo
961 {repoList.length === 1 ? "" : "s"}
962 {q ? <> matching "{q}"</> : <> · sorted by {sortLabel}</>}
963 </span>
964 ) : null}
283fbc2Claude965 </div>
966 </div>
48c9efcClaude967
968 {repoList.length === 0 ? (
969 <div class="explore-empty">
970 <svg
971 class="explore-empty-art"
972 viewBox="0 0 96 96"
973 fill="none"
974 xmlns="http://www.w3.org/2000/svg"
975 aria-hidden="true"
283fbc2Claude976 >
48c9efcClaude977 <defs>
978 <linearGradient
979 id="exploreEmptyG"
980 x1="0"
981 y1="0"
982 x2="96"
983 y2="96"
984 gradientUnits="userSpaceOnUse"
985 >
986 <stop stop-color="#8c6dff" />
987 <stop offset="1" stop-color="#36c5d6" />
988 </linearGradient>
989 </defs>
990 <circle
991 cx="42"
992 cy="42"
993 r="22"
994 stroke="url(#exploreEmptyG)"
995 stroke-width="4"
996 />
997 <path
998 d="M58 58L78 78"
999 stroke="url(#exploreEmptyG)"
1000 stroke-width="4"
1001 stroke-linecap="round"
1002 />
1003 <circle
1004 cx="42"
1005 cy="42"
1006 r="8"
1007 stroke="url(#exploreEmptyG)"
1008 stroke-width="3"
1009 opacity="0.55"
1010 />
1011 </svg>
1012 <h2 class="explore-empty-title">
1013 {q || topic ? "No matching repos" : "Nothing public yet"}
1014 </h2>
1015 <p class="explore-empty-sub">
1016 {q
1017 ? `We couldn't find any public repositories matching "${q}". Try a broader search or browse all repos.`
1018 : topic
1019 ? `No public repositories are tagged with #${topic}. Try a different topic or browse everything.`
1020 : "When public repositories are created they'll show up here. Be the first to ship something."}
1021 </p>
1022 <div class="explore-empty-cta">
1023 <a href="/explore" class="btn btn-primary">
1024 Reset filters
1025 </a>
1026 {user ? (
1027 <a href="/new" class="btn">
1028 New repository
1029 </a>
1030 ) : (
1031 <a href="/register" class="btn">
1032 Get started
1033 </a>
283fbc2Claude1034 )}
48c9efcClaude1035 </div>
1036 </div>
1037 ) : (
1038 <>
1039 <div class="explore-section-head">
1040 <h2 class="explore-section-title">
1041 <span
1042 class="explore-section-title-bar"
1043 aria-hidden="true"
1044 />
1045 {sectionTitle}
1046 </h2>
1047 <span class="explore-section-sub">{sectionSub}</span>
1048 </div>
1049 <div class="explore-grid">
1050 {repoList.map(({ repo, ownerName }) => (
1051 <a
1052 class="explore-card"
1053 href={`/${ownerName}/${repo.name}`}
1054 aria-label={`${ownerName}/${repo.name}`}
1055 >
1056 <div class="explore-card-head">
1057 <div class="explore-card-avatar" aria-hidden="true">
1058 {initials(ownerName)}
1059 </div>
1060 <div class="explore-card-name">
1061 <span class="explore-card-owner">{ownerName}/</span>
1062 <span class="explore-card-repo">{repo.name}</span>
1063 </div>
1064 </div>
1065 {repo.description ? (
1066 <p class="explore-card-desc">{repo.description}</p>
1067 ) : (
1068 <p class="explore-card-desc-empty">No description yet.</p>
283fbc2Claude1069 )}
48c9efcClaude1070 {(repo.isPrivate ||
1071 repo.forkedFromId ||
1072 repo.isArchived ||
1073 repo.isTemplate) && (
1074 <div class="explore-card-badges">
1075 {repo.isPrivate && (
1076 <span class="explore-pill explore-pill-private">
1077 Private
1078 </span>
1079 )}
1080 {repo.forkedFromId && (
1081 <span class="explore-pill explore-pill-fork">
1082 <span aria-hidden="true">{"⑂"}</span> Fork
1083 </span>
1084 )}
1085 {repo.isTemplate && (
1086 <span class="explore-pill explore-pill-template">
1087 Template
1088 </span>
1089 )}
1090 {repo.isArchived && (
1091 <span class="explore-pill explore-pill-archived">
1092 Archived
1093 </span>
1094 )}
1095 </div>
283fbc2Claude1096 )}
48c9efcClaude1097 <div class="explore-card-meta">
1098 <span class="explore-card-meta-item" title="Stars">
1099 <svg
1100 class="explore-card-meta-icon"
1101 viewBox="0 0 16 16"
1102 fill="currentColor"
1103 aria-hidden="true"
1104 >
1105 <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" />
1106 </svg>
1107 {repo.starCount}
283fbc2Claude1108 </span>
48c9efcClaude1109 <span class="explore-card-meta-item" title="Forks">
1110 <svg
1111 class="explore-card-meta-icon"
1112 viewBox="0 0 16 16"
1113 fill="none"
1114 stroke="currentColor"
1115 stroke-width="1.6"
1116 aria-hidden="true"
1117 >
1118 <circle cx="4" cy="3" r="1.4" />
1119 <circle cx="12" cy="3" r="1.4" />
1120 <circle cx="8" cy="13" r="1.4" />
1121 <path
1122 d="M4 4.5v2.5a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2V4.5M8 9v2.5"
1123 stroke-linecap="round"
1124 />
1125 </svg>
1126 {repo.forkCount}
283fbc2Claude1127 </span>
48c9efcClaude1128 {repo.pushedAt && (
1129 <span class="explore-card-meta-item" title="Last push">
1130 <svg
1131 class="explore-card-meta-icon"
1132 viewBox="0 0 16 16"
1133 fill="none"
1134 stroke="currentColor"
1135 stroke-width="1.6"
1136 aria-hidden="true"
1137 >
1138 <circle cx="8" cy="8" r="6" />
1139 <path d="M8 4.5V8l2.5 1.5" stroke-linecap="round" />
1140 </svg>
1141 {formatRelative(repo.pushedAt.toString())}
1142 </span>
1143 )}
1144 </div>
1145 </a>
1146 ))}
1147 </div>
1148
1149 {repoList.length >= 50 && (
1150 <div class="explore-foot">
1151 <p class="explore-foot-text">
1152 Showing the first <strong>50</strong> matches. Refine with
1153 search or a topic filter to surface more repositories.
1154 </p>
1155 <div class="explore-foot-actions">
1156 <a href="/explore" class="btn">
1157 Reset
1158 </a>
1159 {user ? (
1160 <a href="/new" class="btn btn-primary">
1161 New repository
1162 </a>
1163 ) : (
1164 <a href="/register" class="btn btn-primary">
1165 Sign up
1166 </a>
283fbc2Claude1167 )}
1168 </div>
1169 </div>
48c9efcClaude1170 )}
1171 </>
1172 )}
1173 </div>
c81ab7aClaude1174 </Layout>
1175 );
1176});
1177
1178export default explore;