feat: add Iconify-based icon system with search and picker UI

- Replace Font Awesome with Iconify (@iconify/json collections)
- Add IconPicker component with search, collection filtering, lazy loading
- Create authenticated icon API endpoints (search, collections, individual icons)
- Update projects page to render icons via Icon.svelte component
- Pre-cache common icon collections (Lucide, Simple Icons, etc.) on startup
This commit is contained in:
2026-01-06 16:01:09 -06:00
parent 6657d00c4e
commit eca50ef319
14 changed files with 840 additions and 23 deletions
+6 -6
View File
@@ -22,7 +22,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Some("Xevion/xevion.dev"),
None,
10,
Some("fa-globe"),
Some("lucide:globe"),
),
(
"contest",
@@ -32,7 +32,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Some("Xevion/contest"),
Some("https://contest.xevion.dev"),
9,
Some("fa-trophy"),
Some("lucide:trophy"),
),
(
"reforge",
@@ -42,7 +42,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Some("Xevion/reforge"),
None,
8,
Some("fa-file-code"),
Some("lucide:file-code"),
),
(
"algorithms",
@@ -52,7 +52,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Some("Xevion/algorithms"),
None,
5,
Some("fa-brain"),
Some("lucide:brain"),
),
(
"wordplay",
@@ -62,7 +62,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Some("Xevion/wordplay"),
Some("https://wordplay.example.com"),
7,
Some("fa-gamepad"),
Some("lucide:gamepad-2"),
),
(
"dotfiles",
@@ -72,7 +72,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Some("Xevion/dotfiles"),
None,
6,
Some("fa-terminal"),
Some("lucide:terminal"),
),
];
+53
View File
@@ -378,6 +378,8 @@ fn api_routes() -> Router<Arc<AppState>> {
"/tags/recalculate-cooccurrence",
axum::routing::post(recalculate_cooccurrence_handler),
)
// Icon API - proxy to SvelteKit (authentication handled by SvelteKit)
.route("/icons/{*path}", axum::routing::get(proxy_icons_handler))
.fallback(api_404_and_method_handler)
}
@@ -529,6 +531,57 @@ async fn projects_handler(State(state): State<Arc<AppState>>) -> impl IntoRespon
}
}
// Icon API handler - proxy to SvelteKit
async fn proxy_icons_handler(
State(state): State<Arc<AppState>>,
jar: axum_extra::extract::CookieJar,
axum::extract::Path(path): axum::extract::Path<String>,
req: Request,
) -> impl IntoResponse {
let full_path = format!("/api/icons/{}", path);
let query = req.uri().query().unwrap_or("");
let bun_url = if state.downstream_url.starts_with('/') || state.downstream_url.starts_with("./")
{
if query.is_empty() {
format!("http://localhost{}", full_path)
} else {
format!("http://localhost{}?{}", full_path, query)
}
} else if query.is_empty() {
format!("{}{}", state.downstream_url, full_path)
} else {
format!("{}{}?{}", state.downstream_url, full_path, query)
};
// Build trusted headers with session info
let mut forward_headers = HeaderMap::new();
if let Some(cookie) = jar.get("admin_session") {
if let Ok(session_id) = ulid::Ulid::from_string(cookie.value()) {
if let Some(session) = state.session_manager.validate_session(session_id) {
if let Ok(username_value) = axum::http::HeaderValue::from_str(&session.username) {
forward_headers.insert("x-session-user", username_value);
}
}
}
}
match proxy_to_bun(&bun_url, state, forward_headers).await {
Ok((status, headers, body)) => (status, headers, body).into_response(),
Err(err) => {
tracing::error!(error = %err, path = %full_path, "Failed to proxy icon request");
(
StatusCode::BAD_GATEWAY,
Json(serde_json::json!({
"error": "Failed to fetch icon data"
})),
)
.into_response()
}
}
}
// Tag API handlers
async fn list_tags_handler(State(state): State<Arc<AppState>>) -> impl IntoResponse {