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.tsxBlame710 lines · 1 contributor
c81ab7aClaude1/**
2 * Explore page — discover public repositories, search, trending.
3 */
4
5import { Hono } from "hono";
283fbc2Claude6import { eq, desc, sql, and } from "drizzle-orm";
c81ab7aClaude7import { db } from "../db";
8import { repositories, users, repoTopics } from "../db/schema";
9import { Layout } from "../views/layout";
10import { softAuth } from "../middleware/auth";
11import type { AuthEnv } from "../middleware/auth";
12
13const explore = new Hono<AuthEnv>();
14
15explore.use("*", softAuth);
16
283fbc2Claude17const ExploreStyle = () => (
18 <style
19 dangerouslySetInnerHTML={{
20 __html: `
21 /* ─── Hero ─── */
22 .explore-hero {
23 position: relative;
24 margin: 4px 0 24px;
25 padding: 28px 32px;
26 background: var(--bg-elevated);
27 border: 1px solid var(--border);
28 border-radius: 16px;
29 overflow: hidden;
30 }
31 .explore-hero::before {
32 content: '';
33 position: absolute;
34 top: 0; left: 0; right: 0;
35 height: 2px;
36 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
37 opacity: 0.7;
38 pointer-events: none;
39 }
40 .explore-hero-bg {
41 position: absolute;
42 inset: -30% -10% auto auto;
43 width: 360px;
44 height: 360px;
45 pointer-events: none;
46 z-index: 0;
47 }
48 .explore-hero-orb {
49 position: absolute;
50 inset: 0;
51 background: radial-gradient(circle, rgba(140,109,255,0.18), rgba(54,197,214,0.09) 45%, transparent 70%);
52 filter: blur(80px);
53 opacity: 0.7;
54 animation: exploreHeroOrb 14s ease-in-out infinite;
55 }
56 @keyframes exploreHeroOrb {
57 0%, 100% { transform: scale(1) translate(0, 0); opacity: 0.6; }
58 50% { transform: scale(1.1) translate(-12px, 8px); opacity: 0.85; }
59 }
60 @media (prefers-reduced-motion: reduce) {
61 .explore-hero-orb { animation: none; }
62 }
63 .explore-hero-inner {
64 position: relative;
65 z-index: 1;
66 display: flex;
67 flex-direction: column;
68 gap: 18px;
69 }
70 .explore-hero-text { flex: 1; min-width: 280px; }
71 .explore-hero-eyebrow {
72 font-size: 12.5px;
73 color: var(--text-muted);
74 margin-bottom: 8px;
75 letter-spacing: 0.06em;
76 text-transform: uppercase;
77 font-weight: 600;
78 }
79 .explore-hero-title {
80 font-family: var(--font-display);
81 font-size: clamp(28px, 4vw, 40px);
82 font-weight: 800;
83 letter-spacing: -0.028em;
84 line-height: 1.05;
85 margin: 0 0 10px;
86 color: var(--text-strong);
87 }
88 .explore-hero-sub {
89 font-size: 15px;
90 color: var(--text-muted);
91 margin: 0;
92 line-height: 1.5;
93 max-width: 620px;
94 }
95 .explore-hero-search {
96 display: flex;
97 gap: 10px;
98 align-items: stretch;
99 flex-wrap: wrap;
100 margin-top: 4px;
101 }
102 .explore-hero-search input[type="search"],
103 .explore-hero-search input[type="text"] {
104 flex: 1;
105 min-width: 240px;
106 padding: 11px 16px;
107 background: var(--bg);
108 border: 1px solid var(--border);
109 border-radius: 12px;
110 color: var(--text);
111 font-size: 14.5px;
112 font-family: inherit;
113 transition: border-color 140ms ease, box-shadow 140ms ease;
114 }
115 .explore-hero-search input:focus {
116 outline: none;
117 border-color: rgba(140,109,255,0.55);
118 box-shadow: 0 0 0 4px rgba(140,109,255,0.12);
119 }
120 .explore-hero-search .btn { padding: 11px 22px; border-radius: 12px; }
121 @media (max-width: 720px) {
122 .explore-hero { padding: 24px 20px; }
123 .explore-hero-search .btn { flex: 1; min-width: 0; }
124 }
125
126 /* ─── Toolbar / Filter pills ─── */
127 .explore-toolbar {
128 display: flex;
129 align-items: center;
130 justify-content: space-between;
131 gap: 12px;
132 flex-wrap: wrap;
133 margin: 0 0 18px;
134 }
135 .explore-filters {
136 display: inline-flex;
137 background: var(--bg-elevated);
138 border: 1px solid var(--border);
139 border-radius: 9999px;
140 padding: 4px;
141 gap: 2px;
142 }
143 .explore-filter {
144 display: inline-flex;
145 align-items: center;
146 gap: 6px;
147 padding: 7px 16px;
148 border-radius: 9999px;
149 font-size: 13px;
150 font-weight: 500;
151 color: var(--text-muted);
152 text-decoration: none;
153 transition: color 120ms ease, background 120ms ease;
154 line-height: 1.4;
155 }
156 .explore-filter:hover { color: var(--text-strong); text-decoration: none; }
157 .explore-filter.is-active {
158 background: rgba(140,109,255,0.14);
159 color: var(--text-strong);
160 }
161 .explore-toolbar-meta {
162 font-size: 12.5px;
163 color: var(--text-muted);
164 font-variant-numeric: tabular-nums;
165 }
166 .explore-toolbar-meta strong { color: var(--text); font-weight: 600; }
167
168 /* ─── Topic chip ─── */
169 .explore-topic-row {
170 display: flex;
171 align-items: center;
172 gap: 10px;
173 margin: 0 0 16px;
174 font-size: 13px;
175 color: var(--text-muted);
176 }
177 .explore-topic-pill {
178 display: inline-flex;
179 align-items: center;
180 gap: 6px;
181 padding: 4px 12px;
182 border-radius: 9999px;
183 background: rgba(140,109,255,0.12);
184 color: var(--text-strong);
185 border: 1px solid rgba(140,109,255,0.30);
186 font-size: 12.5px;
187 font-weight: 600;
188 }
189 .explore-topic-clear {
190 color: var(--text-muted);
191 text-decoration: none;
192 transition: color 120ms ease;
193 }
194 .explore-topic-clear:hover { color: var(--accent); text-decoration: none; }
195
196 /* ─── Repo cards grid ─── */
197 .explore-grid {
198 display: grid;
199 grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
200 gap: 14px;
201 }
202 .explore-card {
203 position: relative;
204 display: flex;
205 flex-direction: column;
206 gap: 12px;
207 padding: 18px 20px;
208 background: var(--bg-elevated);
209 border: 1px solid var(--border);
210 border-radius: 14px;
211 text-decoration: none;
212 color: inherit;
213 transition: transform 140ms ease, border-color 140ms ease, box-shadow 140ms ease;
214 }
215 .explore-card:hover {
216 transform: translateY(-2px);
217 border-color: rgba(140,109,255,0.45);
218 box-shadow: 0 12px 28px -16px rgba(0,0,0,0.55), 0 0 22px -8px rgba(140,109,255,0.20);
219 text-decoration: none;
220 }
221 .explore-card-head {
222 display: flex;
223 align-items: center;
224 gap: 12px;
225 min-width: 0;
226 }
227 .explore-card-avatar {
228 width: 36px;
229 height: 36px;
230 border-radius: 9px;
231 display: inline-flex;
232 align-items: center;
233 justify-content: center;
234 font-family: var(--font-display);
235 font-weight: 700;
236 font-size: 14px;
237 color: #fff;
238 background: linear-gradient(135deg, #8c6dff 0%, #36c5d6 100%);
239 flex-shrink: 0;
240 letter-spacing: -0.01em;
241 }
242 .explore-card-name {
243 display: flex;
244 flex-direction: column;
245 min-width: 0;
246 line-height: 1.25;
247 }
248 .explore-card-owner {
249 font-size: 12px;
250 color: var(--text-muted);
251 overflow: hidden;
252 text-overflow: ellipsis;
253 white-space: nowrap;
254 }
255 .explore-card-repo {
256 font-family: var(--font-display);
257 font-size: 16px;
258 font-weight: 700;
259 color: var(--text-strong);
260 letter-spacing: -0.018em;
261 overflow: hidden;
262 text-overflow: ellipsis;
263 white-space: nowrap;
264 transition: color 120ms ease;
265 }
266 .explore-card:hover .explore-card-repo { color: var(--accent); }
267 .explore-card-desc {
268 font-size: 13.5px;
269 color: var(--text-muted);
270 margin: 0;
271 line-height: 1.5;
272 display: -webkit-box;
273 -webkit-line-clamp: 2;
274 -webkit-box-orient: vertical;
275 overflow: hidden;
276 }
277 .explore-card-meta {
278 display: flex;
279 align-items: center;
280 gap: 14px;
281 margin-top: auto;
282 font-size: 12px;
283 color: var(--text-muted);
284 flex-wrap: wrap;
285 }
286 .explore-card-meta-item {
287 display: inline-flex;
288 align-items: center;
289 gap: 5px;
290 font-variant-numeric: tabular-nums;
291 }
292 .explore-card-badges {
293 display: flex;
294 gap: 6px;
295 flex-wrap: wrap;
296 }
297 .explore-pill {
298 display: inline-flex;
299 align-items: center;
300 gap: 5px;
301 padding: 2px 10px;
302 border-radius: 9999px;
303 font-size: 11.5px;
304 font-weight: 600;
305 line-height: 1.5;
306 letter-spacing: 0.005em;
307 }
308 .explore-pill-private {
309 background: rgba(255,180,94,0.10);
310 color: #ffb45e;
311 border: 1px solid rgba(255,180,94,0.28);
312 }
313 .explore-pill-fork {
314 background: rgba(54,197,214,0.10);
315 color: #36c5d6;
316 border: 1px solid rgba(54,197,214,0.28);
317 }
318 .explore-pill-archived {
319 background: rgba(255,255,255,0.04);
320 color: var(--text-muted);
321 border: 1px solid var(--border);
322 }
323 .explore-pill-template {
324 background: rgba(140,109,255,0.12);
325 color: var(--text-strong);
326 border: 1px solid rgba(140,109,255,0.30);
327 }
328 .explore-card-dot { color: rgba(140,109,255,0.7); }
329
330 /* ─── Empty state ─── */
331 .explore-empty {
332 margin: 0;
333 padding: 60px 32px;
334 background: var(--bg-elevated);
335 border: 1px solid var(--border);
336 border-radius: 16px;
337 text-align: center;
338 position: relative;
339 overflow: hidden;
340 }
341 .explore-empty::before {
342 content: '';
343 position: absolute;
344 top: 0; left: 0; right: 0;
345 height: 2px;
346 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
347 opacity: 0.55;
348 pointer-events: none;
349 }
350 .explore-empty-art {
351 width: 88px;
352 height: 88px;
353 margin: 0 auto 18px;
354 display: block;
355 opacity: 0.85;
356 }
357 .explore-empty-title {
358 font-family: var(--font-display);
359 font-size: 22px;
360 font-weight: 700;
361 letter-spacing: -0.018em;
362 color: var(--text-strong);
363 margin: 0 0 8px;
364 }
365 .explore-empty-sub {
366 font-size: 14.5px;
367 color: var(--text-muted);
368 line-height: 1.55;
369 margin: 0 auto 22px;
370 max-width: 460px;
371 }
372 .explore-empty-cta {
373 display: inline-flex;
374 gap: 10px;
375 flex-wrap: wrap;
376 justify-content: center;
377 }
378 `,
379 }}
380 />
381);
382
383function initials(name: string): string {
384 if (!name) return "·";
385 const clean = name.replace(/[^a-zA-Z0-9]+/g, " ").trim();
386 if (!clean) return name.charAt(0).toUpperCase();
387 const parts = clean.split(/\s+/);
388 if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
389 return (parts[0][0] + parts[1][0]).toUpperCase();
390}
391
392function formatRelative(dateStr: string | Date): string {
393 const date = typeof dateStr === "string" ? new Date(dateStr) : dateStr;
394 const diffMs = Date.now() - date.getTime();
395 const diffMins = Math.floor(diffMs / 60000);
396 if (diffMins < 1) return "just now";
397 if (diffMins < 60) return `${diffMins}m ago`;
398 const diffHours = Math.floor(diffMins / 60);
399 if (diffHours < 24) return `${diffHours}h ago`;
400 const diffDays = Math.floor(diffHours / 24);
401 if (diffDays < 30) return `${diffDays}d ago`;
402 const diffMonths = Math.floor(diffDays / 30);
403 if (diffMonths < 12) return `${diffMonths}mo ago`;
404 return `${Math.floor(diffMonths / 12)}y ago`;
405}
406
c81ab7aClaude407explore.get("/explore", async (c) => {
408 const user = c.get("user");
409 const q = c.req.query("q") || "";
410 const sort = c.req.query("sort") || "recent";
411 const topic = c.req.query("topic") || "";
412
413 let repoList: Array<{
414 repo: typeof repositories.$inferSelect;
415 ownerName: string;
416 }> = [];
417
418 if (q.trim()) {
419 // Search repos
420 const results = await db
421 .select({
422 repo: repositories,
423 ownerName: users.username,
424 })
425 .from(repositories)
426 .innerJoin(users, eq(repositories.ownerId, users.id))
427 .where(
428 and(
429 eq(repositories.isPrivate, false),
430 sql`(${repositories.name} ILIKE ${'%' + q + '%'} OR ${repositories.description} ILIKE ${'%' + q + '%'})`
431 )
432 )
433 .orderBy(desc(repositories.starCount))
434 .limit(50);
435
436 repoList = results.map((r) => ({
437 repo: r.repo,
438 ownerName: r.ownerName,
439 }));
440 } else if (topic) {
441 // Filter by topic
442 const results = await db
443 .select({
444 repo: repositories,
445 ownerName: users.username,
446 })
447 .from(repositories)
448 .innerJoin(users, eq(repositories.ownerId, users.id))
449 .innerJoin(repoTopics, eq(repoTopics.repositoryId, repositories.id))
450 .where(
451 and(
452 eq(repositories.isPrivate, false),
453 eq(repoTopics.topic, topic.toLowerCase())
454 )
455 )
456 .orderBy(desc(repositories.starCount))
457 .limit(50);
458
459 repoList = results.map((r) => ({
460 repo: r.repo,
461 ownerName: r.ownerName,
462 }));
463 } else {
464 // Default: recent or popular
465 const orderBy =
466 sort === "stars"
467 ? desc(repositories.starCount)
468 : sort === "forks"
469 ? desc(repositories.forkCount)
470 : desc(repositories.createdAt);
471
472 const results = await db
473 .select({
474 repo: repositories,
475 ownerName: users.username,
476 })
477 .from(repositories)
478 .innerJoin(users, eq(repositories.ownerId, users.id))
479 .where(eq(repositories.isPrivate, false))
480 .orderBy(orderBy)
481 .limit(50);
482
483 repoList = results.map((r) => ({
484 repo: r.repo,
485 ownerName: r.ownerName,
486 }));
487 }
488
283fbc2Claude489 const sortLabel =
490 sort === "stars"
491 ? "most-starred"
492 : sort === "forks"
493 ? "most-forked"
494 : "most recent";
495
c81ab7aClaude496 return c.html(
497 <Layout title="Explore" user={user}>
283fbc2Claude498 <ExploreStyle />
499 <section class="explore-hero">
500 <div class="explore-hero-bg" aria-hidden="true">
501 <div class="explore-hero-orb" />
502 </div>
503 <div class="explore-hero-inner">
504 <div class="explore-hero-text">
505 <div class="explore-hero-eyebrow">Discover</div>
506 <h1 class="explore-hero-title">
507 What's <span class="gradient-text">shipping</span>.
508 </h1>
509 <p class="explore-hero-sub">
510 {q
511 ? `Searching public repos for "${q}".`
512 : topic
513 ? `Public repos tagged with #${topic}.`
514 : "Browse public repositories built on Gluecron — AI-reviewed code, gate-checked pushes, and the people shipping them."}
515 </p>
516 </div>
517 <form
518 method="get"
519 action="/explore"
520 class="explore-hero-search"
521 role="search"
c81ab7aClaude522 >
283fbc2Claude523 <input
524 type="search"
525 name="q"
526 value={q}
527 placeholder="Search repos by name or description…"
528 aria-label="Search repositories"
529 autocomplete="off"
530 />
531 <button type="submit" class="btn btn-primary">
532 Search
533 </button>
534 </form>
535 </div>
536 </section>
537
538 {topic && (
539 <div class="explore-topic-row">
540 <span>Filtering by topic:</span>
541 <span class="explore-topic-pill">
542 <span aria-hidden="true">#</span>
543 {topic}
544 </span>
545 <a href="/explore" class="explore-topic-clear">
546 Clear filter
547 </a>
548 </div>
549 )}
550
551 <div class="explore-toolbar">
552 <div
553 class="explore-filters"
554 role="tablist"
555 aria-label="Sort repositories"
556 >
557 <a
558 class={`explore-filter${sort === "stars" ? " is-active" : ""}`}
c81ab7aClaude559 href="/explore?sort=stars"
283fbc2Claude560 role="tab"
561 aria-selected={sort === "stars" ? "true" : "false"}
c81ab7aClaude562 >
283fbc2Claude563 Trending
564 </a>
565 <a
566 class={`explore-filter${sort === "forks" ? " is-active" : ""}`}
c81ab7aClaude567 href="/explore?sort=forks"
283fbc2Claude568 role="tab"
569 aria-selected={sort === "forks" ? "true" : "false"}
c81ab7aClaude570 >
283fbc2Claude571 Most-forked
572 </a>
c81ab7aClaude573 <a
283fbc2Claude574 class={`explore-filter${sort === "recent" ? " is-active" : ""}`}
575 href="/explore?sort=recent"
576 role="tab"
577 aria-selected={sort === "recent" ? "true" : "false"}
c81ab7aClaude578 >
283fbc2Claude579 Recently active
c81ab7aClaude580 </a>
581 </div>
283fbc2Claude582 <div class="explore-toolbar-meta">
583 {repoList.length > 0 ? (
584 <span>
585 <strong>{repoList.length}</strong> repo
586 {repoList.length === 1 ? "" : "s"}
587 {q ? <> matching "{q}"</> : <> · sorted by {sortLabel}</>}
588 </span>
589 ) : null}
590 </div>
591 </div>
592
c81ab7aClaude593 {repoList.length === 0 ? (
283fbc2Claude594 <div class="explore-empty">
595 <svg
596 class="explore-empty-art"
597 viewBox="0 0 96 96"
598 fill="none"
599 xmlns="http://www.w3.org/2000/svg"
600 aria-hidden="true"
601 >
602 <defs>
603 <linearGradient id="exploreEmptyG" x1="0" y1="0" x2="96" y2="96" gradientUnits="userSpaceOnUse">
604 <stop stop-color="#8c6dff" />
605 <stop offset="1" stop-color="#36c5d6" />
606 </linearGradient>
607 </defs>
608 <circle cx="42" cy="42" r="22" stroke="url(#exploreEmptyG)" stroke-width="4" />
609 <path d="M58 58L78 78" stroke="url(#exploreEmptyG)" stroke-width="4" stroke-linecap="round" />
610 <circle cx="42" cy="42" r="8" stroke="url(#exploreEmptyG)" stroke-width="3" opacity="0.55" />
611 </svg>
612 <h2 class="explore-empty-title">
613 {q || topic ? "No matching repos" : "Nothing public yet"}
614 </h2>
615 <p class="explore-empty-sub">
c81ab7aClaude616 {q
283fbc2Claude617 ? `We couldn't find any public repositories matching "${q}". Try a broader search or browse all repos.`
618 : topic
619 ? `No public repositories are tagged with #${topic}. Try a different topic or browse everything.`
620 : "When public repositories are created they'll show up here. Be the first to ship something."}
c81ab7aClaude621 </p>
283fbc2Claude622 <div class="explore-empty-cta">
623 <a href="/explore" class="btn btn-primary">
624 Reset filters
625 </a>
626 {user ? (
627 <a href="/new" class="btn">
628 New repository
629 </a>
630 ) : (
631 <a href="/register" class="btn">
632 Get started
633 </a>
634 )}
635 </div>
636 </div>
c81ab7aClaude637 ) : (
283fbc2Claude638 <div class="explore-grid">
c81ab7aClaude639 {repoList.map(({ repo, ownerName }) => (
283fbc2Claude640 <a
641 class="explore-card"
642 href={`/${ownerName}/${repo.name}`}
643 aria-label={`${ownerName}/${repo.name}`}
644 >
645 <div class="explore-card-head">
646 <div class="explore-card-avatar" aria-hidden="true">
647 {initials(ownerName)}
648 </div>
649 <div class="explore-card-name">
650 <span class="explore-card-owner">{ownerName}/</span>
651 <span class="explore-card-repo">{repo.name}</span>
652 </div>
653 </div>
654 {repo.description && (
655 <p class="explore-card-desc">{repo.description}</p>
656 )}
657 {(repo.isPrivate ||
658 repo.forkedFromId ||
659 repo.isArchived ||
660 repo.isTemplate) && (
661 <div class="explore-card-badges">
662 {repo.isPrivate && (
663 <span class="explore-pill explore-pill-private">
664 Private
665 </span>
666 )}
667 {repo.forkedFromId && (
668 <span class="explore-pill explore-pill-fork">
669 <span aria-hidden="true">{"⑂"}</span> Fork
670 </span>
671 )}
672 {repo.isTemplate && (
673 <span class="explore-pill explore-pill-template">
674 Template
675 </span>
676 )}
677 {repo.isArchived && (
678 <span class="explore-pill explore-pill-archived">
679 Archived
680 </span>
681 )}
682 </div>
683 )}
684 <div class="explore-card-meta">
685 <span class="explore-card-meta-item" title="Stars">
686 <span aria-hidden="true">{"★"}</span>
687 {repo.starCount}
688 </span>
689 <span class="explore-card-meta-item" title="Forks">
690 <span aria-hidden="true">{"⑂"}</span>
691 {repo.forkCount}
692 </span>
693 {repo.pushedAt && (
694 <span class="explore-card-meta-item" title="Last push">
695 <span class="explore-card-dot" aria-hidden="true">
696 {"●"}
697 </span>
698 Pushed {formatRelative(repo.pushedAt.toString())}
699 </span>
700 )}
701 </div>
702 </a>
c81ab7aClaude703 ))}
283fbc2Claude704 </div>
c81ab7aClaude705 )}
706 </Layout>
707 );
708});
709
710export default explore;