Compare commits
1 Commits
feature/br
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e1614b96d |
Submodule src-tauri/easy-nostr updated: 7cebc490ca...1becf76264
@@ -11,9 +11,9 @@ pub struct LocalPost {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fetch_nostr_posts() -> Result<Vec<LocalPost>, String> {
|
||||
pub async fn fetch_nostr_posts(hashtags: Vec<String>) -> Result<Vec<LocalPost>, 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<Vec<LocalPost>, 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
|
||||
|
||||
@@ -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<JsValue, JsValue>;
|
||||
}
|
||||
|
||||
// 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::<Vec<Post>>);
|
||||
let error = use_state(|| false);
|
||||
let hashtag_input = use_state(|| String::new());
|
||||
let active_tags = use_state(Vec::<String>::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<String>,
|
||||
}
|
||||
let args = serde_wasm_bindgen::to_value(&HashtagArgs { hashtags: tags }).unwrap();
|
||||
|
||||
// JSON-Resultat in Vec<Post> umwandeln
|
||||
if let Ok(new_posts) = serde_wasm_bindgen::from_value::<Vec<Post>>(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<Post> umwandeln
|
||||
if let Ok(new_posts) = serde_wasm_bindgen::from_value::<Vec<Post>>(res) {
|
||||
posts.set(Some(new_posts));
|
||||
} else {
|
||||
error.set(true);
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
error.set(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
@@ -68,10 +83,73 @@ pub fn home() -> Html {
|
||||
html! {
|
||||
<div class="feed-container">
|
||||
<div class="feed-header">
|
||||
<h1 class="feed-title">{"✨ Entdecken"}</h1>
|
||||
<button class="reload-btn" onclick={load_posts.clone()}>
|
||||
{"🔄"}
|
||||
</button>
|
||||
<div class="header-main">
|
||||
<h1 class="feed-title">{"✨ Entdecken"}</h1>
|
||||
<button class="reload-btn" onclick={load_posts.clone()}>
|
||||
{"🔄"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hashtag-section">
|
||||
<div class="hashtag-input-row">
|
||||
<input
|
||||
type="text"
|
||||
class="hashtag-input"
|
||||
placeholder="Tags hinzufügen (z.B. bitcoin, rust)..."
|
||||
value={(*hashtag_input).clone()}
|
||||
oninput={
|
||||
let hashtag_input = hashtag_input.clone();
|
||||
move |e: InputEvent| {
|
||||
let target: web_sys::HtmlInputElement = e.target_unchecked_into();
|
||||
hashtag_input.set(target.value());
|
||||
}
|
||||
}
|
||||
onkeydown={
|
||||
let hashtag_input = hashtag_input.clone();
|
||||
let active_tags = active_tags.clone();
|
||||
let load_posts = load_posts.clone();
|
||||
move |e: KeyboardEvent| {
|
||||
if e.key() == "Enter" {
|
||||
let val = (*hashtag_input).trim().to_string();
|
||||
if !val.is_empty() {
|
||||
let mut tags = (*active_tags).clone();
|
||||
// Mehrere Tags per Komma trennen
|
||||
for t in val.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) {
|
||||
if !tags.contains(&t.to_string()) {
|
||||
tags.push(t.to_string());
|
||||
}
|
||||
}
|
||||
active_tags.set(tags);
|
||||
hashtag_input.set(String::new());
|
||||
load_posts.emit(MouseEvent::new("click").unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
if !active_tags.is_empty() {
|
||||
<div class="tag-chips">
|
||||
{ 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! {
|
||||
<span class="tag-chip" onclick={remove}>
|
||||
{ format!("#{}", t) }
|
||||
<span class="tag-remove">{"×"}</span>
|
||||
</span>
|
||||
}
|
||||
}) }
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
|
||||
97
styles.css
97
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%;
|
||||
}
|
||||
Reference in New Issue
Block a user