mirror your GitHub repos to tangled.org automatically
1<script setup lang="ts">
2import type { ConnectInfo } from '#server/api/connect/info.get'
3
4useSeoMeta({
5 title: 'Connect · synchub.to',
6 description: 'Connect your tangled identity to your GitHub installation.',
7})
8
9const route = useRoute()
10
11const installationId = computed(() => {
12 const raw = route.query.installation_id
13 return typeof raw === 'string' && /^\d+$/.test(raw) ? raw : null
14})
15const verified = computed(() => route.query.verified === '1')
16
17const { data: info } = await useFetch<ConnectInfo>('/api/connect/info', {
18 query: { installationId },
19 immediate: !!installationId.value,
20 ignoreResponseError: true,
21})
22
23const login = computed(() =>
24 (typeof route.query.login === 'string' ? route.query.login : null) ?? info.value?.login ?? null,
25)
26const accountLabel = computed(() => login.value ?? 'your GitHub account')
27</script>
28
29<template>
30 <div class="shell">
31 <header class="nav-term">
32 <nav class="nav-term__line" aria-label="primary">
33 <span class="prompt" aria-hidden="true">></span>
34 <a class="nav-term__mark" href="/"><SynchubMark :wordmark="true" :size="18" /></a>
35 </nav>
36 </header>
37
38 <main class="connect">
39 <div v-if="!installationId" class="panel">
40 <h1 class="connect__title">No installation to connect</h1>
41 <p class="connect__body">
42 Start by installing the GitHub App on the repositories you'd like to
43 mirror.
44 </p>
45 <a class="btn btn--primary" href="https://github.com/apps/synchub-to/installations/new">Install the GitHub App</a>
46 </div>
47
48 <div v-else-if="!verified" class="panel">
49 <p class="connect__eyebrow">step 1 of 2 · verify</p>
50 <h1 class="connect__title">Installed on <span class="connect__login">{{ accountLabel }}</span></h1>
51 <p class="connect__body">
52 Confirm you administer <strong>{{ accountLabel }}</strong> on GitHub,
53 then you'll pick the tangled handle that mirrors it. We check this so
54 nobody else can bind your repositories to their identity.
55 </p>
56 <a class="btn btn--primary" :href="`/api/github/oauth/start?installationId=${installationId}`">
57 Verify with GitHub
58 </a>
59 </div>
60
61 <div v-else class="panel">
62 <p class="connect__eyebrow">step 2 of 2 · connect</p>
63 <h1 class="connect__title">Connect a handle to <span class="connect__login">{{ accountLabel }}</span></h1>
64 <p class="connect__body">
65 Enter the tangled handle that should mirror
66 <strong>{{ accountLabel }}</strong>. One GitHub account maps to one
67 tangled identity; connecting a new handle replaces any previous one.
68 </p>
69 <form class="signin" action="/api/atproto/login" method="get">
70 <input type="hidden" name="installationId" :value="installationId">
71 <label class="signin__label">
72 <span>handle</span>
73 <span class="signin__field">
74 <span class="signin__prompt" aria-hidden="true">@</span>
75 <input
76 type="text"
77 name="handle"
78 required
79 placeholder="alice.bsky.social"
80 autocomplete="username"
81 autocapitalize="none"
82 autocorrect="off"
83 spellcheck="false"
84 >
85 </span>
86 </label>
87 <button class="btn btn--primary" type="submit">Connect</button>
88 </form>
89 </div>
90 </main>
91 </div>
92</template>
93
94<style scoped>
95.shell {
96 max-width: 52rem;
97 margin: 0 auto;
98 padding-inline: var(--page-gutter);
99}
100
101.nav-term {
102 padding: var(--space-md) 0;
103 border-bottom: var(--rule-hair) solid var(--color-rule);
104}
105
106.nav-term__line {
107 display: flex;
108 align-items: center;
109 gap: 0.6ch;
110 font-family: var(--font-mono);
111 font-size: var(--text-sm);
112 margin: 0;
113}
114
115.nav-term__line .prompt { color: var(--color-accent); }
116.nav-term__mark { display: inline-flex; text-decoration: none; }
117
118.connect {
119 padding-block: var(--space-2xl) var(--space-xl);
120}
121
122.panel {
123 max-width: 38rem;
124}
125
126.connect__eyebrow {
127 font-family: var(--font-mono);
128 font-size: var(--text-xs);
129 letter-spacing: 0.08em;
130 text-transform: uppercase;
131 color: var(--color-accent-dim);
132 margin: 0 0 var(--space-md);
133}
134
135.connect__title {
136 font-size: var(--text-xl);
137 line-height: 1.1;
138 margin: 0 0 var(--space-md);
139 overflow-wrap: anywhere;
140 min-width: 0;
141}
142
143.connect__login { color: var(--color-accent); }
144
145.connect__body {
146 max-width: var(--measure);
147 color: var(--color-muted);
148 margin: 0 0 var(--space-lg);
149}
150
151.signin {
152 display: flex;
153 flex-wrap: wrap;
154 align-items: end;
155 gap: var(--space-sm);
156}
157
158.signin__label {
159 display: flex;
160 flex-direction: column;
161 gap: var(--space-2xs);
162 font-family: var(--font-mono);
163 font-size: var(--text-xs);
164 letter-spacing: 0.06em;
165 text-transform: uppercase;
166 color: var(--color-neutral);
167}
168
169.signin__field {
170 display: flex;
171 align-items: center;
172 background: var(--color-paper-2);
173 border: var(--rule-hair) solid var(--color-rule-interactive);
174 border-radius: var(--radius-sm);
175}
176
177.signin__field:focus-within {
178 outline: 2px solid var(--color-focus);
179 outline-offset: 2px;
180 border-color: var(--color-accent);
181}
182
183.signin__prompt {
184 padding-inline: 0.75ch 0;
185 color: var(--color-accent);
186 font-family: var(--font-mono);
187}
188
189.signin input:focus-visible { outline: none; }
190
191.signin input {
192 flex: 1;
193 min-width: 14rem;
194 padding: 0.55rem 0.75rem 0.55rem 0.4ch;
195 border: 0;
196 background: transparent;
197 color: var(--color-ink);
198 font-family: var(--font-mono);
199 font-size: var(--text-base);
200}
201
202.signin input::placeholder { color: var(--color-neutral); }
203
204.btn {
205 display: inline-block;
206 padding: 0.55rem 1.1rem;
207 border: var(--rule-hair) solid var(--color-rule-interactive);
208 border-radius: var(--radius-sm);
209 background: var(--color-paper-2);
210 color: var(--color-ink);
211 font-family: var(--font-mono);
212 font-size: var(--text-sm);
213 text-decoration: none;
214 white-space: nowrap;
215 cursor: pointer;
216 transition: transform var(--dur-micro) var(--ease-out), border-color var(--dur-micro) var(--ease-out);
217}
218
219.btn:hover { border-color: var(--color-accent); transform: translateY(-1px); }
220.btn:active { transform: translateY(0); }
221.btn--primary { border-color: var(--color-accent); color: var(--color-accent); }
222</style>