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
claude/adoring-hopper-5x74bqclaude/affectionate-feynman-ykrf1hclaude/architecture-audit-design-wxprenclaude/build-status-update-3MXsfclaude/charming-meitner-mllb5rclaude/compare-gate-gluecron-s4mFQclaude/confident-faraday-tikcwbclaude/continue-work-XMTlIclaude/crontech-gluecron-deploy-7MIECclaude/crontech-platform-setup-SeKfwclaude/design-2026claude/ecstatic-ptolemy-jMdigclaude/enhance-github-integration-QNHdGclaude/fix-aa-loop-issue-PonMQclaude/fix-actions-and-processclaude/fix-desktop-errors-XqoW8claude/fix-red-workflowsclaude/fix-website-access-6FKJNclaude/gatetest-integration-hardeningclaude/github-audit-improvements-bDFr9claude/gluecron-launch-status-FoMRlclaude/hopeful-lamport-olfCTclaude/issue-to-pr-and-protectionsclaude/jolly-heisenberg-2sg1Qclaude/launch-preparation-QmTb6claude/new-session-xk1l7claude/plan-platform-architecture-kkN4yclaude/platform-analysis-roadmap-1nUGLclaude/platform-launch-assessment-8dWV8claude/polish-platform-release-AeDrUclaude/resume-previous-work-KzyLwclaude/review-crontech-handoff-qYEVqclaude/review-project-completeness-lHhS2claude/review-readme-docs-ulqPKclaude/serene-edison-rj87weclaude/setup-multi-repo-dev-BCwNQclaude/ship-fixes-and-tests-Jvz1cclaude/site-audit-competitive-pctlwgclaude/site-migration-vercel-XstpKclaude/standalone-product-repos-XHFTDcopilot/feat-smart-empty-states-keyboard-first-enhancementcopilot/feat-smart-morning-digest-review-context-restorecopilot/fix-and-process-workflowscopilot/update-ai-powered-code-reviewfeat/debt-mapfeat/push-policy-codeowners-hardeningfeat/smart-digest-contextfeat/stage-impactfeat/t1-secret-migrationfeat/u-polishfeat/w-self-hostfeat/w2-claude-configfix/agent-journey-orphan-sweepgatetest/auto-fix-1776586424172gatetest/auto-fix-1776586534814gatetest/auto-fix-1776590685143gatetest/auto-fix-1776590808199mainops/redeploy-retriggerstyle/dxt-cta-themeworktree-agent-a3377aad30d55da26worktree-agent-a7ef607b7ee1d6c74
pinned-repos.tsx4.9 KB · 142 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
/**
 * Block J13 — Pinned repos management UI.
 *
 *   GET  /settings/pins  — pick up to 6 repos to feature on your profile
 *   POST /settings/pins  — save the selection (replaces prior set)
 */

import { Hono } from "hono";
import { Layout } from "../views/layout";
import { softAuth, requireAuth } from "../middleware/auth";
import type { AuthEnv } from "../middleware/auth";
import {
  MAX_PINS,
  listPinCandidates,
  listPinnedForUser,
  setPinsForUser,
} from "../lib/pinned-repos";

const pins = new Hono<AuthEnv>();

pins.get("/settings/pins", softAuth, requireAuth, async (c) => {
  const user = c.get("user")!;
  const [pinned, candidates] = await Promise.all([
    listPinnedForUser(user.id),
    listPinCandidates(user.id),
  ]);
  const pinnedIds = new Set(pinned.map((p) => p.repositoryId));
  const error = c.req.query("error");
  const saved = c.req.query("saved") === "1";

  return c.html(
    <Layout title="Pinned repositories" user={user}>
      <h2>Pinned repositories</h2>
      <p style="color: var(--text-muted); max-width: 620px">
        Choose up to {MAX_PINS} repositories to feature at the top of your
        profile. They appear in the order you select them.
      </p>

      {error && <div class="auth-error">{decodeURIComponent(error)}</div>}
      {saved && (
        <div
          style="padding: 10px 12px; background: rgba(63, 185, 80, 0.1); border: 1px solid var(--green); color: var(--green); border-radius: var(--radius); margin: 12px 0"
        >
          Saved.
        </div>
      )}

      <form method="POST" action="/settings/pins" style="margin-top: 16px">
        {candidates.length === 0 ? (
          <div class="empty-state">
            <p>You don't own any repositories yet.</p>
          </div>
        ) : (
          <ul
            style="list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 8px"
            data-testid="pin-candidates"
          >
            {candidates.map((c) => (
              <li
                style="border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 12px; background: var(--bg-secondary)"
              >
                <label
                  style="display: flex; align-items: center; gap: 10px; cursor: pointer; font-size: 13px"
                >
                  <input
                    type="checkbox"
                    name="repoId"
                    value={c.id}
                    checked={pinnedIds.has(c.id)}
                  />
                  <span style="flex: 1">
                    <strong>{c.name}</strong>
                    {c.isPrivate && (
                      <span
                        style="margin-left: 6px; font-size: 10px; padding: 1px 6px; border-radius: 10px; background: rgba(139, 148, 158, 0.15); color: var(--text-muted); text-transform: uppercase"
                      >
                        Private
                      </span>
                    )}
                  </span>
                </label>
              </li>
            ))}
          </ul>
        )}

        <div style="margin-top: 16px; display: flex; gap: 8px; align-items: center">
          <button type="submit" class="btn btn-primary">
            Save pinned repositories
          </button>
          <span style="color: var(--text-muted); font-size: 12px">
            The first {MAX_PINS} you tick are kept.
          </span>
        </div>
      </form>

      {pinned.length > 0 && (
        <div style="margin-top: 28px">
          <h3>Current pins (preview)</h3>
          <ul
            style="list-style: none; padding: 0; margin: 8px 0 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 8px"
          >
            {pinned.map((p) => (
              <li
                style="border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 12px"
              >
                <div>
                  <a href={`/${p.ownerUsername}/${p.name}`}>
                    <strong>{p.ownerUsername}/{p.name}</strong>
                  </a>
                </div>
                {p.description && (
                  <div style="color: var(--text-muted); font-size: 12px; margin-top: 4px">
                    {p.description}
                  </div>
                )}
                <div style="color: var(--text-muted); font-size: 11px; margin-top: 6px">
                  {p.starCount} {"\u2605"} · {p.forkCount} forks
                </div>
              </li>
            ))}
          </ul>
        </div>
      )}
    </Layout>
  );
});

pins.post("/settings/pins", softAuth, requireAuth, async (c) => {
  const user = c.get("user")!;
  const form = await c.req.parseBody({ all: true });
  const raw = form.repoId;
  const ids: string[] = Array.isArray(raw)
    ? raw.map((x) => String(x))
    : raw != null
      ? [String(raw)]
      : [];
  await setPinsForUser(user.id, ids);
  return c.redirect("/settings/pins?saved=1");
});

export default pins;