this repo has no description
1<script lang="ts">
2 import { getAuthState } from '../lib/auth.svelte'
3 import { navigate } from '../lib/router.svelte'
4 import { api, type InviteCode, ApiError } from '../lib/api'
5 const auth = getAuthState()
6 let codes = $state<InviteCode[]>([])
7 let loading = $state(true)
8 let error = $state<string | null>(null)
9 let creating = $state(false)
10 let createdCode = $state<string | null>(null)
11 $effect(() => {
12 if (!auth.loading && !auth.session) {
13 navigate('/login')
14 }
15 })
16 $effect(() => {
17 if (auth.session) {
18 loadCodes()
19 }
20 })
21 async function loadCodes() {
22 if (!auth.session) return
23 loading = true
24 error = null
25 try {
26 const result = await api.getAccountInviteCodes(auth.session.accessJwt)
27 codes = result.codes
28 } catch (e) {
29 error = e instanceof ApiError ? e.message : 'Failed to load invite codes'
30 } finally {
31 loading = false
32 }
33 }
34 async function handleCreate() {
35 if (!auth.session) return
36 creating = true
37 error = null
38 try {
39 const result = await api.createInviteCode(auth.session.accessJwt, 1)
40 createdCode = result.code
41 await loadCodes()
42 } catch (e) {
43 error = e instanceof ApiError ? e.message : 'Failed to create invite code'
44 } finally {
45 creating = false
46 }
47 }
48 function dismissCreated() {
49 createdCode = null
50 }
51 function copyCode(code: string) {
52 navigator.clipboard.writeText(code)
53 }
54</script>
55<div class="page">
56 <header>
57 <a href="#/dashboard" class="back">← Dashboard</a>
58 <h1>Invite Codes</h1>
59 </header>
60 <p class="description">
61 Invite codes let you invite friends to join. Each code can be used once.
62 </p>
63 {#if error}
64 <div class="error">{error}</div>
65 {/if}
66 {#if createdCode}
67 <div class="created-code">
68 <h3>Invite Code Created</h3>
69 <div class="code-display">
70 <code>{createdCode}</code>
71 <button class="copy" onclick={() => copyCode(createdCode!)}>Copy</button>
72 </div>
73 <button onclick={dismissCreated}>Done</button>
74 </div>
75 {/if}
76 <section class="create-section">
77 <button onclick={handleCreate} disabled={creating}>
78 {creating ? 'Creating...' : 'Create New Invite Code'}
79 </button>
80 </section>
81 <section class="list-section">
82 <h2>Your Invite Codes</h2>
83 {#if loading}
84 <p class="empty">Loading...</p>
85 {:else if codes.length === 0}
86 <p class="empty">No invite codes yet</p>
87 {:else}
88 <ul class="code-list">
89 {#each codes as code}
90 <li class:disabled={code.disabled} class:used={code.uses.length > 0 && code.available === 0}>
91 <div class="code-main">
92 <code>{code.code}</code>
93 <button class="copy-small" onclick={() => copyCode(code.code)} title="Copy">
94 Copy
95 </button>
96 </div>
97 <div class="code-meta">
98 <span class="date">Created {new Date(code.createdAt).toLocaleDateString()}</span>
99 {#if code.disabled}
100 <span class="status disabled">Disabled</span>
101 {:else if code.uses.length > 0}
102 <span class="status used">Used by @{code.uses[0].usedBy.split(':').pop()}</span>
103 {:else}
104 <span class="status available">Available</span>
105 {/if}
106 </div>
107 </li>
108 {/each}
109 </ul>
110 {/if}
111 </section>
112</div>
113<style>
114 .page {
115 max-width: 600px;
116 margin: 0 auto;
117 padding: 2rem;
118 }
119 header {
120 margin-bottom: 1rem;
121 }
122 .back {
123 color: var(--text-secondary);
124 text-decoration: none;
125 font-size: 0.875rem;
126 }
127 .back:hover {
128 color: var(--accent);
129 }
130 h1 {
131 margin: 0.5rem 0 0 0;
132 }
133 .description {
134 color: var(--text-secondary);
135 margin-bottom: 2rem;
136 }
137 .error {
138 padding: 0.75rem;
139 background: var(--error-bg);
140 border: 1px solid var(--error-border);
141 border-radius: 4px;
142 color: var(--error-text);
143 margin-bottom: 1rem;
144 }
145 .created-code {
146 padding: 1.5rem;
147 background: var(--success-bg);
148 border: 1px solid var(--success-border);
149 border-radius: 8px;
150 margin-bottom: 2rem;
151 }
152 .created-code h3 {
153 margin: 0 0 1rem 0;
154 color: var(--success-text);
155 }
156 .code-display {
157 display: flex;
158 align-items: center;
159 gap: 1rem;
160 background: var(--bg-card);
161 padding: 1rem;
162 border-radius: 4px;
163 margin-bottom: 1rem;
164 }
165 .code-display code {
166 font-size: 1.125rem;
167 font-family: monospace;
168 flex: 1;
169 }
170 .copy {
171 padding: 0.5rem 1rem;
172 background: var(--accent);
173 color: white;
174 border: none;
175 border-radius: 4px;
176 cursor: pointer;
177 }
178 .copy:hover {
179 background: var(--accent-hover);
180 }
181 .create-section {
182 margin-bottom: 2rem;
183 }
184 .create-section button {
185 padding: 0.75rem 1.5rem;
186 background: var(--accent);
187 color: white;
188 border: none;
189 border-radius: 4px;
190 cursor: pointer;
191 font-size: 1rem;
192 }
193 .create-section button:hover:not(:disabled) {
194 background: var(--accent-hover);
195 }
196 .create-section button:disabled {
197 opacity: 0.6;
198 cursor: not-allowed;
199 }
200 section h2 {
201 font-size: 1.125rem;
202 margin: 0 0 1rem 0;
203 }
204 .code-list {
205 list-style: none;
206 padding: 0;
207 margin: 0;
208 }
209 .code-list li {
210 padding: 1rem;
211 border: 1px solid var(--border-color);
212 border-radius: 4px;
213 margin-bottom: 0.5rem;
214 background: var(--bg-card);
215 }
216 .code-list li.disabled {
217 opacity: 0.6;
218 }
219 .code-list li.used {
220 background: var(--bg-secondary);
221 }
222 .code-main {
223 display: flex;
224 align-items: center;
225 gap: 0.5rem;
226 margin-bottom: 0.5rem;
227 }
228 .code-main code {
229 font-family: monospace;
230 font-size: 0.9rem;
231 }
232 .copy-small {
233 padding: 0.25rem 0.5rem;
234 background: var(--bg-secondary);
235 border: 1px solid var(--border-color);
236 border-radius: 4px;
237 font-size: 0.75rem;
238 cursor: pointer;
239 color: var(--text-primary);
240 }
241 .copy-small:hover {
242 background: var(--bg-input-disabled);
243 }
244 .code-meta {
245 display: flex;
246 gap: 1rem;
247 font-size: 0.875rem;
248 }
249 .date {
250 color: var(--text-secondary);
251 }
252 .status {
253 padding: 0.125rem 0.5rem;
254 border-radius: 4px;
255 font-size: 0.75rem;
256 }
257 .status.available {
258 background: var(--success-bg);
259 color: var(--success-text);
260 }
261 .status.used {
262 background: var(--bg-secondary);
263 color: var(--text-secondary);
264 }
265 .status.disabled {
266 background: var(--error-bg);
267 color: var(--error-text);
268 }
269 .empty {
270 color: var(--text-secondary);
271 text-align: center;
272 padding: 2rem;
273 }
274</style>