Files
tavolo/web/src/components/copy.rs
T
woggioni b5219062f2
CI / Build and push docker image (push) Successful in 3m0s
Let anyone with the game URL watch as a read-only spectator
Anonymous users and logged-in non-players can now open /game/<id> and
follow the match: they see the table, the score and each player's
captured cards and scope, but never a hand and never the join code.
Spectator actions are rejected with a forbidden error.

Backend (tavolo-platform / tavolo-scopone):
- /ws/games/{id} accepts anonymous and non-seated connections as
  spectators; only unknown games are refused. The state payload carries
  spectator: true and withholds join_code unless the viewer is seated.
- GET /api/games/{id} is public and returns the same personalized
  (player) or read-only (spectator) view.
- GameEngine.view_for/game_over_view take Optional[str] (None = spectator)
  and are documented to hide privileged information for non-seated
  viewers.

Frontend (Sycamore):
- Parse the spectator flag and render a spectator table: card backs plus
  captured/scope for every seat, no clickable hand, "Spectating" badge.
- A pre-start spectator sees a "game not started yet" waiting room with
  the watch link and no join code.
- "Copy watch link" buttons in the waiting room and the game HUD.
- The hand-end summary carries no acknowledge button for spectators and
  the game-over overlay hides "My matches".

Tests and docs updated.
2026-10-06 12:51:53 +08:00

46 lines
1.5 KiB
Rust

//! Clipboard copy button, shared by the join-code and watch-link actions.
use std::cell::RefCell;
use std::rc::Rc;
use sycamore::prelude::*;
/// A button that copies `text` to the clipboard and shows "Copied!" for a
/// moment. `label` is the idle caption.
#[component(inline_props)]
pub fn CopyButton(text: String, label: String) -> View {
let copied = create_signal(false);
// Held so the reset timer is cancelled if the button is rebuilt (e.g.
// another player joins) while the "Copied!" feedback is showing:
// firing it afterwards would set a disposed signal and panic.
let reset_timer: Rc<RefCell<Option<gloo_timers::callback::Timeout>>> =
Rc::new(RefCell::new(None));
on_cleanup({
let reset_timer = reset_timer.clone();
move || {
reset_timer.borrow_mut().take();
}
});
let on_click = {
let text = text.clone();
move |_| {
if let Some(win) = web_sys::window() {
let _ = win.navigator().clipboard().write_text(&text);
}
copied.set(true);
*reset_timer.borrow_mut() = Some(gloo_timers::callback::Timeout::new(
1500,
move || {
copied.set(false);
},
));
}
};
view! {
button(class="btn btn-outline-light", on:click=on_click) {
(move || if copied.get() { "Copied!".to_string() } else { label.clone() })
}
}
}