CI / Build and push docker image (push) Successful in 3m0s
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.
46 lines
1.5 KiB
Rust
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() })
|
|
}
|
|
}
|
|
}
|