upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/http/landing.rs206
1 files changed, 189 insertions, 17 deletions
diff --git a/src/http/landing.rs b/src/http/landing.rs
index 8b416f1..b978851 100644
--- a/src/http/landing.rs
+++ b/src/http/landing.rs
@@ -24,9 +24,136 @@ fn get_footer_script() -> &'static str {
24 </script>"# 24 </script>"#
25} 25}
26 26
27/// Generate the theme toggle HTML button
28fn get_theme_toggle_html() -> &'static str {
29 r##"<button class="theme-toggle" id="theme-toggle" aria-label="Toggle theme" title="Toggle light/dark mode">
30 <svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
31 <path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58a.996.996 0 00-1.41 0 .996.996 0 000 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37a.996.996 0 00-1.41 0 .996.996 0 000 1.41l1.06 1.06c.39.39 1.03.39 1.41 0a.996.996 0 000-1.41l-1.06-1.06zm1.06-10.96a.996.996 0 000-1.41.996.996 0 00-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06zM7.05 18.36a.996.996 0 000-1.41.996.996 0 00-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06z"/>
32 </svg>
33 <svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
34 <path d="M12 3a9 9 0 109 9c0-.46-.04-.92-.1-1.36a5.389 5.389 0 01-4.4 2.26 5.403 5.403 0 01-3.14-9.8c-.44-.06-.9-.1-1.36-.1z"/>
35 </svg>
36 </button>"##
37}
38
39/// Generate the theme toggle JavaScript
40fn get_theme_script() -> &'static str {
41 r#"<script>
42 (function() {
43 const THEME_KEY = 'grasp-theme';
44 const toggle = document.getElementById('theme-toggle');
45
46 // Get saved theme or null (use system preference)
47 function getSavedTheme() {
48 try {
49 return localStorage.getItem(THEME_KEY);
50 } catch (e) {
51 return null;
52 }
53 }
54
55 // Save theme preference
56 function saveTheme(theme) {
57 try {
58 if (theme) {
59 localStorage.setItem(THEME_KEY, theme);
60 } else {
61 localStorage.removeItem(THEME_KEY);
62 }
63 } catch (e) {}
64 }
65
66 // Get current effective theme
67 function getCurrentTheme() {
68 const saved = getSavedTheme();
69 if (saved) return saved;
70 return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
71 }
72
73 // Apply theme to document
74 function applyTheme(theme) {
75 if (theme) {
76 document.documentElement.setAttribute('data-theme', theme);
77 } else {
78 document.documentElement.removeAttribute('data-theme');
79 }
80 }
81
82 // Initialize theme on page load
83 const savedTheme = getSavedTheme();
84 if (savedTheme) {
85 applyTheme(savedTheme);
86 }
87
88 // Toggle theme on button click
89 if (toggle) {
90 toggle.addEventListener('click', function() {
91 const current = getCurrentTheme();
92 const newTheme = current === 'dark' ? 'light' : 'dark';
93 applyTheme(newTheme);
94 saveTheme(newTheme);
95 });
96 }
97
98 // Listen for system theme changes
99 window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', function(e) {
100 // Only react if no manual preference is set
101 if (!getSavedTheme()) {
102 // Theme will auto-update via CSS, no JS needed
103 }
104 });
105 })();
106 </script>"#
107}
108
27/// Generate the common base CSS used across all pages 109/// Generate the common base CSS used across all pages
28fn get_base_css() -> &'static str { 110fn get_base_css() -> &'static str {
29 r#":root { 111 r#"/* Dark mode (default) */
112 :root {
113 --brand: #4434FF;
114 --brand-light: #6b5fff;
115 --bg: #0a0a0f;
116 --surface: #12121a;
117 --border: #1e1e2e;
118 --text: #e4e4eb;
119 --text-muted: #a8a8bd;
120 --error: #ff4444;
121 --success: #22c55e;
122 --logo-bg: #4434FF;
123 --logo-icon: white;
124 }
125 /* Light mode - system preference */
126 @media (prefers-color-scheme: light) {
127 :root:not([data-theme="dark"]) {
128 --brand: #4434FF;
129 --brand-light: #3525cc;
130 --bg: #f8f9fa;
131 --surface: #ffffff;
132 --border: #e1e4e8;
133 --text: #1a1a2e;
134 --text-muted: #586069;
135 --error: #dc3545;
136 --success: #28a745;
137 --logo-bg: #4434FF;
138 --logo-icon: white;
139 }
140 }
141 /* Manual light mode override */
142 :root[data-theme="light"] {
143 --brand: #4434FF;
144 --brand-light: #3525cc;
145 --bg: #f8f9fa;
146 --surface: #ffffff;
147 --border: #e1e4e8;
148 --text: #1a1a2e;
149 --text-muted: #586069;
150 --error: #dc3545;
151 --success: #28a745;
152 --logo-bg: #4434FF;
153 --logo-icon: white;
154 }
155 /* Manual dark mode override */
156 :root[data-theme="dark"] {
30 --brand: #4434FF; 157 --brand: #4434FF;
31 --brand-light: #6b5fff; 158 --brand-light: #6b5fff;
32 --bg: #0a0a0f; 159 --bg: #0a0a0f;
@@ -36,6 +163,8 @@ fn get_base_css() -> &'static str {
36 --text-muted: #a8a8bd; 163 --text-muted: #a8a8bd;
37 --error: #ff4444; 164 --error: #ff4444;
38 --success: #22c55e; 165 --success: #22c55e;
166 --logo-bg: #4434FF;
167 --logo-icon: white;
39 } 168 }
40 * { margin: 0; padding: 0; box-sizing: border-box; } 169 * { margin: 0; padding: 0; box-sizing: border-box; }
41 body { 170 body {
@@ -44,6 +173,7 @@ fn get_base_css() -> &'static str {
44 background: var(--bg); 173 background: var(--bg);
45 color: var(--text); 174 color: var(--text);
46 min-height: 100vh; 175 min-height: 100vh;
176 transition: background-color 0.3s ease, color 0.3s ease;
47 } 177 }
48 a { color: var(--brand-light); text-decoration: none; } 178 a { color: var(--brand-light); text-decoration: none; }
49 a:hover { text-decoration: underline; } 179 a:hover { text-decoration: underline; }
@@ -55,6 +185,44 @@ fn get_base_css() -> &'static str {
55 font-size: 0.875rem; 185 font-size: 0.875rem;
56 color: var(--brand-light); 186 color: var(--brand-light);
57 } 187 }
188 /* Theme toggle button */
189 .theme-toggle {
190 position: fixed;
191 top: 16px;
192 right: 16px;
193 z-index: 1000;
194 background: var(--surface);
195 border: 1px solid var(--border);
196 border-radius: 50%;
197 width: 44px;
198 height: 44px;
199 cursor: pointer;
200 display: flex;
201 align-items: center;
202 justify-content: center;
203 transition: all 0.3s ease;
204 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
205 }
206 .theme-toggle:hover {
207 transform: scale(1.1);
208 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
209 }
210 .theme-toggle svg {
211 width: 20px;
212 height: 20px;
213 fill: var(--text);
214 transition: fill 0.3s ease;
215 }
216 .theme-toggle .sun-icon { display: none; }
217 .theme-toggle .moon-icon { display: block; }
218 :root[data-theme="light"] .theme-toggle .sun-icon,
219 :root:not([data-theme="dark"]) .theme-toggle .sun-icon { display: block; }
220 :root[data-theme="light"] .theme-toggle .moon-icon,
221 :root:not([data-theme="dark"]) .theme-toggle .moon-icon { display: none; }
222 @media (prefers-color-scheme: dark) {
223 :root:not([data-theme="light"]) .theme-toggle .sun-icon { display: none; }
224 :root:not([data-theme="light"]) .theme-toggle .moon-icon { display: block; }
225 }
58 .footer { 226 .footer {
59 margin-top: 48px; 227 margin-top: 48px;
60 padding-top: 24px; 228 padding-top: 24px;
@@ -78,6 +246,12 @@ fn get_base_css() -> &'static str {
78 height: 48px; 246 height: 48px;
79 flex-shrink: 0; 247 flex-shrink: 0;
80 } 248 }
249 .software-logo rect {
250 fill: var(--logo-bg);
251 }
252 .software-logo path {
253 fill: var(--logo-icon);
254 }
81 .software-content { 255 .software-content {
82 flex: 1; 256 flex: 1;
83 } 257 }
@@ -100,22 +274,6 @@ fn get_base_css() -> &'static str {
100 }"# 274 }"#
101} 275}
102 276
103/// Generate the software-box HTML component
104fn get_software_box_html() -> &'static str {
105 r##"<div class="card">
106 <div class="software-box">
107 <svg class="software-logo" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg">
108 <rect width="38" height="38" rx="12" fill="#4434FF"/>
109 <path d="M10.6731 30.6348C8.83687 30.6346 7.34885 29.1458 7.34885 27.3096C7.34891 26.2473 7.84783 25.303 8.62326 24.6943C8.21265 23.3055 7.86571 22.049 7.45334 20.6758C6.90247 18.8412 7.4492 16.8197 8.93576 15.5605L15.7512 9.78906C15.6931 9.54286 15.6614 9.28642 15.6613 9.02246C15.6613 7.51617 16.6628 6.24465 18.0363 5.83594L18.0363 -1.11215e-06C18.511 0.000462658 18.4612 0.000975391 18.9856 0.000975533C19.5102 0.000975578 19.5802 -1.11589e-06 19.9367 -9.46012e-07L19.9367 5.83594C21.3097 6.24503 22.3108 7.5166 22.3108 9.02246C22.3107 9.29118 22.2792 9.55249 22.219 9.80273L29.0783 15.6123C30.5229 16.8359 31.1022 18.8013 30.5539 20.6133L29.3254 24.6758C30.1142 25.2837 30.6232 26.2367 30.6233 27.3096C30.6233 29.1459 29.1344 30.6348 27.2981 30.6348C25.4619 30.6346 23.9738 29.1458 23.9738 27.3096C23.974 25.4734 25.4619 23.9846 27.2981 23.9844C27.3814 23.9844 27.4643 23.9891 27.5461 23.9951L28.7356 20.0625C29.0645 18.9753 28.7166 17.7966 27.8498 17.0625L21.2424 11.4648C20.8746 11.8048 20.4294 12.0622 19.9367 12.209L19.9367 18.9258C21.0425 19.3175 21.836 20.3694 21.8362 21.6094C21.8362 23.1834 20.5596 24.46 18.9856 24.46C17.4117 24.4598 16.136 23.1833 16.136 21.6094C16.1361 20.3689 16.93 19.3172 18.0363 18.9258L18.0363 12.21C17.5395 12.0622 17.0916 11.801 16.7219 11.457L10.1643 17.0107C9.27919 17.7605 8.93068 18.9867 9.27365 20.1289C9.68708 21.5056 10.0175 22.7009 10.3986 23.998C10.4892 23.9906 10.5806 23.9844 10.6731 23.9844C12.5093 23.9844 13.9981 25.4733 13.9983 27.3096C13.9983 29.1459 12.5094 30.6348 10.6731 30.6348Z" fill="white"/>
110 </svg>
111 <div class="software-content">
112 <h3 class="software-heading"><a href="https://gitworkshop.dev/danconwaydev.com/ngit-grasp">Grasp Server</a> Powered by <a href="https://gitworkshop.dev/danconwaydev.com/ngit-grasp">ngit-grasp</a></h3>
113 <p class="software-desc">Git hosting distributed across relays using Nostr for authorization. <a href="https://ngit.dev/grasp">Find out more...</a></p>
114 </div>
115 </div>
116 </div>"##
117}
118
119/// Generate the HTML landing page 277/// Generate the HTML landing page
120pub fn get_html(config: &Config) -> String { 278pub fn get_html(config: &Config) -> String {
121 // Curation matches NIP-11 document - currently None for this relay 279 // Curation matches NIP-11 document - currently None for this relay
@@ -128,6 +286,8 @@ pub fn get_html(config: &Config) -> String {
128 relay_description = config.relay_description, 286 relay_description = config.relay_description,
129 version = get_version(), 287 version = get_version(),
130 curation = curation, 288 curation = curation,
289 theme_toggle = get_theme_toggle_html(),
290 theme_script = get_theme_script(),
131 ) 291 )
132} 292}
133 293
@@ -180,6 +340,7 @@ pub fn get_generic_404_html(config: &Config, path: &str) -> String {
180 </style> 340 </style>
181</head> 341</head>
182<body> 342<body>
343 {theme_toggle}
183 <div class="container"> 344 <div class="container">
184 <div class="error-code">404</div> 345 <div class="error-code">404</div>
185 <h2>Page Not Found</h2> 346 <h2>Page Not Found</h2>
@@ -192,6 +353,7 @@ pub fn get_generic_404_html(config: &Config, path: &str) -> String {
192 <div class="footer"><span id="footer-domain"></span><span class="footer-separator">•</span>powered by <a href="https://gitworkshop.dev/danconwaydev.com/ngit-grasp"><strong>ngit-grasp</strong></a><span class="footer-separator">•</span>{version}<span class="footer-separator">•</span>MIT Licensed</div> 353 <div class="footer"><span id="footer-domain"></span><span class="footer-separator">•</span>powered by <a href="https://gitworkshop.dev/danconwaydev.com/ngit-grasp"><strong>ngit-grasp</strong></a><span class="footer-separator">•</span>{version}<span class="footer-separator">•</span>MIT Licensed</div>
193 </div> 354 </div>
194 {footer_script} 355 {footer_script}
356 {theme_script}
195</body> 357</body>
196</html>"##, 358</html>"##,
197 base_css = get_base_css(), 359 base_css = get_base_css(),
@@ -199,6 +361,8 @@ pub fn get_generic_404_html(config: &Config, path: &str) -> String {
199 path = path, 361 path = path,
200 version = get_version(), 362 version = get_version(),
201 footer_script = get_footer_script(), 363 footer_script = get_footer_script(),
364 theme_toggle = get_theme_toggle_html(),
365 theme_script = get_theme_script(),
202 ) 366 )
203} 367}
204 368
@@ -268,6 +432,7 @@ pub fn get_404_html(config: &Config, npub: &str, identifier: &str) -> String {
268 </style> 432 </style>
269</head> 433</head>
270<body> 434<body>
435 {theme_toggle}
271 <div class="container"> 436 <div class="container">
272 <div class="error-code">404</div> 437 <div class="error-code">404</div>
273 <h2>Repository Not Found</h2> 438 <h2>Repository Not Found</h2>
@@ -287,6 +452,7 @@ pub fn get_404_html(config: &Config, npub: &str, identifier: &str) -> String {
287 <div class="footer"><span id="footer-domain"></span><span class="footer-separator">•</span>powered by <a href="https://gitworkshop.dev/danconwaydev.com/ngit-grasp"><strong>ngit-grasp</strong></a><span class="footer-separator">•</span>{version}<span class="footer-separator">•</span>MIT Licensed</div> 452 <div class="footer"><span id="footer-domain"></span><span class="footer-separator">•</span>powered by <a href="https://gitworkshop.dev/danconwaydev.com/ngit-grasp"><strong>ngit-grasp</strong></a><span class="footer-separator">•</span>{version}<span class="footer-separator">•</span>MIT Licensed</div>
288 </div> 453 </div>
289 {footer_script} 454 {footer_script}
455 {theme_script}
290</body> 456</body>
291</html>"##, 457</html>"##,
292 base_css = get_base_css(), 458 base_css = get_base_css(),
@@ -295,6 +461,8 @@ pub fn get_404_html(config: &Config, npub: &str, identifier: &str) -> String {
295 identifier = identifier, 461 identifier = identifier,
296 version = get_version(), 462 version = get_version(),
297 footer_script = get_footer_script(), 463 footer_script = get_footer_script(),
464 theme_toggle = get_theme_toggle_html(),
465 theme_script = get_theme_script(),
298 ) 466 )
299} 467}
300 468
@@ -377,6 +545,7 @@ pub fn get_repo_html(config: &Config, npub: &str, identifier: &str) -> String {
377 </style> 545 </style>
378</head> 546</head>
379<body> 547<body>
548 {theme_toggle}
380 <div class="container"> 549 <div class="container">
381 <div class="back-link"> 550 <div class="back-link">
382 <a href="/">&larr; {relay_name}</a> 551 <a href="/">&larr; {relay_name}</a>
@@ -425,6 +594,7 @@ pub fn get_repo_html(config: &Config, npub: &str, identifier: &str) -> String {
425 footerDomain.textContent = host; 594 footerDomain.textContent = host;
426 }} 595 }}
427 </script> 596 </script>
597 {theme_script}
428</body> 598</body>
429</html>"##, 599</html>"##,
430 base_css = get_base_css(), 600 base_css = get_base_css(),
@@ -432,5 +602,7 @@ pub fn get_repo_html(config: &Config, npub: &str, identifier: &str) -> String {
432 npub = npub, 602 npub = npub,
433 identifier = identifier, 603 identifier = identifier,
434 version = get_version(), 604 version = get_version(),
605 theme_toggle = get_theme_toggle_html(),
606 theme_script = get_theme_script(),
435 ) 607 )
436} 608}