+1
src/main.rs
+1
src/main.rs
+25
src/routes.rs
+25
src/routes.rs
···
346
346
"avatarUrl": null
347
347
}))
348
348
}
349
+
350
+
#[derive(Deserialize)]
351
+
pub struct ValidateUrlQuery {
352
+
url: String,
353
+
}
354
+
355
+
#[get("/api/validate-url")]
356
+
pub async fn validate_url(query: web::Query<ValidateUrlQuery>) -> HttpResponse {
357
+
let url = &query.url;
358
+
359
+
// Try to make a HEAD request with a short timeout
360
+
let client = reqwest::Client::builder()
361
+
.timeout(std::time::Duration::from_secs(3))
362
+
.build()
363
+
.unwrap();
364
+
365
+
let is_valid = match client.head(url).send().await {
366
+
Ok(response) => response.status().is_success() || response.status().is_redirection(),
367
+
Err(_) => false,
368
+
};
369
+
370
+
HttpResponse::Ok().json(serde_json::json!({
371
+
"valid": is_valid
372
+
}))
373
+
}
+11
src/templates.rs
+11
src/templates.rs
···
534
534
color: var(--text);
535
535
}}
536
536
537
+
.app-name.invalid-link {{
538
+
color: var(--text-light);
539
+
opacity: 0.5;
540
+
cursor: not-allowed;
541
+
}}
542
+
543
+
.app-name.invalid-link:hover {{
544
+
text-decoration: none;
545
+
color: var(--text-light);
546
+
}}
547
+
537
548
.detail-panel {{
538
549
position: fixed;
539
550
top: 0;
+16
-1
static/app.js
+16
-1
static/app.js
···
156
156
157
157
div.innerHTML = `
158
158
<div class="app-circle" data-namespace="${namespace}">${firstLetter}</div>
159
-
<a href="${url}" target="_blank" rel="noopener noreferrer" class="app-name">${displayName}</a>
159
+
<a href="${url}" target="_blank" rel="noopener noreferrer" class="app-name" data-url="${url}">${displayName}</a>
160
160
`;
161
161
162
162
// Try to fetch and display avatar
···
166
166
circle.innerHTML = `<img src="${avatarUrl}" class="app-logo" alt="${namespace}" />`;
167
167
}
168
168
});
169
+
170
+
// Validate URL
171
+
fetch(`/api/validate-url?url=${encodeURIComponent(url)}`)
172
+
.then(r => r.json())
173
+
.then(data => {
174
+
const link = div.querySelector('.app-name');
175
+
if (!data.valid) {
176
+
link.classList.add('invalid-link');
177
+
link.setAttribute('title', 'this domain is not reachable');
178
+
link.style.pointerEvents = 'none';
179
+
}
180
+
})
181
+
.catch(() => {
182
+
// Silently fail validation check
183
+
});
169
184
170
185
div.addEventListener('click', () => {
171
186
const detail = document.getElementById('detail');