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