Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commit283fbc2unknown_key

polish(discovery): 2026 hero + repo cards + result categories (parallel session 3.H)

Claude committed on May 17, 2026Parent: 7a99d47
2 files changed+1334142283fbc25b774ed38b362a90a567a396f8cab7663
2 changed files+1334−142
Modifiedsrc/routes/explore.tsx+590−66View fileUnifiedSplit
33 */
44
55import { Hono } from "hono";
6import { eq, desc, sql, like, and } from "drizzle-orm";
6import { eq, desc, sql, and } from "drizzle-orm";
77import { db } from "../db";
88import { repositories, users, repoTopics } from "../db/schema";
99import { Layout } from "../views/layout";
10import { RepoCard } from "../views/components";
1110import { softAuth } from "../middleware/auth";
1211import type { AuthEnv } from "../middleware/auth";
13import {
14 Flex,
15 Grid,
16 Badge,
17 Button,
18 LinkButton,
19 Input,
20 EmptyState,
21 PageHeader,
22} from "../views/ui";
2312
2413const explore = new Hono<AuthEnv>();
2514
2615explore.use("*", softAuth);
2716
17const 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
28407explore.get("/explore", async (c) => {
29408 const user = c.get("user");
30409 const q = c.req.query("q") || "";
107486 }));
108487 }
109488
489 const sortLabel =
490 sort === "stars"
491 ? "most-starred"
492 : sort === "forks"
493 ? "most-forked"
494 : "most recent";
495
110496 return c.html(
111497 <Layout title="Explore" user={user}>
112 <PageHeader title="Explore repositories" />
113 <Flex gap={12} wrap align="center" style="margin-bottom:24px">
114 <form
115 method="get"
116 action="/explore"
117 style="display:flex;gap:8px;flex:1;min-width:250px"
118 >
119 <Input
120 name="q"
121 value={q}
122 placeholder="Search repositories..."
123 aria-label="Search repositories"
124 style="flex:1;padding:8px 12px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);color:var(--text);font-size:14px"
125 />
126 <Button type="submit" variant="primary">
127 Search
128 </Button>
129 </form>
130 <Flex gap={8}>
131 <LinkButton
132 href="/explore?sort=recent"
133 size="sm"
134 variant={sort === "recent" && !q ? "primary" : "default"}
498 <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"
135522 >
136 Recent
137 </LinkButton>
138 <LinkButton
523 <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" : ""}`}
139559 href="/explore?sort=stars"
140 size="sm"
141 variant={sort === "stars" ? "primary" : "default"}
560 role="tab"
561 aria-selected={sort === "stars" ? "true" : "false"}
142562 >
143 Most stars
144 </LinkButton>
145 <LinkButton
563 Trending
564 </a>
565 <a
566 class={`explore-filter${sort === "forks" ? " is-active" : ""}`}
146567 href="/explore?sort=forks"
147 size="sm"
148 variant={sort === "forks" ? "primary" : "default"}
568 role="tab"
569 aria-selected={sort === "forks" ? "true" : "false"}
149570 >
150 Most forks
151 </LinkButton>
152 </Flex>
153 </Flex>
154 {topic && (
155 <div style="margin-bottom:16px">
156 <Badge style="font-size:14px;padding:4px 12px">
157 Topic: {topic}
158 </Badge>
571 Most-forked
572 </a>
159573 <a
160 href="/explore"
161 style="margin-left:8px;font-size:13px;color:var(--text-muted)"
574 class={`explore-filter${sort === "recent" ? " is-active" : ""}`}
575 href="/explore?sort=recent"
576 role="tab"
577 aria-selected={sort === "recent" ? "true" : "false"}
162578 >
163 Clear
579 Recently active
164580 </a>
165581 </div>
166 )}
582 <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
167593 {repoList.length === 0 ? (
168 <EmptyState>
169 <p>
594 <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">
170616 {q
171 ? `No repositories matching "${q}"`
172 : "No public repositories yet."}
617 ? `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."}
173621 </p>
174 </EmptyState>
622 <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>
175637 ) : (
176 <Grid>
638 <div class="explore-grid">
177639 {repoList.map(({ repo, ownerName }) => (
178 <RepoCard repo={repo} ownerName={ownerName} />
640 <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>
179703 ))}
180 </Grid>
704 </div>
181705 )}
182706 </Layout>
183707 );
Modifiedsrc/routes/search.tsx+744−76View fileUnifiedSplit
88 */
99
1010import { Hono } from "hono";
11import { and, desc, eq, or, sql } from "drizzle-orm";
11import { and, desc, eq, sql } from "drizzle-orm";
1212import { db } from "../db";
1313import {
1414 issues,
1717 users,
1818} from "../db/schema";
1919import { Layout } from "../views/layout";
20import { RepoCard } from "../views/components";
2120import { softAuth } from "../middleware/auth";
2221import type { AuthEnv } from "../middleware/auth";
2322import { getUnreadCount } from "../lib/unread";
2524const search = new Hono<AuthEnv>();
2625search.use("*", softAuth);
2726
27const SearchPageStyle = () => (
28 <style
29 dangerouslySetInnerHTML={{
30 __html: `
31 /* ─── Hero ─── */
32 .search-page-hero {
33 position: relative;
34 margin: 4px 0 22px;
35 padding: 32px 32px 28px;
36 background: var(--bg-elevated);
37 border: 1px solid var(--border);
38 border-radius: 16px;
39 overflow: hidden;
40 }
41 .search-page-hero::before {
42 content: '';
43 position: absolute;
44 top: 0; left: 0; right: 0;
45 height: 2px;
46 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
47 opacity: 0.7;
48 pointer-events: none;
49 }
50 .search-page-hero-bg {
51 position: absolute;
52 inset: -30% -10% auto auto;
53 width: 360px;
54 height: 360px;
55 pointer-events: none;
56 z-index: 0;
57 }
58 .search-page-hero-orb {
59 position: absolute;
60 inset: 0;
61 background: radial-gradient(circle, rgba(140,109,255,0.18), rgba(54,197,214,0.09) 45%, transparent 70%);
62 filter: blur(80px);
63 opacity: 0.7;
64 animation: searchPageHeroOrb 14s ease-in-out infinite;
65 }
66 @keyframes searchPageHeroOrb {
67 0%, 100% { transform: scale(1) translate(0, 0); opacity: 0.6; }
68 50% { transform: scale(1.1) translate(-12px, 8px); opacity: 0.85; }
69 }
70 @media (prefers-reduced-motion: reduce) {
71 .search-page-hero-orb { animation: none; }
72 }
73 .search-page-hero-inner {
74 position: relative;
75 z-index: 1;
76 display: flex;
77 flex-direction: column;
78 gap: 16px;
79 }
80 .search-page-hero-eyebrow {
81 font-size: 12.5px;
82 color: var(--text-muted);
83 letter-spacing: 0.06em;
84 text-transform: uppercase;
85 font-weight: 600;
86 }
87 .search-page-hero-title {
88 font-family: var(--font-display);
89 font-size: clamp(28px, 4vw, 40px);
90 font-weight: 800;
91 letter-spacing: -0.028em;
92 line-height: 1.05;
93 margin: 0;
94 color: var(--text-strong);
95 }
96 .search-page-hero-sub {
97 font-size: 15px;
98 color: var(--text-muted);
99 margin: 0;
100 line-height: 1.5;
101 max-width: 620px;
102 }
103 .search-page-form {
104 margin-top: 6px;
105 position: relative;
106 display: flex;
107 align-items: stretch;
108 gap: 0;
109 }
110 .search-page-input-wrap {
111 position: relative;
112 flex: 1;
113 }
114 .search-page-input-icon {
115 position: absolute;
116 top: 50%;
117 left: 18px;
118 transform: translateY(-50%);
119 color: var(--text-muted);
120 pointer-events: none;
121 width: 18px;
122 height: 18px;
123 }
124 .search-page-input {
125 width: 100%;
126 padding: 14px 18px 14px 48px;
127 background: var(--bg);
128 border: 1px solid var(--border);
129 border-radius: 14px;
130 color: var(--text);
131 font-size: 15px;
132 font-family: inherit;
133 transition: border-color 140ms ease, box-shadow 140ms ease;
134 }
135 .search-page-input::placeholder { color: var(--text-muted); }
136 .search-page-input:focus {
137 outline: none;
138 border-color: rgba(140,109,255,0.55);
139 box-shadow: 0 0 0 4px rgba(140,109,255,0.12);
140 }
141 .search-page-kbd {
142 position: absolute;
143 right: 14px;
144 top: 50%;
145 transform: translateY(-50%);
146 font-family: var(--font-mono, monospace);
147 font-size: 11px;
148 color: var(--text-muted);
149 background: rgba(255,255,255,0.04);
150 border: 1px solid var(--border);
151 padding: 2px 8px;
152 border-radius: 6px;
153 pointer-events: none;
154 }
155 @media (max-width: 720px) {
156 .search-page-hero { padding: 24px 20px; }
157 .search-page-kbd { display: none; }
158 }
159
160 /* ─── Tab pills ─── */
161 .search-page-tabs {
162 display: inline-flex;
163 background: var(--bg-elevated);
164 border: 1px solid var(--border);
165 border-radius: 9999px;
166 padding: 4px;
167 gap: 2px;
168 margin: 0 0 18px;
169 flex-wrap: wrap;
170 }
171 .search-page-tab {
172 display: inline-flex;
173 align-items: center;
174 gap: 7px;
175 padding: 7px 16px;
176 border-radius: 9999px;
177 font-size: 13px;
178 font-weight: 500;
179 color: var(--text-muted);
180 text-decoration: none;
181 transition: color 120ms ease, background 120ms ease;
182 line-height: 1.4;
183 }
184 .search-page-tab:hover {
185 color: var(--text-strong);
186 text-decoration: none;
187 }
188 .search-page-tab.is-active {
189 background: rgba(140,109,255,0.14);
190 color: var(--text-strong);
191 }
192 .search-page-tab-count {
193 font-variant-numeric: tabular-nums;
194 font-size: 11.5px;
195 color: var(--text-muted);
196 background: rgba(255,255,255,0.04);
197 padding: 1px 8px;
198 border-radius: 9999px;
199 }
200 .search-page-tab.is-active .search-page-tab-count {
201 background: rgba(140,109,255,0.20);
202 color: var(--text);
203 }
204
205 /* ─── Result list ─── */
206 .search-page-results {
207 display: flex;
208 flex-direction: column;
209 gap: 10px;
210 }
211 .search-page-result {
212 position: relative;
213 display: flex;
214 gap: 14px;
215 padding: 16px 20px;
216 background: var(--bg-elevated);
217 border: 1px solid var(--border);
218 border-radius: 14px;
219 text-decoration: none;
220 color: inherit;
221 transition: transform 140ms ease, border-color 140ms ease, box-shadow 140ms ease;
222 }
223 .search-page-result:hover {
224 transform: translateY(-1px);
225 border-color: rgba(140,109,255,0.45);
226 box-shadow: 0 10px 22px -16px rgba(0,0,0,0.5), 0 0 18px -8px rgba(140,109,255,0.18);
227 text-decoration: none;
228 }
229 .search-page-result-avatar {
230 width: 40px;
231 height: 40px;
232 border-radius: 10px;
233 display: inline-flex;
234 align-items: center;
235 justify-content: center;
236 font-family: var(--font-display);
237 font-weight: 700;
238 font-size: 15px;
239 color: #fff;
240 background: linear-gradient(135deg, #8c6dff 0%, #36c5d6 100%);
241 flex-shrink: 0;
242 letter-spacing: -0.01em;
243 }
244 .search-page-result-avatar.is-user {
245 border-radius: 9999px;
246 }
247 .search-page-result-body {
248 flex: 1;
249 min-width: 0;
250 display: flex;
251 flex-direction: column;
252 gap: 4px;
253 }
254 .search-page-result-title {
255 font-family: var(--font-display);
256 font-size: 15.5px;
257 font-weight: 600;
258 color: var(--text-strong);
259 letter-spacing: -0.014em;
260 line-height: 1.35;
261 margin: 0;
262 display: flex;
263 flex-wrap: wrap;
264 align-items: center;
265 gap: 8px;
266 }
267 .search-page-result:hover .search-page-result-title { color: var(--accent); }
268 .search-page-result-meta {
269 font-size: 12.5px;
270 color: var(--text-muted);
271 line-height: 1.5;
272 }
273 .search-page-result-meta strong {
274 color: var(--text);
275 font-weight: 600;
276 }
277 .search-page-result-desc {
278 font-size: 13.5px;
279 color: var(--text-muted);
280 margin: 4px 0 0;
281 line-height: 1.5;
282 display: -webkit-box;
283 -webkit-line-clamp: 2;
284 -webkit-box-orient: vertical;
285 overflow: hidden;
286 }
287
288 /* ─── State pill (issues/PRs) ─── */
289 .search-page-state {
290 display: inline-flex;
291 align-items: center;
292 gap: 5px;
293 padding: 2px 10px;
294 border-radius: 9999px;
295 font-size: 11.5px;
296 font-weight: 600;
297 line-height: 1.5;
298 letter-spacing: 0.005em;
299 }
300 .search-page-state-open {
301 background: rgba(52,211,153,0.10);
302 color: #34d399;
303 border: 1px solid rgba(52,211,153,0.30);
304 }
305 .search-page-state-closed {
306 background: rgba(140,109,255,0.12);
307 color: #b69dff;
308 border: 1px solid rgba(140,109,255,0.30);
309 }
310 .search-page-state-merged {
311 background: rgba(54,197,214,0.10);
312 color: #36c5d6;
313 border: 1px solid rgba(54,197,214,0.28);
314 }
315 .search-page-state-draft {
316 background: rgba(255,255,255,0.04);
317 color: var(--text-muted);
318 border: 1px solid var(--border);
319 }
320
321 /* ─── Code result card ─── */
322 .search-page-code {
323 display: flex;
324 flex-direction: column;
325 gap: 10px;
326 padding: 14px 18px;
327 background: var(--bg-elevated);
328 border: 1px solid var(--border);
329 border-radius: 14px;
330 }
331 .search-page-code-path {
332 font-family: var(--font-mono, monospace);
333 font-size: 12.5px;
334 color: var(--text);
335 display: flex;
336 align-items: center;
337 gap: 8px;
338 }
339 .search-page-code-snippet {
340 display: flex;
341 background: var(--bg);
342 border: 1px solid var(--border);
343 border-radius: 10px;
344 padding: 8px 12px;
345 font-family: var(--font-mono, monospace);
346 font-size: 12.5px;
347 overflow-x: auto;
348 }
349 .search-page-code-lineno {
350 color: var(--text-muted);
351 flex-shrink: 0;
352 padding-right: 14px;
353 border-right: 1px solid var(--border);
354 margin-right: 12px;
355 text-align: right;
356 min-width: 32px;
357 font-variant-numeric: tabular-nums;
358 }
359 .search-page-code-line {
360 color: var(--text);
361 white-space: pre;
362 }
363 .search-page-code-mark {
364 background: rgba(140,109,255,0.22);
365 color: var(--text-strong);
366 border-radius: 3px;
367 padding: 0 2px;
368 }
369
370 /* ─── Empty / hint states ─── */
371 .search-page-empty {
372 margin: 0;
373 padding: 56px 32px;
374 background: var(--bg-elevated);
375 border: 1px solid var(--border);
376 border-radius: 16px;
377 text-align: center;
378 position: relative;
379 overflow: hidden;
380 }
381 .search-page-empty::before {
382 content: '';
383 position: absolute;
384 top: 0; left: 0; right: 0;
385 height: 2px;
386 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
387 opacity: 0.55;
388 pointer-events: none;
389 }
390 .search-page-empty-art {
391 width: 88px;
392 height: 88px;
393 margin: 0 auto 18px;
394 display: block;
395 opacity: 0.85;
396 }
397 .search-page-empty-title {
398 font-family: var(--font-display);
399 font-size: 22px;
400 font-weight: 700;
401 letter-spacing: -0.018em;
402 color: var(--text-strong);
403 margin: 0 0 8px;
404 }
405 .search-page-empty-sub {
406 font-size: 14.5px;
407 color: var(--text-muted);
408 line-height: 1.55;
409 margin: 0 auto 22px;
410 max-width: 480px;
411 }
412 .search-page-empty-cta {
413 display: inline-flex;
414 gap: 10px;
415 flex-wrap: wrap;
416 justify-content: center;
417 }
418 .search-page-empty-tip {
419 display: inline-flex;
420 flex-direction: column;
421 gap: 6px;
422 margin-top: 12px;
423 font-size: 13px;
424 color: var(--text-muted);
425 text-align: left;
426 }
427 .search-page-empty-tip-row { color: var(--text-muted); }
428 .search-page-empty-tip-row strong { color: var(--text); font-weight: 600; }
429
430 /* ─── Shortcuts page (kept simplereuses panel) ─── */
431 .search-page-shortcut-row {
432 display: flex;
433 justify-content: space-between;
434 align-items: center;
435 padding: 11px 16px;
436 border-bottom: 1px solid var(--border);
437 }
438 .search-page-shortcut-row:last-child { border-bottom: none; }
439 `,
440 }}
441 />
442);
443
444function userInitials(u: typeof users.$inferSelect): string {
445 const src = u.displayName || u.username || "·";
446 const clean = src.replace(/[^a-zA-Z0-9]+/g, " ").trim();
447 if (!clean) return src.charAt(0).toUpperCase();
448 const parts = clean.split(/\s+/);
449 if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
450 return (parts[0][0] + parts[1][0]).toUpperCase();
451}
452
453function repoInitials(ownerName: string, name: string): string {
454 const src = name || ownerName || "·";
455 const clean = src.replace(/[^a-zA-Z0-9]+/g, " ").trim();
456 if (!clean) return src.charAt(0).toUpperCase();
457 const parts = clean.split(/\s+/);
458 if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
459 return (parts[0][0] + parts[1][0]).toUpperCase();
460}
461
28462search.get("/search", async (c) => {
29463 const user = c.get("user");
30464 const q = (c.req.query("q") || "").trim();
122556 }
123557 }
124558
125 const tab = (id: string, label: string) => (
126 <a
127 href={`/search?q=${encodeURIComponent(q)}&type=${id}`}
128 class={type === id ? "active" : ""}
129 >
130 {label}
131 </a>
559 // Active-tab count for the count chip (only the active tab knows its count).
560 const activeCount =
561 type === "repos"
562 ? repoHits.length
563 : type === "users"
564 ? userHits.length
565 : type === "issues"
566 ? issueHits.length
567 : type === "prs"
568 ? prHits.length
569 : 0;
570
571 const tabPill = (id: string, label: string) => {
572 const isActive = type === id;
573 const showCount = isActive && q;
574 return (
575 <a
576 href={`/search?q=${encodeURIComponent(q)}&type=${id}`}
577 class={`search-page-tab${isActive ? " is-active" : ""}`}
578 role="tab"
579 aria-selected={isActive ? "true" : "false"}
580 >
581 <span>{label}</span>
582 {showCount && (
583 <span class="search-page-tab-count">{activeCount}</span>
584 )}
585 </a>
586 );
587 };
588
589 const stateClass = (state: string) =>
590 state === "open"
591 ? "search-page-state search-page-state-open"
592 : state === "merged"
593 ? "search-page-state search-page-state-merged"
594 : state === "draft"
595 ? "search-page-state search-page-state-draft"
596 : "search-page-state search-page-state-closed";
597
598 const noResultsFor = (label: string) => (
599 <div class="search-page-empty">
600 <svg
601 class="search-page-empty-art"
602 viewBox="0 0 96 96"
603 fill="none"
604 xmlns="http://www.w3.org/2000/svg"
605 aria-hidden="true"
606 >
607 <defs>
608 <linearGradient id="searchEmptyG" x1="0" y1="0" x2="96" y2="96" gradientUnits="userSpaceOnUse">
609 <stop stop-color="#8c6dff" />
610 <stop offset="1" stop-color="#36c5d6" />
611 </linearGradient>
612 </defs>
613 <circle cx="40" cy="40" r="22" stroke="url(#searchEmptyG)" stroke-width="4" />
614 <path d="M56 56L78 78" stroke="url(#searchEmptyG)" stroke-width="4" stroke-linecap="round" />
615 <path d="M32 40h16M40 32v16" stroke="url(#searchEmptyG)" stroke-width="3" stroke-linecap="round" opacity="0.55" />
616 </svg>
617 <h2 class="search-page-empty-title">No {label} for "{q}"</h2>
618 <p class="search-page-empty-sub">
619 Try a different keyword, switch categories, or browse{" "}
620 <a href="/explore">Explore</a> to discover what's shipping on Gluecron.
621 </p>
622 <div class="search-page-empty-cta">
623 <a href="/explore" class="btn btn-primary">
624 Go to Explore
625 </a>
626 <a href={`/search?q=&type=${type}`} class="btn">
627 Clear search
628 </a>
629 </div>
630 </div>
132631 );
133632
134633 return c.html(
137636 user={user}
138637 notificationCount={unread}
139638 >
140 <form method="get" action="/search" style="margin-bottom: 16px">
141 <input
142 type="hidden"
143 name="type"
144 value={type}
145 />
146 <input
147 type="search"
148 name="q"
149 value={q}
150 placeholder="Search repositories, users, issues, PRs…"
151 aria-label="Search"
152 style="width: 100%; padding: 10px 14px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 14px"
153 autofocus
154 />
155 </form>
639 <SearchPageStyle />
640
641 <section class="search-page-hero">
642 <div class="search-page-hero-bg" aria-hidden="true">
643 <div class="search-page-hero-orb" />
644 </div>
645 <div class="search-page-hero-inner">
646 <div class="search-page-hero-eyebrow">Global search</div>
647 <h1 class="search-page-hero-title">
648 <span class="gradient-text">Search</span> Gluecron.
649 </h1>
650 <p class="search-page-hero-sub">
651 Find repos, people, issues, pull requests, and code across the
652 platform. Everything public is fair game.
653 </p>
654 <form
655 method="get"
656 action="/search"
657 class="search-page-form"
658 role="search"
659 >
660 <input type="hidden" name="type" value={type} />
661 <div class="search-page-input-wrap">
662 <svg
663 class="search-page-input-icon"
664 viewBox="0 0 24 24"
665 fill="none"
666 xmlns="http://www.w3.org/2000/svg"
667 aria-hidden="true"
668 >
669 <circle cx="11" cy="11" r="7" stroke="currentColor" stroke-width="2" />
670 <path d="M20 20L17 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
671 </svg>
672 <input
673 type="search"
674 name="q"
675 value={q}
676 placeholder="Search repos, users, issues, code…"
677 aria-label="Search Gluecron"
678 class="search-page-input"
679 autofocus
680 autocomplete="off"
681 />
682 <span class="search-page-kbd" aria-hidden="true">
683 Enter
684 </span>
685 </div>
686 </form>
687 </div>
688 </section>
156689
157 <div class="issue-tabs" style="margin-bottom: 20px">
158 {tab("repos", "Repositories")}
159 {tab("users", "Users")}
160 {tab("issues", "Issues")}
161 {tab("prs", "Pull requests")}
690 <div
691 class="search-page-tabs"
692 role="tablist"
693 aria-label="Result categories"
694 >
695 {tabPill("repos", "Repositories")}
696 {tabPill("users", "Users")}
697 {tabPill("issues", "Issues")}
698 {tabPill("prs", "Pull requests")}
699 {tabPill("code", "Code")}
162700 </div>
163701
164702 {!q && (
165 <div class="empty-state">
166 <p>Type to search across GlueCron.</p>
703 <div class="search-page-empty">
704 <svg
705 class="search-page-empty-art"
706 viewBox="0 0 96 96"
707 fill="none"
708 xmlns="http://www.w3.org/2000/svg"
709 aria-hidden="true"
710 >
711 <defs>
712 <linearGradient id="searchIdleG" x1="0" y1="0" x2="96" y2="96" gradientUnits="userSpaceOnUse">
713 <stop stop-color="#8c6dff" />
714 <stop offset="1" stop-color="#36c5d6" />
715 </linearGradient>
716 </defs>
717 <circle cx="42" cy="42" r="22" stroke="url(#searchIdleG)" stroke-width="4" />
718 <path d="M58 58L78 78" stroke="url(#searchIdleG)" stroke-width="4" stroke-linecap="round" />
719 <circle cx="42" cy="42" r="8" stroke="url(#searchIdleG)" stroke-width="3" opacity="0.55" />
720 </svg>
721 <h2 class="search-page-empty-title">Search Gluecron</h2>
722 <p class="search-page-empty-sub">
723 Type to search across repos, users, issues, and pull requests.
724 Switch categories with the tabs above.
725 </p>
726 <div class="search-page-empty-tip">
727 <span class="search-page-empty-tip-row">
728 <strong>Tip:</strong> Press <code>/</code> from any page to focus
729 the global search.
730 </span>
731 <span class="search-page-empty-tip-row">
732 Looking for something to browse?{" "}
733 <a href="/explore">Head to Explore</a>.
734 </span>
735 </div>
167736 </div>
168737 )}
169738
170739 {q && type === "repos" && (
171740 repoHits.length === 0 ? (
172 <div class="empty-state"><p>No repositories match "{q}"</p></div>
741 noResultsFor("repositories")
173742 ) : (
174 <div class="card-grid">
743 <div class="search-page-results">
175744 {repoHits.map(({ repo, ownerName }) => (
176 <RepoCard repo={repo} ownerName={ownerName} />
745 <a
746 class="search-page-result"
747 href={`/${ownerName}/${repo.name}`}
748 aria-label={`${ownerName}/${repo.name}`}
749 >
750 <div class="search-page-result-avatar" aria-hidden="true">
751 {repoInitials(ownerName, repo.name)}
752 </div>
753 <div class="search-page-result-body">
754 <h3 class="search-page-result-title">
755 <span>
756 {ownerName}/{repo.name}
757 </span>
758 {repo.forkedFromId && (
759 <span class="search-page-state search-page-state-merged">
760 Fork
761 </span>
762 )}
763 </h3>
764 {repo.description && (
765 <p class="search-page-result-desc">{repo.description}</p>
766 )}
767 <div class="search-page-result-meta">
768 <span>{"★"} {repo.starCount}</span>
769 {" · "}
770 <span>{"⑂"} {repo.forkCount}</span>
771 {repo.pushedAt && (
772 <>
773 {" · "}
774 <span>
775 Updated{" "}
776 {new Date(repo.pushedAt).toLocaleDateString("en-US", {
777 month: "short",
778 day: "numeric",
779 year: "numeric",
780 })}
781 </span>
782 </>
783 )}
784 </div>
785 </div>
786 </a>
177787 ))}
178788 </div>
179789 )
181791
182792 {q && type === "users" && (
183793 userHits.length === 0 ? (
184 <div class="empty-state"><p>No users match "{q}"</p></div>
794 noResultsFor("users")
185795 ) : (
186 <div class="panel">
796 <div class="search-page-results">
187797 {userHits.map((u) => (
188 <div class="panel-item">
189 <div class="dot blue"></div>
190 <div style="flex: 1">
191 <a href={`/${u.username}`} style="font-weight: 600">
192 {u.displayName || u.username}
193 </a>
194 <div class="meta">@{u.username}</div>
195 {u.bio && <div class="meta">{u.bio}</div>}
798 <a
799 class="search-page-result"
800 href={`/${u.username}`}
801 aria-label={u.username}
802 >
803 <div
804 class="search-page-result-avatar is-user"
805 aria-hidden="true"
806 >
807 {userInitials(u)}
808 </div>
809 <div class="search-page-result-body">
810 <h3 class="search-page-result-title">
811 <span>{u.displayName || u.username}</span>
812 </h3>
813 <div class="search-page-result-meta">@{u.username}</div>
814 {u.bio && (
815 <p class="search-page-result-desc">{u.bio}</p>
816 )}
196817 </div>
197 </div>
818 </a>
198819 ))}
199820 </div>
200821 )
202823
203824 {q && type === "issues" && (
204825 issueHits.length === 0 ? (
205 <div class="empty-state"><p>No issues match "{q}"</p></div>
826 noResultsFor("issues")
206827 ) : (
207 <div class="panel">
828 <div class="search-page-results">
208829 {issueHits.map((i) => (
209 <div class="panel-item">
210 <div
211 class={`dot ${i.state === "open" ? "green" : "yellow"}`}
212 ></div>
213 <div style="flex: 1">
214 <a
215 href={`/${i.repoOwner}/${i.repoName}/issues/${i.number}`}
216 >
217 {i.title}
218 </a>
219 <div class="meta">
220 {i.repoOwner}/{i.repoName}#{i.number} · {i.state}
830 <a
831 class="search-page-result"
832 href={`/${i.repoOwner}/${i.repoName}/issues/${i.number}`}
833 aria-label={`Issue ${i.title}`}
834 >
835 <div class="search-page-result-avatar" aria-hidden="true">
836 {repoInitials(i.repoOwner, i.repoName)}
837 </div>
838 <div class="search-page-result-body">
839 <h3 class="search-page-result-title">
840 <span>{i.title}</span>
841 <span class={stateClass(i.state)}>{i.state}</span>
842 </h3>
843 <div class="search-page-result-meta">
844 <strong>
845 {i.repoOwner}/{i.repoName}
846 </strong>{" "}
847 · #{i.number}
221848 </div>
222849 </div>
223 </div>
850 </a>
224851 ))}
225852 </div>
226853 )
228855
229856 {q && type === "prs" && (
230857 prHits.length === 0 ? (
231 <div class="empty-state"><p>No pull requests match "{q}"</p></div>
858 noResultsFor("pull requests")
232859 ) : (
233 <div class="panel">
860 <div class="search-page-results">
234861 {prHits.map((pr) => (
235 <div class="panel-item">
236 <div
237 class={`dot ${pr.state === "open" ? "green" : pr.state === "merged" ? "blue" : "yellow"}`}
238 ></div>
239 <div style="flex: 1">
240 <a
241 href={`/${pr.repoOwner}/${pr.repoName}/pull/${pr.number}`}
242 >
243 {pr.title}
244 </a>
245 <div class="meta">
246 {pr.repoOwner}/{pr.repoName}#{pr.number} · {pr.state}
862 <a
863 class="search-page-result"
864 href={`/${pr.repoOwner}/${pr.repoName}/pull/${pr.number}`}
865 aria-label={`Pull request ${pr.title}`}
866 >
867 <div class="search-page-result-avatar" aria-hidden="true">
868 {repoInitials(pr.repoOwner, pr.repoName)}
869 </div>
870 <div class="search-page-result-body">
871 <h3 class="search-page-result-title">
872 <span>{pr.title}</span>
873 <span class={stateClass(pr.state)}>{pr.state}</span>
874 </h3>
875 <div class="search-page-result-meta">
876 <strong>
877 {pr.repoOwner}/{pr.repoName}
878 </strong>{" "}
879 · #{pr.number}
247880 </div>
248881 </div>
249 </div>
882 </a>
250883 ))}
251884 </div>
252885 )
253886 )}
887
888 {q && type === "code" && (
889 <div class="search-page-empty">
890 <svg
891 class="search-page-empty-art"
892 viewBox="0 0 96 96"
893 fill="none"
894 xmlns="http://www.w3.org/2000/svg"
895 aria-hidden="true"
896 >
897 <defs>
898 <linearGradient id="searchCodeG" x1="0" y1="0" x2="96" y2="96" gradientUnits="userSpaceOnUse">
899 <stop stop-color="#8c6dff" />
900 <stop offset="1" stop-color="#36c5d6" />
901 </linearGradient>
902 </defs>
903 <path d="M34 30L18 48L34 66" stroke="url(#searchCodeG)" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
904 <path d="M62 30L78 48L62 66" stroke="url(#searchCodeG)" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
905 <path d="M54 24L42 72" stroke="url(#searchCodeG)" stroke-width="4" stroke-linecap="round" opacity="0.6" />
906 </svg>
907 <h2 class="search-page-empty-title">Code search is per-repo</h2>
908 <p class="search-page-empty-sub">
909 Cross-repo code search is on the roadmap. For now, open a repo and
910 use its search tab to find symbols and matching lines.
911 </p>
912 <div class="search-page-empty-cta">
913 <a href="/explore" class="btn btn-primary">
914 Browse repos
915 </a>
916 <a href={`/search?q=${encodeURIComponent(q)}&type=repos`} class="btn">
917 Search repos instead
918 </a>
919 </div>
920 </div>
921 )}
254922 </Layout>
255923 );
256924});
257925