diff --git a/src-tauri/easy-nostr b/src-tauri/easy-nostr index 7cebc49..1becf76 160000 --- a/src-tauri/easy-nostr +++ b/src-tauri/easy-nostr @@ -1 +1 @@ -Subproject commit 7cebc490cad3d809425ea114b4b7c05393d064e9 +Subproject commit 1becf76264c51958a3e618d61e4a8c0c319c9384 diff --git a/src-tauri/src/home.rs b/src-tauri/src/home.rs index e8d88b2..a769006 100644 --- a/src-tauri/src/home.rs +++ b/src-tauri/src/home.rs @@ -11,9 +11,9 @@ pub struct LocalPost { } #[tauri::command] -pub async fn fetch_nostr_posts() -> Result, String> { +pub async fn fetch_nostr_posts(hashtags: Vec) -> Result, String> { + println!("Fetching Nostr posts for hashtags: {:?}", hashtags); // 1. Temporären Einweg-Schlüssel generieren - // Das erzeugt ein Schlüsselpaar im RAM, das nach dem Funktionsaufruf verschwindet. let random_keys = Keys::generate(); let temp_nsec = random_keys .secret_key() @@ -34,8 +34,14 @@ pub async fn fetch_nostr_posts() -> Result, String> { .await .map_err(|e| e.to_string())?; - // 4. Posts von der Library holen - let raw_posts = easy.get_random_posts().await.map_err(|e| e.to_string())?; + // 4. Posts von der Library holen - Entweder per Hashtag oder Random + let raw_posts = if hashtags.is_empty() { + easy.get_random_posts().await.map_err(|e| e.to_string())? + } else { + easy.get_posts_by_hashtags(hashtags) + .await + .map_err(|e| e.to_string())? + }; // 5. Mappen: Library-Typ -> Unser serialisierbarer Typ let mapped_posts = raw_posts diff --git a/src/pages/home.rs b/src/pages/home.rs index d0fb393..7626c2e 100644 --- a/src/pages/home.rs +++ b/src/pages/home.rs @@ -7,8 +7,8 @@ use yew::prelude::*; // Tauri 'invoke' Funktion deklarieren #[wasm_bindgen] extern "C" { - #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])] - async fn invoke(cmd: &str, args: JsValue) -> JsValue; + #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], catch)] + async fn invoke(cmd: &str, args: JsValue) -> Result; } // Wir definieren Post lokal, damit Yew weiß, wie die JSON-Daten vom Backend aussehen @@ -24,13 +24,17 @@ pub struct Post { pub fn home() -> Html { let posts = use_state(|| None::>); let error = use_state(|| false); + let hashtag_input = use_state(|| String::new()); + let active_tags = use_state(Vec::::new); let load_posts = { let posts = posts.clone(); let error = error.clone(); + let active_tags = active_tags.clone(); Callback::from(move |_e: MouseEvent| { let posts = posts.clone(); let error = error.clone(); + let tags = (*active_tags).clone(); // UI zurücksetzen & Scrollen posts.set(None); @@ -43,14 +47,25 @@ pub fn home() -> Html { } spawn_local(async move { - // Daten vom Rust-Backend anfordern - let res = invoke("fetch_nostr_posts", JsValue::NULL).await; + #[derive(Serialize)] + struct HashtagArgs { + hashtags: Vec, + } + let args = serde_wasm_bindgen::to_value(&HashtagArgs { hashtags: tags }).unwrap(); - // JSON-Resultat in Vec umwandeln - if let Ok(new_posts) = serde_wasm_bindgen::from_value::>(res) { - posts.set(Some(new_posts)); - } else { - error.set(true); + // Daten vom Rust-Backend anfordern + match invoke("fetch_nostr_posts", args).await { + Ok(res) => { + // JSON-Resultat in Vec umwandeln + if let Ok(new_posts) = serde_wasm_bindgen::from_value::>(res) { + posts.set(Some(new_posts)); + } else { + error.set(true); + } + } + Err(_) => { + error.set(true); + } } }); }) @@ -68,10 +83,73 @@ pub fn home() -> Html { html! {
-

{"✨ Entdecken"}

- +
+

{"✨ Entdecken"}

+ +
+
+ +
+
+ +
+ if !active_tags.is_empty() { +
+ { for active_tags.iter().map(|t| { + let t_clone = t.clone(); + let active_tags = active_tags.clone(); + let load_posts = load_posts.clone(); + let remove = move |_| { + let mut tags = (*active_tags).clone(); + tags.retain(|x| x != &t_clone); + active_tags.set(tags); + load_posts.emit(MouseEvent::new("click").unwrap()); + }; + html! { + + { format!("#{}", t) } + {"×"} + + } + }) } +
+ }
{ diff --git a/styles.css b/styles.css index 2bd6a7e..04a1bea 100644 --- a/styles.css +++ b/styles.css @@ -10,13 +10,15 @@ --text-primary: #e2e2e7; --text-secondary: #a0a0b0; --accent-color: #4a90e2; - --nav-bg: rgba(30, 30, 46, 0.95); /* Fallback for blur */ + --nav-bg: rgba(30, 30, 46, 0.95); + /* Fallback for blur */ --danger: #ef4444; } * { box-sizing: border-box; - -webkit-tap-highlight-color: transparent; /* UX-Finishing: No blue flash */ + -webkit-tap-highlight-color: transparent; + /* UX-Finishing: No blue flash */ } /* --- Global & Layout --- */ @@ -35,7 +37,8 @@ body { display: flex; justify-content: center; min-height: 100vh; - overflow-x: hidden; /* Performance: Prevent horizontal scroll */ + overflow-x: hidden; + /* Performance: Prevent horizontal scroll */ -webkit-tap-highlight-color: transparent; } @@ -48,8 +51,7 @@ html { width: 100%; max-width: 600px; /* Safe Areas: Top inset and mobile-friendly padding */ - padding: calc(20px + env(safe-area-inset-top)) 16px - calc(120px + env(safe-area-inset-bottom)) 16px; + padding: calc(20px + env(safe-area-inset-top)) 16px calc(120px + env(safe-area-inset-bottom)) 16px; } @media (min-width: 768px) { @@ -93,7 +95,8 @@ html { transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); /* Touch-Ergonomie: Min 44px effective height */ padding: 10px 12px; - user-select: none; /* UX-Finishing */ + user-select: none; + /* UX-Finishing */ display: flex; align-items: center; justify-content: center; @@ -358,10 +361,12 @@ html { /* --- Animations --- */ @keyframes float { + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-15px); } @@ -372,6 +377,7 @@ html { opacity: 0; transform: translateY(-10px); } + to { opacity: 1; transform: translateY(0); @@ -381,11 +387,9 @@ html { /* --- Utilities --- */ .reload-btn-large { cursor: pointer; - background: linear-gradient( - 135deg, - rgba(74, 144, 226, 0.15), - rgba(74, 144, 226, 0.05) - ); + background: linear-gradient(135deg, + rgba(74, 144, 226, 0.15), + rgba(74, 144, 226, 0.05)); color: var(--accent-color); padding: 16px; border-radius: 16px; @@ -418,9 +422,11 @@ html { 0% { box-shadow: 0 0 0 0 rgba(74, 144, 226, 0.4); } + 70% { box-shadow: 0 0 0 10px rgba(74, 144, 226, 0); } + 100% { box-shadow: 0 0 0 0 rgba(74, 144, 226, 0); } @@ -513,9 +519,74 @@ html { opacity: 0.8; } +/* --- Hashtag Section --- */ +.hashtag-section { + margin-bottom: 24px; +} + +.hashtag-input-row { + display: flex; + gap: 10px; +} + +.hashtag-input { + flex: 1; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + padding: 12px 16px; + color: white; + font-size: 0.95rem; + outline: none; + transition: all 0.2s; +} + +.hashtag-input:focus { + border-color: var(--accent-color); + background: rgba(255, 255, 255, 0.08); +} + +.tag-chips { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 12px; +} + +.tag-chip { + background: rgba(74, 144, 226, 0.15); + color: var(--accent-color); + border: 1px solid rgba(74, 144, 226, 0.3); + padding: 4px 12px; + border-radius: 20px; + font-size: 0.85rem; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + gap: 6px; + transition: all 0.2s; +} + +.tag-chip:hover { + background: rgba(74, 144, 226, 0.25); + transform: scale(1.05); +} + +.tag-remove { + font-size: 1.1rem; + line-height: 1; + opacity: 0.6; +} + +.tag-chip:hover .tag-remove { + opacity: 1; +} + /* --- Layout Adjustment für Provider Switch --- */ .header-main { display: flex; align-items: center; - gap: 12px; -} + justify-content: space-between; + width: 100%; +} \ No newline at end of file