web/src/routes/settings/+page.svelte (view raw)
1<script lang="ts">
2 import { onMount } from 'svelte';
3 import { uploadAvatar, getMe } from '$lib/api';
4 import Avatar from '$lib/Avatar.svelte';
5
6 let file = $state<File | null>(null);
7 let previewUrl = $state('');
8 let username = $state('');
9 let error = $state('');
10 let success = $state('');
11 let loading = $state(false);
12
13 onMount(async () => {
14 try {
15 const u = await getMe();
16 username = u.username;
17 if (u.avatar_url) {
18 previewUrl = u.avatar_url;
19 }
20 } catch {}
21 });
22
23 function handleFileSelect(e: Event) {
24 const f = (e.target as HTMLInputElement).files?.[0];
25 if (!f) return;
26 file = f;
27 previewUrl = URL.createObjectURL(f);
28 error = '';
29 success = '';
30 }
31
32 async function handleSubmit(e: Event) {
33 e.preventDefault();
34 if (!file) return;
35 error = '';
36 success = '';
37 loading = true;
38 try {
39 const result = await uploadAvatar(file);
40 success = 'Avatar updated.';
41 previewUrl = result.avatar_url;
42 file = null;
43 } catch (err) {
44 error = err instanceof Error ? err.message : 'Upload failed';
45 } finally {
46 loading = false;
47 }
48 }
49</script>
50
51<div class="page-head">
52 <h1>SETTINGS</h1>
53 <div class="head-line"></div>
54</div>
55
56<form onsubmit={handleSubmit}>
57 {#if error}
58 <p class="error">{error}</p>
59 {/if}
60 {#if success}
61 <p class="success">{success}</p>
62 {/if}
63
64 <div class="avatar-section">
65 <div class="avatar-preview">
66 <Avatar src={previewUrl} username={username} size={100} />
67 </div>
68
69 <label>
70 <span class="label-text">AVATAR IMAGE</span>
71 <div class="file-wrap">
72 <input type="file" id="avatar-input" accept="image/*"
73 onchange={handleFileSelect}
74 disabled={loading}
75 />
76 <label for="avatar-input" class="file-label">
77 {file ? file.name : 'CHOOSE IMAGE'}
78 </label>
79 </div>
80 </label>
81 </div>
82
83 <button type="submit" class="btn" disabled={loading || !file}>
84 {loading ? 'UPLOADING...' : 'UPLOAD AVATAR'}
85 </button>
86</form>
87
88<style>
89 .page-head {
90 margin-bottom: 1.5rem;
91 }
92 .page-head h1 {
93 margin: 0;
94 }
95 .head-line {
96 height: 3px;
97 width: 3rem;
98 background: #c62828;
99 margin-top: 0.4rem;
100 }
101
102 form {
103 display: flex;
104 flex-direction: column;
105 gap: 1rem;
106 max-width: 400px;
107 }
108
109 .error {
110 color: #c62828;
111 font-family: 'Inter', sans-serif;
112 font-size: 0.85rem;
113 font-weight: 600;
114 }
115 .success {
116 color: #2e7d32;
117 font-family: 'Inter', sans-serif;
118 font-size: 0.85rem;
119 font-weight: 600;
120 }
121
122 .avatar-section {
123 display: flex;
124 gap: 1.5rem;
125 align-items: flex-start;
126 }
127 .avatar-preview {
128 width: 100px;
129 height: 100px;
130 background: #1a1a1a;
131 flex-shrink: 0;
132 display: flex;
133 align-items: center;
134 justify-content: center;
135 overflow: hidden;
136 }
137
138 .label-text {
139 font-family: 'Oswald', sans-serif;
140 font-weight: 500;
141 font-size: 0.75rem;
142 letter-spacing: 0.15em;
143 color: #888;
144 }
145 .file-wrap {
146 position: relative;
147 margin-top: 0.25rem;
148 }
149 #avatar-input {
150 position: absolute;
151 width: 1px;
152 height: 1px;
153 opacity: 0;
154 pointer-events: none;
155 }
156 .file-label {
157 display: flex;
158 align-items: center;
159 padding: 0.55rem 0.75rem;
160 border: 2px solid #1a1a1a;
161 font-family: 'Inter', sans-serif;
162 font-size: 0.85rem;
163 font-weight: 500;
164 color: #888;
165 background: #fff;
166 cursor: pointer;
167 transition: border-color 0.15s;
168 }
169 .file-label:hover {
170 border-color: #c62828;
171 }
172
173 .btn {
174 display: inline-flex;
175 align-items: center;
176 justify-content: center;
177 padding: 0.6rem 1.5rem;
178 background: #1a1a1a;
179 color: #fff;
180 border: none;
181 font-family: 'Oswald', sans-serif;
182 font-weight: 500;
183 font-size: 0.85rem;
184 letter-spacing: 0.1em;
185 cursor: pointer;
186 text-transform: uppercase;
187 transition: background 0.15s;
188 align-self: flex-start;
189 }
190 .btn:hover {
191 background: #c62828;
192 }
193 .btn:disabled {
194 opacity: 0.4;
195 cursor: default;
196 }
197</style>