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.tsxBlame1084 lines · 2 contributors
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";
f6730d0ccantynz-alt22import { PageHeader, Badge, Button, sharedComponentStyles } from "../views/components";
c81ab7aClaude23import { softAuth } from "../middleware/auth";
24import type { AuthEnv } from "../middleware/auth";
25
26const explore = new Hono<AuthEnv>();
27
28explore.use("*", softAuth);
29
283fbc2Claude30const ExploreStyle = () => (
31 <style
32 dangerouslySetInnerHTML={{
33 __html: `
eed4684Claude34 .explore-wrap { max-width: 1680px; margin: 0 auto; }
48c9efcClaude35
f6730d0ccantynz-alt36 /* ─── Hero search (header now rendered via shared <PageHeader>) ─── */
283fbc2Claude37 @keyframes exploreHeroOrb {
48c9efcClaude38 0%, 100% { transform: scale(1) translate(0, 0); opacity: 0.65; }
39 50% { transform: scale(1.12) translate(-14px, 10px); opacity: 0.92; }
40 }
283fbc2Claude41 .explore-hero-search {
42 display: flex;
43 gap: 10px;
44 align-items: stretch;
45 flex-wrap: wrap;
48c9efcClaude46 margin-top: 6px;
283fbc2Claude47 }
48c9efcClaude48 .explore-hero-search-field {
49 position: relative;
283fbc2Claude50 flex: 1;
51 min-width: 240px;
48c9efcClaude52 }
53 .explore-hero-search-icon {
54 position: absolute;
55 left: 14px;
56 top: 50%;
57 transform: translateY(-50%);
58 width: 16px; height: 16px;
59 color: var(--text-faint);
60 pointer-events: none;
61 z-index: 1;
62 }
63 .explore-hero-search input[type="search"] {
64 width: 100%;
65 padding: 12px 16px 12px 40px;
283fbc2Claude66 background: var(--bg);
67 border: 1px solid var(--border);
68 border-radius: 12px;
69 color: var(--text);
70 font-size: 14.5px;
71 font-family: inherit;
48c9efcClaude72 transition: border-color 140ms ease, box-shadow 140ms ease, background 140ms ease;
73 }
74 .explore-hero-search input[type="search"]::placeholder {
75 color: var(--text-faint);
283fbc2Claude76 }
77 .explore-hero-search input:focus {
78 outline: none;
6fd5915Claude79 border-color: rgba(91,110,232,0.65);
80 box-shadow: 0 0 0 4px rgba(91,110,232,0.14);
48c9efcClaude81 background: var(--bg-secondary);
283fbc2Claude82 }
48c9efcClaude83 .explore-hero-search .btn { padding: 12px 22px; border-radius: 12px; }
283fbc2Claude84 @media (max-width: 720px) {
85 .explore-hero-search .btn { flex: 1; min-width: 0; }
86 }
87
88 /* ─── Toolbar / Filter pills ─── */
89 .explore-toolbar {
90 display: flex;
91 align-items: center;
92 justify-content: space-between;
93 gap: 12px;
94 flex-wrap: wrap;
48c9efcClaude95 margin: 0 0 20px;
283fbc2Claude96 }
97 .explore-filters {
98 display: inline-flex;
99 background: var(--bg-elevated);
100 border: 1px solid var(--border);
101 border-radius: 9999px;
102 padding: 4px;
103 gap: 2px;
48c9efcClaude104 box-shadow: 0 1px 0 rgba(255,255,255,0.02) inset;
283fbc2Claude105 }
106 .explore-filter {
107 display: inline-flex;
108 align-items: center;
109 gap: 6px;
48c9efcClaude110 padding: 8px 16px;
283fbc2Claude111 border-radius: 9999px;
112 font-size: 13px;
113 font-weight: 500;
114 color: var(--text-muted);
115 text-decoration: none;
48c9efcClaude116 transition: color 120ms ease, background 120ms ease, transform 120ms ease;
283fbc2Claude117 line-height: 1.4;
118 }
119 .explore-filter:hover { color: var(--text-strong); text-decoration: none; }
120 .explore-filter.is-active {
6fd5915Claude121 background: linear-gradient(135deg, rgba(91,110,232,0.18), rgba(95,143,160,0.14));
283fbc2Claude122 color: var(--text-strong);
6fd5915Claude123 box-shadow: inset 0 0 0 1px rgba(91,110,232,0.30);
48c9efcClaude124 }
125 .explore-filter-icon {
126 width: 13px; height: 13px;
127 display: inline-block;
128 opacity: 0.9;
283fbc2Claude129 }
130 .explore-toolbar-meta {
131 font-size: 12.5px;
132 color: var(--text-muted);
133 font-variant-numeric: tabular-nums;
134 }
135 .explore-toolbar-meta strong { color: var(--text); font-weight: 600; }
136
137 /* ─── Topic chip ─── */
138 .explore-topic-row {
139 display: flex;
140 align-items: center;
141 gap: 10px;
142 margin: 0 0 16px;
143 font-size: 13px;
144 color: var(--text-muted);
48c9efcClaude145 flex-wrap: wrap;
283fbc2Claude146 }
147 .explore-topic-pill {
148 display: inline-flex;
149 align-items: center;
150 gap: 6px;
151 padding: 4px 12px;
152 border-radius: 9999px;
6fd5915Claude153 background: rgba(91,110,232,0.12);
283fbc2Claude154 color: var(--text-strong);
6fd5915Claude155 border: 1px solid rgba(91,110,232,0.30);
283fbc2Claude156 font-size: 12.5px;
157 font-weight: 600;
158 }
159 .explore-topic-clear {
160 color: var(--text-muted);
161 text-decoration: none;
162 transition: color 120ms ease;
163 }
164 .explore-topic-clear:hover { color: var(--accent); text-decoration: none; }
165
48c9efcClaude166 /* ─── Section header (results category) ─── */
167 .explore-section-head {
168 display: flex;
169 align-items: baseline;
170 justify-content: space-between;
171 gap: 12px;
172 margin: 28px 0 14px;
173 padding: 0 2px;
174 flex-wrap: wrap;
175 }
176 .explore-section-head:first-of-type { margin-top: 4px; }
177 .explore-section-title {
178 font-family: var(--font-display);
179 font-size: 18px;
180 font-weight: 700;
181 letter-spacing: -0.018em;
182 color: var(--text-strong);
183 margin: 0;
184 display: inline-flex;
185 align-items: center;
186 gap: 10px;
187 }
188 .explore-section-title-bar {
189 display: inline-block;
190 width: 3px;
191 height: 16px;
192 border-radius: 2px;
6fd5915Claude193 background: linear-gradient(180deg, #5b6ee8, #5f8fa0);
48c9efcClaude194 }
195 .explore-section-sub {
196 font-size: 12.5px;
197 color: var(--text-faint);
198 font-variant-numeric: tabular-nums;
199 }
200
283fbc2Claude201 /* ─── Repo cards grid ─── */
202 .explore-grid {
203 display: grid;
204 grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
205 gap: 14px;
206 }
207 .explore-card {
208 position: relative;
209 display: flex;
210 flex-direction: column;
211 gap: 12px;
212 padding: 18px 20px;
213 background: var(--bg-elevated);
214 border: 1px solid var(--border);
215 border-radius: 14px;
216 text-decoration: none;
217 color: inherit;
48c9efcClaude218 overflow: hidden;
219 transition: transform 160ms ease, border-color 160ms ease, box-shadow 160ms ease;
220 }
221 /* Gradient hairline on hover */
222 .explore-card::before {
223 content: '';
224 position: absolute;
225 top: 0; left: 0; right: 0;
226 height: 2px;
6fd5915Claude227 background: linear-gradient(90deg, transparent 0%, #5b6ee8 30%, #5f8fa0 70%, transparent 100%);
48c9efcClaude228 opacity: 0;
229 transition: opacity 180ms ease;
230 pointer-events: none;
283fbc2Claude231 }
232 .explore-card:hover {
48c9efcClaude233 transform: translateY(-3px);
6fd5915Claude234 border-color: rgba(91,110,232,0.45);
235 box-shadow: 0 14px 30px -16px rgba(0,0,0,0.55), 0 0 24px -8px rgba(91,110,232,0.22);
283fbc2Claude236 text-decoration: none;
237 }
48c9efcClaude238 .explore-card:hover::before { opacity: 0.85; }
239 .explore-card:focus-visible {
240 outline: none;
6fd5915Claude241 border-color: rgba(91,110,232,0.65);
242 box-shadow: 0 0 0 3px rgba(91,110,232,0.22);
48c9efcClaude243 }
283fbc2Claude244 .explore-card-head {
245 display: flex;
246 align-items: center;
247 gap: 12px;
248 min-width: 0;
249 }
250 .explore-card-avatar {
251 width: 36px;
252 height: 36px;
253 border-radius: 9px;
254 display: inline-flex;
255 align-items: center;
256 justify-content: center;
257 font-family: var(--font-display);
258 font-weight: 700;
259 font-size: 14px;
260 color: #fff;
6fd5915Claude261 background: linear-gradient(135deg, #5b6ee8 0%, #5f8fa0 100%);
283fbc2Claude262 flex-shrink: 0;
263 letter-spacing: -0.01em;
6fd5915Claude264 box-shadow: 0 4px 12px -4px rgba(91,110,232,0.45);
283fbc2Claude265 }
266 .explore-card-name {
267 display: flex;
268 flex-direction: column;
269 min-width: 0;
270 line-height: 1.25;
271 }
272 .explore-card-owner {
273 font-size: 12px;
274 color: var(--text-muted);
275 overflow: hidden;
276 text-overflow: ellipsis;
277 white-space: nowrap;
278 }
279 .explore-card-repo {
280 font-family: var(--font-display);
281 font-size: 16px;
282 font-weight: 700;
283 color: var(--text-strong);
284 letter-spacing: -0.018em;
285 overflow: hidden;
286 text-overflow: ellipsis;
287 white-space: nowrap;
288 transition: color 120ms ease;
289 }
48c9efcClaude290 .explore-card:hover .explore-card-repo { color: var(--accent-hover); }
283fbc2Claude291 .explore-card-desc {
292 font-size: 13.5px;
293 color: var(--text-muted);
294 margin: 0;
295 line-height: 1.5;
296 display: -webkit-box;
297 -webkit-line-clamp: 2;
298 -webkit-box-orient: vertical;
299 overflow: hidden;
300 }
48c9efcClaude301 .explore-card-desc-empty {
302 font-size: 13px;
303 color: var(--text-faint);
304 margin: 0;
305 font-style: italic;
306 }
283fbc2Claude307 .explore-card-meta {
308 display: flex;
309 align-items: center;
310 gap: 14px;
311 margin-top: auto;
48c9efcClaude312 padding-top: 4px;
283fbc2Claude313 font-size: 12px;
314 color: var(--text-muted);
315 flex-wrap: wrap;
48c9efcClaude316 font-variant-numeric: tabular-nums;
283fbc2Claude317 }
318 .explore-card-meta-item {
319 display: inline-flex;
320 align-items: center;
321 gap: 5px;
322 font-variant-numeric: tabular-nums;
323 }
48c9efcClaude324 .explore-card-meta-icon {
325 width: 12px; height: 12px;
326 color: var(--text-faint);
327 display: inline-block;
328 }
329 .explore-card:hover .explore-card-meta-icon { color: var(--accent); }
283fbc2Claude330 .explore-card-badges {
331 display: flex;
332 gap: 6px;
333 flex-wrap: wrap;
334 }
48c9efcClaude335
336 /* ─── Loading skeleton (used when JS later swaps in async fragments) ─── */
337 .explore-skel-grid {
338 display: grid;
339 grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
340 gap: 14px;
341 }
342 .explore-skel {
343 padding: 18px 20px;
344 background: var(--bg-elevated);
345 border: 1px solid var(--border);
346 border-radius: 14px;
347 display: flex;
348 flex-direction: column;
349 gap: 12px;
350 min-height: 140px;
351 }
352 .explore-skel-row {
353 height: 12px;
354 border-radius: 6px;
355 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%);
356 background-size: 200% 100%;
357 animation: exploreSkelPulse 1.4s ease-in-out infinite;
358 }
359 .explore-skel-row.is-title { height: 16px; width: 60%; }
360 .explore-skel-row.is-line { width: 100%; }
361 .explore-skel-row.is-short { width: 40%; }
362 @keyframes exploreSkelPulse {
363 0% { background-position: 200% 0; opacity: 0.7; }
364 100% { background-position: -200% 0; opacity: 1; }
365 }
366 @media (prefers-reduced-motion: reduce) {
367 .explore-skel-row { animation: none; }
368 }
283fbc2Claude369
370 /* ─── Empty state ─── */
371 .explore-empty {
372 margin: 0;
48c9efcClaude373 padding: 64px 32px;
283fbc2Claude374 background: var(--bg-elevated);
48c9efcClaude375 border: 1px dashed var(--border-strong);
376 border-radius: 18px;
283fbc2Claude377 text-align: center;
378 position: relative;
379 overflow: hidden;
380 }
381 .explore-empty::before {
382 content: '';
383 position: absolute;
48c9efcClaude384 inset: -40% -20% auto auto;
385 width: 320px; height: 320px;
6fd5915Claude386 background: radial-gradient(circle, rgba(91,110,232,0.18), rgba(95,143,160,0.08) 50%, transparent 75%);
48c9efcClaude387 filter: blur(70px);
283fbc2Claude388 opacity: 0.55;
389 pointer-events: none;
48c9efcClaude390 animation: exploreHeroOrb 16s ease-in-out infinite;
283fbc2Claude391 }
392 .explore-empty-art {
393 width: 88px;
394 height: 88px;
395 margin: 0 auto 18px;
396 display: block;
48c9efcClaude397 opacity: 0.92;
398 position: relative;
399 z-index: 1;
283fbc2Claude400 }
401 .explore-empty-title {
402 font-family: var(--font-display);
48c9efcClaude403 font-size: 24px;
283fbc2Claude404 font-weight: 700;
48c9efcClaude405 letter-spacing: -0.022em;
283fbc2Claude406 color: var(--text-strong);
407 margin: 0 0 8px;
48c9efcClaude408 position: relative;
409 z-index: 1;
283fbc2Claude410 }
411 .explore-empty-sub {
412 font-size: 14.5px;
413 color: var(--text-muted);
414 line-height: 1.55;
415 margin: 0 auto 22px;
48c9efcClaude416 max-width: 480px;
417 position: relative;
418 z-index: 1;
283fbc2Claude419 }
420 .explore-empty-cta {
421 display: inline-flex;
422 gap: 10px;
423 flex-wrap: wrap;
424 justify-content: center;
48c9efcClaude425 position: relative;
426 z-index: 1;
427 }
428
429 /* ─── Footer hint (limit + suggest search) ─── */
430 .explore-foot {
431 margin-top: 28px;
432 padding: 18px 22px;
433 background: transparent;
434 border: 1px dashed var(--border);
435 border-radius: 14px;
436 display: flex;
437 align-items: center;
438 justify-content: space-between;
439 gap: 14px;
440 flex-wrap: wrap;
441 }
442 .explore-foot-text {
443 font-size: 13px;
444 color: var(--text-muted);
445 line-height: 1.5;
446 margin: 0;
447 flex: 1;
448 min-width: 200px;
449 }
450 .explore-foot-text strong {
451 color: var(--text);
452 font-weight: 600;
453 font-variant-numeric: tabular-nums;
454 }
455 .explore-foot-actions {
456 display: flex;
457 gap: 8px;
458 flex-wrap: wrap;
283fbc2Claude459 }
f1dc7c7Claude460
461 /* ─── Mobile (≤720px) ─── */
462 @media (max-width: 720px) {
463 .explore-wrap { padding: 0 2px; }
464 .explore-hero-search-field { min-width: 0; width: 100%; }
465 .explore-hero-search input[type="search"] { padding: 14px 14px 14px 38px; min-height: 44px; }
466 .explore-toolbar { flex-direction: column; align-items: stretch; }
467 .explore-filters { width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }
468 .explore-filter { min-height: 40px; padding: 10px 14px; }
469 .explore-grid { grid-template-columns: 1fr; }
470 .explore-skel-grid { grid-template-columns: 1fr; }
471 .explore-card { padding: 14px 16px; }
472 .explore-empty { padding: 40px 20px; }
473 .explore-foot { flex-direction: column; align-items: stretch; padding: 16px 18px; }
474 .explore-foot-actions { width: 100%; }
475 .explore-foot-actions .btn { flex: 1; min-width: 0; }
adf5e18Claude476 .explore-gh-callout { flex-direction: column; gap: 12px; }
477 .explore-gh-callout-cta { width: 100%; justify-content: center; }
f1dc7c7Claude478 }
adf5e18Claude479
480 /* "Coming from GitHub?" callout — bottom-of-page migration prompt */
481 .explore-gh-callout {
482 display: flex;
483 align-items: center;
484 gap: 16px;
485 margin-top: 32px;
486 padding: 20px 24px;
487 background: var(--bg-elevated);
488 border: 1px solid var(--border);
489 border-radius: 14px;
490 position: relative;
491 overflow: hidden;
492 }
493 .explore-gh-callout::before {
494 content: '';
495 position: absolute;
496 top: 0; left: 0; right: 0;
497 height: 2px;
6fd5915Claude498 background: linear-gradient(90deg, transparent 0%, #5b6ee8 30%, #5f8fa0 70%, transparent 100%);
adf5e18Claude499 opacity: 0.7;
500 pointer-events: none;
501 }
502 .explore-gh-callout-icon {
503 width: 40px; height: 40px;
504 border-radius: 10px;
6fd5915Claude505 background: rgba(91,110,232,0.10);
506 border: 1px solid rgba(91,110,232,0.20);
adf5e18Claude507 display: flex; align-items: center; justify-content: center;
508 color: var(--accent);
509 flex-shrink: 0;
510 }
511 .explore-gh-callout-body { flex: 1; min-width: 0; }
512 .explore-gh-callout-title {
513 font-size: 15px;
514 font-weight: 700;
515 color: var(--text-strong);
516 margin: 0 0 3px;
517 letter-spacing: -0.012em;
518 }
519 .explore-gh-callout-sub {
520 font-size: 13.5px;
521 color: var(--text-muted);
522 margin: 0;
523 line-height: 1.5;
524 }
525 .explore-gh-callout-cta { flex-shrink: 0; white-space: nowrap; }
f6730d0ccantynz-alt526
527 ${sharedComponentStyles}
283fbc2Claude528 `,
529 }}
530 />
531);
532
533function initials(name: string): string {
534 if (!name) return "·";
535 const clean = name.replace(/[^a-zA-Z0-9]+/g, " ").trim();
536 if (!clean) return name.charAt(0).toUpperCase();
537 const parts = clean.split(/\s+/);
538 if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
539 return (parts[0][0] + parts[1][0]).toUpperCase();
540}
541
542function formatRelative(dateStr: string | Date): string {
543 const date = typeof dateStr === "string" ? new Date(dateStr) : dateStr;
544 const diffMs = Date.now() - date.getTime();
545 const diffMins = Math.floor(diffMs / 60000);
546 if (diffMins < 1) return "just now";
547 if (diffMins < 60) return `${diffMins}m ago`;
548 const diffHours = Math.floor(diffMins / 60);
549 if (diffHours < 24) return `${diffHours}h ago`;
550 const diffDays = Math.floor(diffHours / 24);
551 if (diffDays < 30) return `${diffDays}d ago`;
552 const diffMonths = Math.floor(diffDays / 30);
553 if (diffMonths < 12) return `${diffMonths}mo ago`;
554 return `${Math.floor(diffMonths / 12)}y ago`;
555}
556
c81ab7aClaude557explore.get("/explore", async (c) => {
558 const user = c.get("user");
559 const q = c.req.query("q") || "";
560 const sort = c.req.query("sort") || "recent";
561 const topic = c.req.query("topic") || "";
562
f1dc7c7Claude563 // ── Loading skeleton (flag-gated) ──
564 // Renders the explore shell + card-grid skeleton when `?skeleton=1`
565 // is set. Re-uses the existing `.explore-skel-*` styles from
566 // ExploreStyle. Behind a flag so we don't flash empty placeholders
567 // before the real list lands.
568 if (c.req.query("skeleton") === "1") {
569 return c.html(
5acce80Claude570 <Layout
571 title="Explore"
572 user={user}
573 description="Discover open-source projects hosted on Gluecron — the AI-native GitHub alternative."
574 ogTitle="Explore — Gluecron"
575 ogDescription="Discover open-source projects hosted on Gluecron — the AI-native GitHub alternative."
576 twitterCard="summary"
577 >
f1dc7c7Claude578 <ExploreStyle />
579 <div class="explore-wrap">
f6730d0ccantynz-alt580 <PageHeader
581 eyebrow="Discover"
582 title="What’s shipping."
583 lede="Loading public repositories…"
584 />
f1dc7c7Claude585 <div class="explore-skel-grid" aria-hidden="true">
586 {Array.from({ length: 9 }).map(() => (
587 <div class="explore-skel">
588 <div class="explore-skel-row is-title" />
589 <div class="explore-skel-row is-line" />
590 <div class="explore-skel-row is-line" />
591 <div class="explore-skel-row is-short" />
592 </div>
593 ))}
594 </div>
595 <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">
596 Loading public repositories…
597 </span>
598 </div>
599 </Layout>
600 );
601 }
602
c81ab7aClaude603 let repoList: Array<{
604 repo: typeof repositories.$inferSelect;
605 ownerName: string;
606 }> = [];
607
608 if (q.trim()) {
609 // Search repos
610 const results = await db
611 .select({
612 repo: repositories,
613 ownerName: users.username,
614 })
615 .from(repositories)
616 .innerJoin(users, eq(repositories.ownerId, users.id))
617 .where(
618 and(
619 eq(repositories.isPrivate, false),
620 sql`(${repositories.name} ILIKE ${'%' + q + '%'} OR ${repositories.description} ILIKE ${'%' + q + '%'})`
621 )
622 )
623 .orderBy(desc(repositories.starCount))
624 .limit(50);
625
626 repoList = results.map((r) => ({
627 repo: r.repo,
628 ownerName: r.ownerName,
629 }));
630 } else if (topic) {
631 // Filter by topic
632 const results = await db
633 .select({
634 repo: repositories,
635 ownerName: users.username,
636 })
637 .from(repositories)
638 .innerJoin(users, eq(repositories.ownerId, users.id))
639 .innerJoin(repoTopics, eq(repoTopics.repositoryId, repositories.id))
640 .where(
641 and(
642 eq(repositories.isPrivate, false),
643 eq(repoTopics.topic, topic.toLowerCase())
644 )
645 )
646 .orderBy(desc(repositories.starCount))
647 .limit(50);
648
649 repoList = results.map((r) => ({
650 repo: r.repo,
651 ownerName: r.ownerName,
652 }));
653 } else {
654 // Default: recent or popular
655 const orderBy =
656 sort === "stars"
657 ? desc(repositories.starCount)
658 : sort === "forks"
659 ? desc(repositories.forkCount)
660 : desc(repositories.createdAt);
661
662 const results = await db
663 .select({
664 repo: repositories,
665 ownerName: users.username,
666 })
667 .from(repositories)
668 .innerJoin(users, eq(repositories.ownerId, users.id))
669 .where(eq(repositories.isPrivate, false))
670 .orderBy(orderBy)
671 .limit(50);
672
673 repoList = results.map((r) => ({
674 repo: r.repo,
675 ownerName: r.ownerName,
676 }));
677 }
678
283fbc2Claude679 const sortLabel =
680 sort === "stars"
681 ? "most-starred"
682 : sort === "forks"
683 ? "most-forked"
684 : "most recent";
685
48c9efcClaude686 const sectionTitle = q
687 ? `Search results`
688 : topic
689 ? `Tagged #${topic}`
690 : sort === "stars"
691 ? `Trending repositories`
692 : sort === "forks"
693 ? `Most-forked repositories`
694 : `Recently active`;
695
696 const sectionSub = q
697 ? `Public repositories matching "${q}"`
698 : topic
699 ? `Public repositories tagged with this topic`
700 : sort === "stars"
701 ? `Sorted by stars across the platform`
702 : sort === "forks"
703 ? `Sorted by fork count across the platform`
704 : `Sorted by repository creation date`;
705
c81ab7aClaude706 return c.html(
5acce80Claude707 <Layout
708 title="Explore"
709 user={user}
710 description="Discover open-source projects hosted on Gluecron — the AI-native GitHub alternative."
711 ogTitle="Explore — Gluecron"
712 ogDescription="Discover open-source projects hosted on Gluecron — the AI-native GitHub alternative."
713 twitterCard="summary"
714 >
283fbc2Claude715 <ExploreStyle />
48c9efcClaude716 <div class="explore-wrap">
f6730d0ccantynz-alt717 <PageHeader
718 eyebrow="Discover"
719 title="What’s shipping."
720 lede={
721 q
722 ? `Searching public repos for "${q}".`
723 : topic
724 ? `Public repos tagged with #${topic}.`
725 : "Browse public repositories built on Gluecron — AI-reviewed code, gate-checked pushes, and the people shipping them."
726 }
727 actions={
48c9efcClaude728 <form
729 method="get"
730 action="/explore"
731 class="explore-hero-search"
732 role="search"
733 >
734 <div class="explore-hero-search-field">
735 <svg
736 class="explore-hero-search-icon"
737 viewBox="0 0 16 16"
738 fill="none"
739 xmlns="http://www.w3.org/2000/svg"
740 aria-hidden="true"
741 >
742 <circle
743 cx="7"
744 cy="7"
745 r="5"
746 stroke="currentColor"
747 stroke-width="1.6"
748 />
749 <path
750 d="M11 11L14 14"
751 stroke="currentColor"
752 stroke-width="1.6"
753 stroke-linecap="round"
754 />
755 </svg>
756 <input
757 type="search"
758 name="q"
759 value={q}
760 placeholder="Search repos by name or description…"
761 aria-label="Search repositories"
762 autocomplete="off"
763 />
764 </div>
765 <button type="submit" class="btn btn-primary">
766 Search
767 </button>
768 </form>
f6730d0ccantynz-alt769 }
770 />
283fbc2Claude771
48c9efcClaude772 {topic && (
773 <div class="explore-topic-row">
774 <span>Filtering by topic:</span>
775 <span class="explore-topic-pill">
776 <span aria-hidden="true">#</span>
777 {topic}
283fbc2Claude778 </span>
48c9efcClaude779 <a href="/explore" class="explore-topic-clear">
780 Clear filter
781 </a>
782 </div>
783 )}
283fbc2Claude784
48c9efcClaude785 <div class="explore-toolbar">
786 <div
787 class="explore-filters"
788 role="tablist"
789 aria-label="Sort repositories"
283fbc2Claude790 >
48c9efcClaude791 <a
792 class={`explore-filter${sort === "stars" ? " is-active" : ""}`}
793 href="/explore?sort=stars"
794 role="tab"
795 aria-selected={sort === "stars" ? "true" : "false"}
796 >
797 <svg
798 class="explore-filter-icon"
799 viewBox="0 0 16 16"
800 fill="currentColor"
801 aria-hidden="true"
802 >
803 <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" />
804 </svg>
805 Trending
806 </a>
807 <a
808 class={`explore-filter${sort === "forks" ? " is-active" : ""}`}
809 href="/explore?sort=forks"
810 role="tab"
811 aria-selected={sort === "forks" ? "true" : "false"}
812 >
813 <svg
814 class="explore-filter-icon"
815 viewBox="0 0 16 16"
816 fill="none"
817 stroke="currentColor"
818 stroke-width="1.6"
819 aria-hidden="true"
820 >
821 <circle cx="4" cy="3" r="1.6" />
822 <circle cx="12" cy="3" r="1.6" />
823 <circle cx="8" cy="13" r="1.6" />
824 <path d="M4 4.5v2.5a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2V4.5M8 9v2.5" stroke-linecap="round" />
825 </svg>
826 Most-forked
827 </a>
828 <a
829 class={`explore-filter${sort === "recent" ? " is-active" : ""}`}
830 href="/explore?sort=recent"
831 role="tab"
832 aria-selected={sort === "recent" ? "true" : "false"}
833 >
834 <svg
835 class="explore-filter-icon"
836 viewBox="0 0 16 16"
837 fill="none"
838 stroke="currentColor"
839 stroke-width="1.6"
840 aria-hidden="true"
841 >
842 <circle cx="8" cy="8" r="6" />
843 <path d="M8 4.5V8l2.5 1.5" stroke-linecap="round" />
844 </svg>
845 Recently active
283fbc2Claude846 </a>
48c9efcClaude847 </div>
848 <div class="explore-toolbar-meta">
849 {repoList.length > 0 ? (
850 <span>
851 <strong>{repoList.length}</strong> repo
852 {repoList.length === 1 ? "" : "s"}
853 {q ? <> matching "{q}"</> : <> · sorted by {sortLabel}</>}
854 </span>
855 ) : null}
283fbc2Claude856 </div>
857 </div>
48c9efcClaude858
859 {repoList.length === 0 ? (
860 <div class="explore-empty">
861 <svg
862 class="explore-empty-art"
863 viewBox="0 0 96 96"
864 fill="none"
865 xmlns="http://www.w3.org/2000/svg"
866 aria-hidden="true"
283fbc2Claude867 >
48c9efcClaude868 <defs>
869 <linearGradient
870 id="exploreEmptyG"
871 x1="0"
872 y1="0"
873 x2="96"
874 y2="96"
875 gradientUnits="userSpaceOnUse"
876 >
6fd5915Claude877 <stop stop-color="#5b6ee8" />
878 <stop offset="1" stop-color="#5f8fa0" />
48c9efcClaude879 </linearGradient>
880 </defs>
881 <circle
882 cx="42"
883 cy="42"
884 r="22"
885 stroke="url(#exploreEmptyG)"
886 stroke-width="4"
887 />
888 <path
889 d="M58 58L78 78"
890 stroke="url(#exploreEmptyG)"
891 stroke-width="4"
892 stroke-linecap="round"
893 />
894 <circle
895 cx="42"
896 cy="42"
897 r="8"
898 stroke="url(#exploreEmptyG)"
899 stroke-width="3"
900 opacity="0.55"
901 />
902 </svg>
903 <h2 class="explore-empty-title">
904 {q || topic ? "No matching repos" : "Nothing public yet"}
905 </h2>
906 <p class="explore-empty-sub">
907 {q
908 ? `We couldn't find any public repositories matching "${q}". Try a broader search or browse all repos.`
909 : topic
910 ? `No public repositories are tagged with #${topic}. Try a different topic or browse everything.`
911 : "When public repositories are created they'll show up here. Be the first to ship something."}
912 </p>
913 <div class="explore-empty-cta">
f6730d0ccantynz-alt914 <Button href="/explore" variant="primary">
48c9efcClaude915 Reset filters
f6730d0ccantynz-alt916 </Button>
48c9efcClaude917 {user ? (
f6730d0ccantynz-alt918 <Button href="/new" variant="secondary">
48c9efcClaude919 New repository
f6730d0ccantynz-alt920 </Button>
48c9efcClaude921 ) : (
f6730d0ccantynz-alt922 <Button href="/register" variant="secondary">
48c9efcClaude923 Get started
f6730d0ccantynz-alt924 </Button>
283fbc2Claude925 )}
48c9efcClaude926 </div>
927 </div>
928 ) : (
929 <>
930 <div class="explore-section-head">
931 <h2 class="explore-section-title">
932 <span
933 class="explore-section-title-bar"
934 aria-hidden="true"
935 />
936 {sectionTitle}
937 </h2>
938 <span class="explore-section-sub">{sectionSub}</span>
939 </div>
940 <div class="explore-grid">
941 {repoList.map(({ repo, ownerName }) => (
942 <a
943 class="explore-card"
944 href={`/${ownerName}/${repo.name}`}
945 aria-label={`${ownerName}/${repo.name}`}
946 >
947 <div class="explore-card-head">
948 <div class="explore-card-avatar" aria-hidden="true">
949 {initials(ownerName)}
950 </div>
951 <div class="explore-card-name">
952 <span class="explore-card-owner">{ownerName}/</span>
953 <span class="explore-card-repo">{repo.name}</span>
954 </div>
955 </div>
956 {repo.description ? (
957 <p class="explore-card-desc">{repo.description}</p>
958 ) : (
959 <p class="explore-card-desc-empty">No description yet.</p>
283fbc2Claude960 )}
48c9efcClaude961 {(repo.isPrivate ||
962 repo.forkedFromId ||
963 repo.isArchived ||
964 repo.isTemplate) && (
965 <div class="explore-card-badges">
966 {repo.isPrivate && (
f6730d0ccantynz-alt967 <Badge variant="warn">Private</Badge>
48c9efcClaude968 )}
969 {repo.forkedFromId && (
f6730d0ccantynz-alt970 <Badge variant="info">
48c9efcClaude971 <span aria-hidden="true">{"⑂"}</span> Fork
f6730d0ccantynz-alt972 </Badge>
48c9efcClaude973 )}
974 {repo.isTemplate && (
f6730d0ccantynz-alt975 <Badge variant="neutral">Template</Badge>
48c9efcClaude976 )}
977 {repo.isArchived && (
f6730d0ccantynz-alt978 <Badge variant="neutral">Archived</Badge>
48c9efcClaude979 )}
980 </div>
283fbc2Claude981 )}
48c9efcClaude982 <div class="explore-card-meta">
983 <span class="explore-card-meta-item" title="Stars">
984 <svg
985 class="explore-card-meta-icon"
986 viewBox="0 0 16 16"
987 fill="currentColor"
988 aria-hidden="true"
989 >
990 <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" />
991 </svg>
992 {repo.starCount}
283fbc2Claude993 </span>
48c9efcClaude994 <span class="explore-card-meta-item" title="Forks">
995 <svg
996 class="explore-card-meta-icon"
997 viewBox="0 0 16 16"
998 fill="none"
999 stroke="currentColor"
1000 stroke-width="1.6"
1001 aria-hidden="true"
1002 >
1003 <circle cx="4" cy="3" r="1.4" />
1004 <circle cx="12" cy="3" r="1.4" />
1005 <circle cx="8" cy="13" r="1.4" />
1006 <path
1007 d="M4 4.5v2.5a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2V4.5M8 9v2.5"
1008 stroke-linecap="round"
1009 />
1010 </svg>
1011 {repo.forkCount}
283fbc2Claude1012 </span>
48c9efcClaude1013 {repo.pushedAt && (
1014 <span class="explore-card-meta-item" title="Last push">
1015 <svg
1016 class="explore-card-meta-icon"
1017 viewBox="0 0 16 16"
1018 fill="none"
1019 stroke="currentColor"
1020 stroke-width="1.6"
1021 aria-hidden="true"
1022 >
1023 <circle cx="8" cy="8" r="6" />
1024 <path d="M8 4.5V8l2.5 1.5" stroke-linecap="round" />
1025 </svg>
1026 {formatRelative(repo.pushedAt.toString())}
1027 </span>
1028 )}
1029 </div>
1030 </a>
1031 ))}
1032 </div>
1033
1034 {repoList.length >= 50 && (
1035 <div class="explore-foot">
1036 <p class="explore-foot-text">
1037 Showing the first <strong>50</strong> matches. Refine with
1038 search or a topic filter to surface more repositories.
1039 </p>
1040 <div class="explore-foot-actions">
f6730d0ccantynz-alt1041 <Button href="/explore" variant="secondary">
48c9efcClaude1042 Reset
f6730d0ccantynz-alt1043 </Button>
48c9efcClaude1044 {user ? (
f6730d0ccantynz-alt1045 <Button href="/new" variant="primary">
48c9efcClaude1046 New repository
f6730d0ccantynz-alt1047 </Button>
48c9efcClaude1048 ) : (
f6730d0ccantynz-alt1049 <Button href="/register" variant="primary">
48c9efcClaude1050 Sign up
f6730d0ccantynz-alt1051 </Button>
283fbc2Claude1052 )}
1053 </div>
1054 </div>
48c9efcClaude1055 )}
1056 </>
1057 )}
adf5e18Claude1058 {/* "Coming from GitHub?" callout — shown to all visitors since
1059 Explore is the discovery surface most likely visited by evaluators
1060 comparing Gluecron to GitHub. Non-intrusive card at the bottom. */}
1061 <div class="explore-gh-callout" aria-label="Coming from GitHub?">
1062 <div class="explore-gh-callout-icon" aria-hidden="true">
1063 <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
1064 <polyline points="4 17 10 11 4 5" />
1065 <line x1="12" y1="19" x2="20" y2="19" />
1066 </svg>
1067 </div>
1068 <div class="explore-gh-callout-body">
1069 <p class="explore-gh-callout-title">Coming from GitHub?</p>
1070 <p class="explore-gh-callout-sub">
1071 Migrate your repos, issues, and full history in 60 seconds — no
1072 manual setup, no SaaS migration project required.
1073 </p>
1074 </div>
f6730d0ccantynz-alt1075 <Button href="/import" variant="primary" class="explore-gh-callout-cta">
adf5e18Claude1076 Migrate from GitHub →
f6730d0ccantynz-alt1077 </Button>
adf5e18Claude1078 </div>
48c9efcClaude1079 </div>
c81ab7aClaude1080 </Layout>
1081 );
1082});
1083
1084export default explore;