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.
| c81ab7a | 1 | /** |
| 2 | * Explore page — discover public repositories, search, trending. | |
| 3 | */ | |
| 4 | ||
| 5 | import { Hono } from "hono"; | |
| 6 | import { eq, desc, sql, like, and } from "drizzle-orm"; | |
| 7 | import { db } from "../db"; | |
| 8 | import { repositories, users, repoTopics } from "../db/schema"; | |
| 9 | import { Layout } from "../views/layout"; | |
| 10 | import { RepoCard } from "../views/components"; | |
| 11 | import { softAuth } from "../middleware/auth"; | |
| 12 | import type { AuthEnv } from "../middleware/auth"; | |
| 3e8f8e8 | 13 | import { |
| 14 | Flex, | |
| 15 | Grid, | |
| 16 | Badge, | |
| 17 | Button, | |
| 18 | LinkButton, | |
| 19 | Input, | |
| 20 | EmptyState, | |
| 21 | PageHeader, | |
| 22 | } from "../views/ui"; | |
| c81ab7a | 23 | |
| 24 | const explore = new Hono<AuthEnv>(); | |
| 25 | ||
| 26 | explore.use("*", softAuth); | |
| 27 | ||
| 28 | explore.get("/explore", async (c) => { | |
| 29 | const user = c.get("user"); | |
| 30 | const q = c.req.query("q") || ""; | |
| 31 | const sort = c.req.query("sort") || "recent"; | |
| 32 | const topic = c.req.query("topic") || ""; | |
| 33 | ||
| 34 | let repoList: Array<{ | |
| 35 | repo: typeof repositories.$inferSelect; | |
| 36 | ownerName: string; | |
| 37 | }> = []; | |
| 38 | ||
| 39 | if (q.trim()) { | |
| 40 | // Search repos | |
| 41 | const results = await db | |
| 42 | .select({ | |
| 43 | repo: repositories, | |
| 44 | ownerName: users.username, | |
| 45 | }) | |
| 46 | .from(repositories) | |
| 47 | .innerJoin(users, eq(repositories.ownerId, users.id)) | |
| 48 | .where( | |
| 49 | and( | |
| 50 | eq(repositories.isPrivate, false), | |
| 51 | sql`(${repositories.name} ILIKE ${'%' + q + '%'} OR ${repositories.description} ILIKE ${'%' + q + '%'})` | |
| 52 | ) | |
| 53 | ) | |
| 54 | .orderBy(desc(repositories.starCount)) | |
| 55 | .limit(50); | |
| 56 | ||
| 57 | repoList = results.map((r) => ({ | |
| 58 | repo: r.repo, | |
| 59 | ownerName: r.ownerName, | |
| 60 | })); | |
| 61 | } else if (topic) { | |
| 62 | // Filter by topic | |
| 63 | const results = await db | |
| 64 | .select({ | |
| 65 | repo: repositories, | |
| 66 | ownerName: users.username, | |
| 67 | }) | |
| 68 | .from(repositories) | |
| 69 | .innerJoin(users, eq(repositories.ownerId, users.id)) | |
| 70 | .innerJoin(repoTopics, eq(repoTopics.repositoryId, repositories.id)) | |
| 71 | .where( | |
| 72 | and( | |
| 73 | eq(repositories.isPrivate, false), | |
| 74 | eq(repoTopics.topic, topic.toLowerCase()) | |
| 75 | ) | |
| 76 | ) | |
| 77 | .orderBy(desc(repositories.starCount)) | |
| 78 | .limit(50); | |
| 79 | ||
| 80 | repoList = results.map((r) => ({ | |
| 81 | repo: r.repo, | |
| 82 | ownerName: r.ownerName, | |
| 83 | })); | |
| 84 | } else { | |
| 85 | // Default: recent or popular | |
| 86 | const orderBy = | |
| 87 | sort === "stars" | |
| 88 | ? desc(repositories.starCount) | |
| 89 | : sort === "forks" | |
| 90 | ? desc(repositories.forkCount) | |
| 91 | : desc(repositories.createdAt); | |
| 92 | ||
| 93 | const results = await db | |
| 94 | .select({ | |
| 95 | repo: repositories, | |
| 96 | ownerName: users.username, | |
| 97 | }) | |
| 98 | .from(repositories) | |
| 99 | .innerJoin(users, eq(repositories.ownerId, users.id)) | |
| 100 | .where(eq(repositories.isPrivate, false)) | |
| 101 | .orderBy(orderBy) | |
| 102 | .limit(50); | |
| 103 | ||
| 104 | repoList = results.map((r) => ({ | |
| 105 | repo: r.repo, | |
| 106 | ownerName: r.ownerName, | |
| 107 | })); | |
| 108 | } | |
| 109 | ||
| 110 | return c.html( | |
| 111 | <Layout title="Explore" user={user}> | |
| 3e8f8e8 | 112 | <PageHeader title="Explore repositories" /> |
| 113 | <Flex gap={12} wrap align="center" style="margin-bottom:24px"> | |
| c81ab7a | 114 | <form |
| 45e31d0 | 115 | method="get" |
| c81ab7a | 116 | action="/explore" |
| 3e8f8e8 | 117 | style="display:flex;gap:8px;flex:1;min-width:250px" |
| c81ab7a | 118 | > |
| 3e8f8e8 | 119 | <Input |
| c81ab7a | 120 | name="q" |
| 121 | value={q} | |
| 122 | placeholder="Search repositories..." | |
| 2c3ba6e | 123 | aria-label="Search repositories" |
| 3e8f8e8 | 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" |
| c81ab7a | 125 | /> |
| 3e8f8e8 | 126 | <Button type="submit" variant="primary"> |
| c81ab7a | 127 | Search |
| 3e8f8e8 | 128 | </Button> |
| c81ab7a | 129 | </form> |
| 3e8f8e8 | 130 | <Flex gap={8}> |
| 131 | <LinkButton | |
| c81ab7a | 132 | href="/explore?sort=recent" |
| 3e8f8e8 | 133 | size="sm" |
| 134 | variant={sort === "recent" && !q ? "primary" : "default"} | |
| c81ab7a | 135 | > |
| 136 | Recent | |
| 3e8f8e8 | 137 | </LinkButton> |
| 138 | <LinkButton | |
| c81ab7a | 139 | href="/explore?sort=stars" |
| 3e8f8e8 | 140 | size="sm" |
| 141 | variant={sort === "stars" ? "primary" : "default"} | |
| c81ab7a | 142 | > |
| 143 | Most stars | |
| 3e8f8e8 | 144 | </LinkButton> |
| 145 | <LinkButton | |
| c81ab7a | 146 | href="/explore?sort=forks" |
| 3e8f8e8 | 147 | size="sm" |
| 148 | variant={sort === "forks" ? "primary" : "default"} | |
| c81ab7a | 149 | > |
| 150 | Most forks | |
| 3e8f8e8 | 151 | </LinkButton> |
| 152 | </Flex> | |
| 153 | </Flex> | |
| c81ab7a | 154 | {topic && ( |
| 3e8f8e8 | 155 | <div style="margin-bottom:16px"> |
| 156 | <Badge style="font-size:14px;padding:4px 12px"> | |
| c81ab7a | 157 | Topic: {topic} |
| 3e8f8e8 | 158 | </Badge> |
| c81ab7a | 159 | <a |
| 160 | href="/explore" | |
| 3e8f8e8 | 161 | style="margin-left:8px;font-size:13px;color:var(--text-muted)" |
| c81ab7a | 162 | > |
| 163 | Clear | |
| 164 | </a> | |
| 165 | </div> | |
| 166 | )} | |
| 167 | {repoList.length === 0 ? ( | |
| 3e8f8e8 | 168 | <EmptyState> |
| c81ab7a | 169 | <p> |
| 170 | {q | |
| 171 | ? `No repositories matching "${q}"` | |
| 172 | : "No public repositories yet."} | |
| 173 | </p> | |
| 3e8f8e8 | 174 | </EmptyState> |
| c81ab7a | 175 | ) : ( |
| 3e8f8e8 | 176 | <Grid> |
| c81ab7a | 177 | {repoList.map(({ repo, ownerName }) => ( |
| 178 | <RepoCard repo={repo} ownerName={ownerName} /> | |
| 179 | ))} | |
| 3e8f8e8 | 180 | </Grid> |
| c81ab7a | 181 | )} |
| 182 | </Layout> | |
| 183 | ); | |
| 184 | }); | |
| 185 | ||
| 186 | export default explore; |