web/src/routes/+layout.svelte (view raw)
1<script lang="ts">
2 import { onMount } from 'svelte';
3 import { page } from '$app/state';
4 import { logout, getMe, getConfig, type CurrentUser, type SiteConfig } from '$lib/api';
5 import Avatar from '$lib/Avatar.svelte';
6
7 let { children } = $props();
8
9 let user = $state<CurrentUser | null>(null);
10 let checking = $state(true);
11 let siteConfig = $state<SiteConfig | null>(null);
12 let baseTitle = $state('weimar');
13
14 onMount(() => {
15 getConfig()
16 .then((c) => {
17 siteConfig = c;
18 baseTitle = c.site_title;
19 })
20 .catch(() => {});
21 getMe()
22 .then((u) => { user = u; })
23 .catch(() => {})
24 .finally(() => { checking = false; });
25 });
26
27 $effect(() => {
28 const path = page.url.pathname;
29 const base = baseTitle;
30 switch (path) {
31 case '/browse':
32 document.title = base + ' - Browse';
33 break;
34 case '/upload':
35 document.title = base + ' - Upload';
36 break;
37 case '/settings':
38 document.title = base + ' - Settings';
39 break;
40 case '/login':
41 document.title = base + ' - Login';
42 break;
43 default:
44 if (path.startsWith('/image/')) {
45 document.title = base + ' - Image #' + path.split('/')[2];
46 } else {
47 document.title = base;
48 }
49 }
50 });
51
52 async function handleLogout() {
53 await logout();
54 user = null;
55 window.location.href = '/login';
56 }
57</script>
58
59<nav>
60 <div class="nav-brand">
61 <a href="/" class="nav-logo">
62 {#if siteConfig?.logo_url}
63 <img src={siteConfig.logo_url} alt={siteConfig.site_title} class="nav-logo-img" />
64 {:else}
65 {siteConfig?.site_title ?? 'weimar'}
66 {/if}
67 </a>
68 <span class="nav-divider"></span>
69 </div>
70 <div class="nav-links">
71 <a href="/browse">BROWSE</a>
72 <a href="/upload">UPLOAD</a>
73 {#if !checking}
74 {#if user}
75 <a href="/settings" class="nav-avatar-link">
76 <Avatar src={user.avatar_url} username={user.username} size={28} />
77 </a>
78 <button class="link logout" onclick={handleLogout}>LOGOUT</button>
79 {:else}
80 <a href="/login">LOGIN</a>
81 {/if}
82 {/if}
83 </div>
84</nav>
85
86<div class="main">
87 {@render children()}
88</div>
89
90<style>
91 :global(*) {
92 margin: 0;
93 padding: 0;
94 box-sizing: border-box;
95 }
96
97 :global(body) {
98 font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
99 background: #f5f5f5;
100 color: #1a1a1a;
101 -webkit-font-smoothing: antialiased;
102 }
103
104 :global(h1) {
105 font-family: 'Oswald', sans-serif;
106 font-weight: 700;
107 font-size: 2.5rem;
108 text-transform: uppercase;
109 letter-spacing: 0.03em;
110 line-height: 1.1;
111 color: #1a1a1a;
112 }
113
114 :global(h2) {
115 font-family: 'Oswald', sans-serif;
116 font-weight: 600;
117 font-size: 1.5rem;
118 text-transform: uppercase;
119 letter-spacing: 0.05em;
120 }
121
122 :global(a) {
123 color: #c62828;
124 text-decoration: none;
125 font-weight: 600;
126 }
127
128 :global(a:hover) {
129 text-decoration: underline;
130 }
131
132 nav {
133 display: flex;
134 align-items: center;
135 justify-content: space-between;
136 padding: 0 1.5rem;
137 height: 3.5rem;
138 background: #1a1a1a;
139 border-bottom: 3px solid #c62828;
140 }
141
142 .nav-brand {
143 display: flex;
144 align-items: center;
145 gap: 1rem;
146 }
147
148 .nav-logo {
149 font-family: 'Oswald', sans-serif;
150 font-weight: 700;
151 font-size: 1.4rem;
152 letter-spacing: 0.15em;
153 color: #fff;
154 text-decoration: none;
155 }
156 .nav-logo:hover {
157 text-decoration: none;
158 color: #ef5350;
159 }
160 .nav-logo-img {
161 display: block;
162 height: 1.8rem;
163 width: auto;
164 }
165
166 .nav-divider {
167 display: block;
168 width: 1px;
169 height: 1.6rem;
170 background: rgba(255,255,255,0.2);
171 }
172
173 .nav-links {
174 display: flex;
175 align-items: center;
176 gap: 0;
177 height: 100%;
178 }
179
180 .nav-links a, .logout {
181 display: flex;
182 align-items: center;
183 height: 100%;
184 padding: 0 1.25rem;
185 font-family: 'Oswald', sans-serif;
186 font-weight: 500;
187 font-size: 0.85rem;
188 letter-spacing: 0.12em;
189 color: rgba(255,255,255,0.8);
190 text-decoration: none;
191 text-transform: uppercase;
192 transition: background 0.15s, color 0.15s;
193 position: relative;
194 }
195
196 .nav-links a:hover, .logout:hover {
197 background: #c62828;
198 color: #fff;
199 text-decoration: none;
200 }
201
202 .nav-links a::after {
203 content: '';
204 position: absolute;
205 bottom: 0;
206 left: 0;
207 right: 0;
208 height: 0;
209 background: #c62828;
210 transition: height 0.15s;
211 }
212
213 .nav-links a:hover::after {
214 height: 3px;
215 }
216
217 .nav-avatar-link {
218 padding: 0 0.75rem !important;
219 }
220 .nav-avatar-link::after {
221 display: none !important;
222 }
223 .nav-avatar-link:hover {
224 background: none !important;
225 }
226
227 .logout {
228 background: none;
229 border: none;
230 cursor: pointer;
231 font-size: 0.85rem;
232 }
233
234 .main {
235 max-width: 1400px;
236 margin: 0 auto;
237 padding: 1.5rem;
238 }
239</style>