error enum for errorbox instead of String + setup
This commit is contained in:
parent
d3d2819e12
commit
9bba472917
|
|
@ -4061,6 +4061,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"sorted-vec",
|
"sorted-vec",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
"thiserror 2.0.17",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
.icon-fit {
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
filter: contrast(120%) brightness(120%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-shrink {
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
|
@ -5,17 +5,30 @@ $host_nav_top_pad: 10px;
|
||||||
$host_nav_bottom_pad: 10px;
|
$host_nav_bottom_pad: 10px;
|
||||||
$host_nav_total_height: $host_nav_height + $host_nav_top_pad + $host_nav_bottom_pad;
|
$host_nav_total_height: $host_nav_height + $host_nav_top_pad + $host_nav_bottom_pad;
|
||||||
|
|
||||||
$wolves_color: rgba(255, 0, 0, 0.7);
|
$red1: #f62b5a;
|
||||||
$village_color: rgba(0, 0, 255, 0.7);
|
$red2: #ff4b50;
|
||||||
|
$green1: #47b413;
|
||||||
|
$green2: #36d450;
|
||||||
|
$yellow1: #e3c401;
|
||||||
|
$yellow2: #e9e836;
|
||||||
|
$blue1: #24acd4;
|
||||||
|
$blue2: #5dc5f8;
|
||||||
|
$magenta1: #f2affd;
|
||||||
|
$magenta2: #feabf2;
|
||||||
|
$cyan1: #13c299;
|
||||||
|
$cyan2: #24dfc4;
|
||||||
|
|
||||||
|
$wolves_color: oklch(0.4836 0.1982 28.87);
|
||||||
|
$village_color: oklch(0.3817 0.261 264.95);
|
||||||
$village_border: color.change($village_color, $alpha: 1.0);
|
$village_border: color.change($village_color, $alpha: 1.0);
|
||||||
$wolves_border: color.change($wolves_color, $alpha: 1.0);
|
$wolves_border: color.change($wolves_color, $alpha: 1.0);
|
||||||
$intel_color: color.adjust($village_color, $hue: -30deg);
|
$intel_color: oklch(0.4556 0.109276 242.5749);
|
||||||
$intel_border: color.change($intel_color, $alpha: 1.0);
|
$intel_border: color.change($intel_color, $alpha: 1.0);
|
||||||
$defensive_color: rgba(0, 128, 32, 0.9); //color.adjust(rgba(0, 16, 128, 0.9), $hue: -60deg);
|
$defensive_color: oklch(0.4509 0.077867 189.9496);
|
||||||
$defensive_border: color.change($defensive_color, $alpha: 1.0);
|
$defensive_border: color.change($defensive_color, $alpha: 1.0);
|
||||||
$offensive_color: color.adjust($village_color, $hue: 30deg);
|
$offensive_color: oklch(0.4342 0.2309 298.01);
|
||||||
$offensive_border: color.change($offensive_color, $alpha: 1.0);
|
$offensive_border: color.change($offensive_color, $alpha: 1.0);
|
||||||
$starts_as_villager_color: color.adjust($village_color, $hue: 60deg);
|
$starts_as_villager_color: oklch(0.4864 0.2059 346.12);
|
||||||
$starts_as_villager_border: color.change($starts_as_villager_color, $alpha: 1.0);
|
$starts_as_villager_border: color.change($starts_as_villager_color, $alpha: 1.0);
|
||||||
$damned_color: color.adjust($village_color, $hue: 45deg);
|
$damned_color: color.adjust($village_color, $hue: 45deg);
|
||||||
$damned_border: color.change($damned_color, $alpha: 1.0);
|
$damned_border: color.change($damned_color, $alpha: 1.0);
|
||||||
|
|
@ -41,6 +54,8 @@ $damned_color_faint: color.change($damned_color, $alpha: 0.1);
|
||||||
$drunk_color_faint: color.change($drunk_color, $alpha: 0.1);
|
$drunk_color_faint: color.change($drunk_color, $alpha: 0.1);
|
||||||
|
|
||||||
@import 'faction';
|
@import 'faction';
|
||||||
|
@import 'setup';
|
||||||
|
@import 'icon';
|
||||||
|
|
||||||
@mixin flexbox() {
|
@mixin flexbox() {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
|
|
@ -57,6 +72,16 @@ body {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.big-screen-wrapper {
|
||||||
|
margin: 0;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
.error_container {
|
.error_container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 3vh;
|
top: 3vh;
|
||||||
|
|
@ -861,3 +886,44 @@ form {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.category {
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&.add-list {
|
||||||
|
@media only screen and (max-width : 599px) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width : 600px) {
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
width: auto;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1px;
|
||||||
|
|
||||||
|
|
||||||
|
&>.title {
|
||||||
|
font-size: 0.5em !important;
|
||||||
|
margin-bottom: 0px !important;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-list {
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .title {
|
||||||
|
text-shadow: black 1px 1px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
.setup-screen {
|
||||||
|
.inactive {
|
||||||
|
filter: brightness(0%);
|
||||||
|
}
|
||||||
|
|
||||||
|
margin: 2%;
|
||||||
|
font-size: 1.5em;
|
||||||
|
|
||||||
|
.setup {
|
||||||
|
display: grid;
|
||||||
|
grid: auto-flow / 1fr 1fr 1fr;
|
||||||
|
gap: 5vw;
|
||||||
|
row-gap: 2ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-category {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 0.25ch;
|
||||||
|
text-align: left;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
|
||||||
|
.title {
|
||||||
|
padding: 0.1ch;
|
||||||
|
text-align: center;
|
||||||
|
text-shadow: black 1px 1px;
|
||||||
|
width: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-bottom: 0.25ch;
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
padding: 0 0.5ch 0 0.5ch;
|
||||||
|
|
||||||
|
&.invisible {
|
||||||
|
opacity: 0%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.attributes {
|
||||||
|
margin-left: 10px;
|
||||||
|
align-self: flex-end;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role {
|
||||||
|
flex-grow: 1;
|
||||||
|
text-shadow: black 1px 1px;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
filter: saturate(40%);
|
||||||
|
padding: 0.25ch 0 0.25ch 1ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wakes {
|
||||||
|
$wakes_color: oklch(0.9195 0.1839 109.60356514768961);
|
||||||
|
border: 2px solid $wakes_color;
|
||||||
|
box-shadow: 0 0 3px $wakes_color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrcode {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
z-index: 100;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
margin: 5vw;
|
||||||
|
width: 90vw;
|
||||||
|
height: 90vh;
|
||||||
|
gap: 1cm;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 70%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
font-size: 5vw;
|
||||||
|
border: 1px solid $village_border;
|
||||||
|
background-color: color.change($village_color, $alpha: 0.3);
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&>* {
|
||||||
|
margin-top: 0.5cm;
|
||||||
|
margin-bottom: 0.5cm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -125,10 +125,10 @@ macro_rules! id_impl {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl core::str::FromStr for $name {
|
impl core::str::FromStr for $name {
|
||||||
type Err = uuid::Error;
|
type Err = ::uuid::Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> core::result::Result<Self, Self::Err> {
|
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||||
Ok(Self(uuid::Uuid::from_str(s)?))
|
uuid::Uuid::parse_str(s).map(Self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ werewolves-macros.workspace = true
|
||||||
werewolves-proto.workspace = true
|
werewolves-proto.workspace = true
|
||||||
codee.workspace = true
|
codee.workspace = true
|
||||||
convert_case.workspace = true
|
convert_case.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
sorted-vec.workspace = true
|
sorted-vec.workspace = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,10 @@ pub mod components {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod class;
|
pub mod class;
|
||||||
|
pub mod error;
|
||||||
pub mod storage;
|
pub mod storage;
|
||||||
|
|
||||||
use codee::string::JsonSerdeCodec;
|
use codee::string::JsonSerdeCodec;
|
||||||
use gloo::storage::Storage;
|
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos_meta::{Link, MetaTags, Stylesheet, Title, provide_meta_context};
|
use leptos_meta::{Link, MetaTags, Stylesheet, Title, provide_meta_context};
|
||||||
use leptos_router::{
|
use leptos_router::{
|
||||||
|
|
@ -27,7 +27,8 @@ use serde::{Deserialize, Serialize};
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{
|
app::{
|
||||||
components::{ErrorBox, Nav},
|
components::{ErrorBox, Nav},
|
||||||
pages::{GamePage, Main, NotFound, Signin, Signup, UserSettings},
|
error::WolfError,
|
||||||
|
pages::{GamePage, Main, NotFound, Signin, Signup, UserSettings, big::BigScreen},
|
||||||
storage::{
|
storage::{
|
||||||
Stored,
|
Stored,
|
||||||
user::{AuthContext, AuthContextStoreFields},
|
user::{AuthContext, AuthContextStoreFields},
|
||||||
|
|
@ -96,7 +97,7 @@ pub fn App() -> impl IntoView {
|
||||||
.then_some(auth_store.session().get().is_some())
|
.then_some(auth_store.session().get().is_some())
|
||||||
};
|
};
|
||||||
let not_logged_in = move || Some(auth_store.session().get().is_none());
|
let not_logged_in = move || Some(auth_store.session().get().is_none());
|
||||||
let error = RwSignal::new(None);
|
let error: RwSignal<Option<WolfError>> = RwSignal::new(None);
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<Stylesheet id="leptos" href="/pkg/werewolves.css" />
|
<Stylesheet id="leptos" href="/pkg/werewolves.css" />
|
||||||
|
|
@ -109,22 +110,25 @@ pub fn App() -> impl IntoView {
|
||||||
<Nav />
|
<Nav />
|
||||||
<ErrorBox msg=error />
|
<ErrorBox msg=error />
|
||||||
<Routes fallback=NotFound>
|
<Routes fallback=NotFound>
|
||||||
<Route path=path!("/") view=Main />
|
<Route
|
||||||
|
path=path!("/")
|
||||||
|
view=move || view! { <Main error=error.write_only() /> }
|
||||||
|
/>
|
||||||
<ProtectedRoute
|
<ProtectedRoute
|
||||||
path=path!("/signin")
|
path=path!("/signin")
|
||||||
view=|| view! { <Signin /> }
|
view=move || view! { <Signin error=error.write_only() /> }
|
||||||
condition=not_logged_in
|
condition=not_logged_in
|
||||||
redirect_path=|| "/"
|
redirect_path=|| "/"
|
||||||
/>
|
/>
|
||||||
<ProtectedRoute
|
<ProtectedRoute
|
||||||
path=path!("/signup")
|
path=path!("/signup")
|
||||||
view=|| view! { <Signup /> }
|
view=move || view! { <Signup error=error.write_only() /> }
|
||||||
condition=not_logged_in
|
condition=not_logged_in
|
||||||
redirect_path=|| "/"
|
redirect_path=|| "/"
|
||||||
/>
|
/>
|
||||||
<ProtectedRoute
|
<ProtectedRoute
|
||||||
path=path!("/user/settings")
|
path=path!("/user/settings")
|
||||||
view=UserSettings
|
view=move || view! { <UserSettings error=error.write_only() /> }
|
||||||
condition=is_logged_in
|
condition=is_logged_in
|
||||||
redirect_path=|| "/"
|
redirect_path=|| "/"
|
||||||
/>
|
/>
|
||||||
|
|
@ -132,6 +136,7 @@ pub fn App() -> impl IntoView {
|
||||||
path=path!("/games/:id")
|
path=path!("/games/:id")
|
||||||
view=move || view! { <GamePage error=error.write_only() /> }
|
view=move || view! { <GamePage error=error.write_only() /> }
|
||||||
/>
|
/>
|
||||||
|
<Route path=path!("/games/:id/big") view=BigScreen />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use core::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
aura::AuraTitle, character::Character, game::Category, role::RoleTitle, team::Team,
|
aura::AuraTitle, character::Character, game::Category, role::RoleTitle, team::Team,
|
||||||
};
|
};
|
||||||
|
|
@ -70,8 +72,27 @@ impl Class for Category {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
pub struct Classes(Vec<String>);
|
pub struct Classes(Vec<String>);
|
||||||
|
impl Classes {
|
||||||
|
pub fn push_opt(&mut self, opt: Option<String>) {
|
||||||
|
if let Some(val) = opt {
|
||||||
|
self.0.push(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Deref for Classes {
|
||||||
|
type Target = Vec<String>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl DerefMut for Classes {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
impl<I> From<I> for Classes
|
impl<I> From<I> for Classes
|
||||||
where
|
where
|
||||||
I: Into<Vec<String>>,
|
I: Into<Vec<String>>,
|
||||||
|
|
@ -96,6 +117,19 @@ impl AsClasses for [&str] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> AsClasses for [Option<T>]
|
||||||
|
where
|
||||||
|
T: ToString,
|
||||||
|
{
|
||||||
|
fn as_classes(&self) -> Classes {
|
||||||
|
Classes(
|
||||||
|
self.iter()
|
||||||
|
.filter_map(|c| c.as_ref().map(|c| c.to_string()))
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl leptos::tachys::html::class::IntoClass for Classes {
|
impl leptos::tachys::html::class::IntoClass for Classes {
|
||||||
type AsyncOutput = Self;
|
type AsyncOutput = Self;
|
||||||
type State = (leptos::tachys::renderer::types::Element, Self);
|
type State = (leptos::tachys::renderer::types::Element, Self);
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,10 @@ use leptos_use::{
|
||||||
UseDraggableOptions, UseDraggableReturn, core::Position, use_draggable_with_options,
|
UseDraggableOptions, UseDraggableReturn, core::Position, use_draggable_with_options,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub trait ViewError: core::error::Error {
|
use crate::app::error::WolfError;
|
||||||
fn view(&self) -> impl IntoView;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ErrorBox(msg: RwSignal<Option<String>>) -> impl IntoView {
|
pub fn ErrorBox(msg: RwSignal<Option<WolfError>>) -> impl IntoView {
|
||||||
let el = NodeRef::<Div>::new();
|
let el = NodeRef::<Div>::new();
|
||||||
|
|
||||||
// `style` is a helper string "left: {x}px; top: {y}px;"
|
// `style` is a helper string "left: {x}px; top: {y}px;"
|
||||||
|
|
@ -17,12 +15,12 @@ pub fn ErrorBox(msg: RwSignal<Option<String>>) -> impl IntoView {
|
||||||
UseDraggableOptions::default().initial_value(Position { x: 40.0, y: 40.0 }),
|
UseDraggableOptions::default().initial_value(Position { x: 40.0, y: 40.0 }),
|
||||||
);
|
);
|
||||||
let content = move || {
|
let content = move || {
|
||||||
msg.get().map(|text| {
|
msg.get().map(|err| {
|
||||||
view! {
|
view! {
|
||||||
<div class="error_container" hidden=move || msg.get().is_none()>
|
<div class="error_container" hidden=move || msg.get().is_none()>
|
||||||
<div node_ref=el style=move || style.get() class="error">
|
<div node_ref=el style=move || style.get() class="error">
|
||||||
<h5>"error"</h5>
|
<h5>"error"</h5>
|
||||||
<p>{text.to_string()}</p>
|
<p>{err.to_string()}</p>
|
||||||
<button on:click=move |ev| {
|
<button on:click=move |ev| {
|
||||||
ev.prevent_default();
|
ev.prevent_default();
|
||||||
msg.set(None);
|
msg.set(None);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,227 @@
|
||||||
|
// Copyright (C) 2025-2026 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use werewolves_proto::{
|
||||||
|
aura::AuraTitle,
|
||||||
|
diedto::DiedToTitle,
|
||||||
|
role::{Alignment, Killer, Powerful, RoleTitle},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::app::class::{Class, Classes, PartialClass};
|
||||||
|
|
||||||
|
macro_rules! decl_icon {
|
||||||
|
($($name:ident: $path:literal,)*) => {
|
||||||
|
#[allow(unused)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum IconSource {
|
||||||
|
$(
|
||||||
|
$name,
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IconSource {
|
||||||
|
pub const fn source(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
$(
|
||||||
|
Self::$name => $path,
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
decl_icon!(
|
||||||
|
Village: "/img/village.svg",
|
||||||
|
Wolves: "/img/wolf.svg",
|
||||||
|
Killer: "/img/killer.svg",
|
||||||
|
Powerful: "/img/powerful.svg",
|
||||||
|
ListItem: "/img/li.svg",
|
||||||
|
Skull: "/img/skull.svg",
|
||||||
|
Heart: "/img/heart.svg",
|
||||||
|
Shield: "/img/shield.svg",
|
||||||
|
ShieldAndSword: "/img/shield-and-sword.svg",
|
||||||
|
Seer: "/img/seer.svg",
|
||||||
|
Hunter: "/img/hunter.svg",
|
||||||
|
MapleWolf: "/img/maple-wolf.svg",
|
||||||
|
Gravedigger: "/img/gravedigger.svg",
|
||||||
|
PowerSeer: "/img/power-seer.svg",
|
||||||
|
Scapegoat: "/img/scapegoat.svg",
|
||||||
|
Diseased: "/img/diseased.svg",
|
||||||
|
Mortician: "/img/mortician.svg",
|
||||||
|
Pyremaster: "/img/pyremaster.svg",
|
||||||
|
Sword: "/img/sword.svg",
|
||||||
|
Roleblock: "/img/roleblock.svg",
|
||||||
|
Beholder: "/img/beholder.svg",
|
||||||
|
LoneWolf: "/img/lone-wolf.svg",
|
||||||
|
Vindicator: "/img/vindicator.svg",
|
||||||
|
Apprentice: "/img/apprentice.svg",
|
||||||
|
Elder: "/img/elder.svg",
|
||||||
|
Shapeshifter: "/img/shapeshifter.svg",
|
||||||
|
Arcanist: "/img/arcanist.svg",
|
||||||
|
Adjudicator: "/img/adjudicator.svg",
|
||||||
|
Weightlifter: "/img/weightlifter.svg",
|
||||||
|
Insomniac: "/img/insomniac.svg",
|
||||||
|
BlackKnight: "/img/black-knight.svg",
|
||||||
|
Mason: "/img/mason.svg",
|
||||||
|
NotEqual: "/img/not-equal.svg",
|
||||||
|
Equal: "/img/equal.svg",
|
||||||
|
RedX: "/img/red-x.svg",
|
||||||
|
Damned: "/img/damned.svg",
|
||||||
|
Bloodlet: "/img/bloodlet.svg",
|
||||||
|
Drunk: "/img/drunk.svg",
|
||||||
|
Insane: "/img/insane.svg",
|
||||||
|
);
|
||||||
|
|
||||||
|
impl PartialClass for IconSource {
|
||||||
|
fn partial_class(&self) -> Option<&'static str> {
|
||||||
|
match self {
|
||||||
|
IconSource::Killer => Some("killer"),
|
||||||
|
IconSource::Powerful => Some("powerful"),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||||
|
pub enum IconType {
|
||||||
|
Small,
|
||||||
|
#[default]
|
||||||
|
Fit,
|
||||||
|
Shrink,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Class for IconType {
|
||||||
|
fn class(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
IconType::Fit => "icon-fit",
|
||||||
|
IconType::Small => "icon",
|
||||||
|
IconType::Shrink => "icon-shrink",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Icon(
|
||||||
|
source: IconSource,
|
||||||
|
#[prop(optional)] r#type: IconType,
|
||||||
|
#[prop(optional)] mut classes: Classes,
|
||||||
|
) -> impl IntoView {
|
||||||
|
classes.push_opt(source.partial_class().map(ToString::to_string));
|
||||||
|
classes.push(r#type.class().to_string());
|
||||||
|
view! { <img src=source.source() class=classes.to_string() /> }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait PartialAssociatedIcon {
|
||||||
|
fn icon(&self) -> Option<IconSource>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait AssociatedIcon {
|
||||||
|
fn icon(&self) -> IconSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssociatedIcon for Alignment {
|
||||||
|
fn icon(&self) -> IconSource {
|
||||||
|
match self {
|
||||||
|
Alignment::Village => IconSource::Village,
|
||||||
|
Alignment::Wolves => IconSource::Wolves,
|
||||||
|
Alignment::Damned => IconSource::Damned,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssociatedIcon for Killer {
|
||||||
|
fn icon(&self) -> IconSource {
|
||||||
|
IconSource::Killer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssociatedIcon for Powerful {
|
||||||
|
fn icon(&self) -> IconSource {
|
||||||
|
IconSource::Powerful
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialAssociatedIcon for RoleTitle {
|
||||||
|
fn icon(&self) -> Option<IconSource> {
|
||||||
|
Some(match self {
|
||||||
|
RoleTitle::AlphaWolf | RoleTitle::DireWolf => return None,
|
||||||
|
|
||||||
|
RoleTitle::Bloodletter => IconSource::Bloodlet,
|
||||||
|
RoleTitle::MasonLeader => IconSource::Mason,
|
||||||
|
RoleTitle::BlackKnight => IconSource::BlackKnight,
|
||||||
|
RoleTitle::Insomniac => IconSource::Insomniac,
|
||||||
|
RoleTitle::Weightlifter => IconSource::Weightlifter,
|
||||||
|
RoleTitle::Adjudicator => IconSource::Adjudicator,
|
||||||
|
RoleTitle::Arcanist => IconSource::Arcanist,
|
||||||
|
RoleTitle::Shapeshifter => IconSource::Shapeshifter,
|
||||||
|
RoleTitle::Elder => IconSource::Elder,
|
||||||
|
RoleTitle::Apprentice => IconSource::Apprentice,
|
||||||
|
RoleTitle::LoneWolf => IconSource::LoneWolf,
|
||||||
|
RoleTitle::Villager => IconSource::Village,
|
||||||
|
RoleTitle::Beholder => IconSource::Beholder,
|
||||||
|
RoleTitle::Werewolf => IconSource::Wolves,
|
||||||
|
RoleTitle::Militia => IconSource::Sword,
|
||||||
|
RoleTitle::PyreMaster => IconSource::Pyremaster,
|
||||||
|
RoleTitle::Mortician => IconSource::Mortician,
|
||||||
|
RoleTitle::Diseased => IconSource::Diseased,
|
||||||
|
RoleTitle::Scapegoat => IconSource::Scapegoat,
|
||||||
|
RoleTitle::PowerSeer => IconSource::PowerSeer,
|
||||||
|
RoleTitle::Gravedigger => IconSource::Gravedigger,
|
||||||
|
RoleTitle::MapleWolf => IconSource::MapleWolf,
|
||||||
|
RoleTitle::Hunter => IconSource::Hunter,
|
||||||
|
RoleTitle::Empath => IconSource::Heart,
|
||||||
|
RoleTitle::Seer => IconSource::Seer,
|
||||||
|
RoleTitle::Guardian => IconSource::ShieldAndSword,
|
||||||
|
RoleTitle::Protector => IconSource::Shield,
|
||||||
|
RoleTitle::Vindicator => IconSource::Vindicator,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialAssociatedIcon for DiedToTitle {
|
||||||
|
fn icon(&self) -> Option<IconSource> {
|
||||||
|
match self {
|
||||||
|
DiedToTitle::Execution => Some(IconSource::Skull),
|
||||||
|
DiedToTitle::MapleWolf | DiedToTitle::MapleWolfStarved => Some(IconSource::MapleWolf),
|
||||||
|
DiedToTitle::Militia => Some(IconSource::Sword),
|
||||||
|
DiedToTitle::Wolfpack => None,
|
||||||
|
DiedToTitle::AlphaWolf => None,
|
||||||
|
DiedToTitle::Shapeshift => None,
|
||||||
|
DiedToTitle::Hunter => Some(IconSource::Hunter),
|
||||||
|
DiedToTitle::GuardianProtecting => Some(IconSource::ShieldAndSword),
|
||||||
|
DiedToTitle::PyreMaster => Some(IconSource::Pyremaster),
|
||||||
|
DiedToTitle::PyreMasterLynchMob => None,
|
||||||
|
DiedToTitle::MasonLeaderRecruitFail => None,
|
||||||
|
DiedToTitle::LoneWolf => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialAssociatedIcon for AuraTitle {
|
||||||
|
fn icon(&self) -> Option<IconSource> {
|
||||||
|
match self {
|
||||||
|
AuraTitle::Damned => Some(IconSource::Damned),
|
||||||
|
AuraTitle::Drunk => Some(IconSource::Drunk),
|
||||||
|
AuraTitle::Insane => Some(IconSource::Insane),
|
||||||
|
AuraTitle::Bloodlet => Some(IconSource::Bloodlet),
|
||||||
|
AuraTitle::Scapegoat
|
||||||
|
| AuraTitle::RedeemableScapegoat
|
||||||
|
| AuraTitle::VindictiveScapegoat
|
||||||
|
| AuraTitle::SpitefulScapegoat
|
||||||
|
| AuraTitle::InevitableScapegoat => Some(IconSource::Scapegoat),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,18 +2,16 @@ use core::num::NonZeroU8;
|
||||||
|
|
||||||
use leptos::{
|
use leptos::{
|
||||||
ev::{Event, MouseEvent, SubmitEvent, Targeted},
|
ev::{Event, MouseEvent, SubmitEvent, Targeted},
|
||||||
html::Form,
|
|
||||||
prelude::*,
|
prelude::*,
|
||||||
tachys::html::node_ref::NodeRefContainer,
|
|
||||||
web_sys::HtmlInputElement,
|
web_sys::HtmlInputElement,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::ConsoleLogError;
|
use crate::app::error::WolfError;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ChangePlayerNumber(
|
pub fn ChangePlayerNumber(
|
||||||
submitted_number: WriteSignal<Option<NonZeroU8>>,
|
submitted_number: WriteSignal<Option<NonZeroU8>>,
|
||||||
error: WriteSignal<Option<String>>,
|
error: WriteSignal<Option<WolfError>>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let number: RwSignal<Option<NonZeroU8>> = RwSignal::new(None);
|
let number: RwSignal<Option<NonZeroU8>> = RwSignal::new(None);
|
||||||
let update = move |e: Targeted<Event, HtmlInputElement>| {
|
let update = move |e: Targeted<Event, HtmlInputElement>| {
|
||||||
|
|
@ -43,7 +41,7 @@ pub fn ChangePlayerNumber(
|
||||||
ev.prevent_default();
|
ev.prevent_default();
|
||||||
log::warn!("called submit with number: {:?}", number.get());
|
log::warn!("called submit with number: {:?}", number.get());
|
||||||
let Some(num) = number.get() else {
|
let Some(num) = number.get() else {
|
||||||
error.set(Some("please set a number".into()));
|
error.set(Some(WolfError::NoSeatNumber));
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
submitted_number.set(Some(num));
|
submitted_number.set(Some(num));
|
||||||
|
|
@ -52,7 +50,7 @@ pub fn ChangePlayerNumber(
|
||||||
ev.prevent_default();
|
ev.prevent_default();
|
||||||
log::warn!("called submit with number: {:?}", number.get());
|
log::warn!("called submit with number: {:?}", number.get());
|
||||||
let Some(num) = number.get() else {
|
let Some(num) = number.get() else {
|
||||||
error.set(Some("please set a number".into()));
|
error.set(Some(WolfError::NoSeatNumber));
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
submitted_number.set(Some(num));
|
submitted_number.set(Some(num));
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ use core::ops::Not;
|
||||||
use leptos::{ev::MouseEvent, prelude::*};
|
use leptos::{ev::MouseEvent, prelude::*};
|
||||||
use leptos_router::hooks::use_url;
|
use leptos_router::hooks::use_url;
|
||||||
use reactive_stores::Store;
|
use reactive_stores::Store;
|
||||||
|
use uuid::Uuid;
|
||||||
use werewolves_proto::{error::ServerError, game::GameId, token::TokenString};
|
use werewolves_proto::{error::ServerError, game::GameId, token::TokenString};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -113,6 +114,10 @@ pub fn Nav() -> impl IntoView {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
move || {
|
||||||
|
if is_big_screen_path(url.get().path()) {
|
||||||
|
return ().into_any();
|
||||||
|
}
|
||||||
view! {
|
view! {
|
||||||
<nav class="header">
|
<nav class="header">
|
||||||
<Show when=move || auth.session().get().is_some() fallback=default_view>
|
<Show when=move || auth.session().get().is_some() fallback=default_view>
|
||||||
|
|
@ -120,4 +125,18 @@ pub fn Nav() -> impl IntoView {
|
||||||
</Show>
|
</Show>
|
||||||
</nav>
|
</nav>
|
||||||
}
|
}
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_big_screen_path(path: &str) -> bool {
|
||||||
|
let parts = path
|
||||||
|
.split('/')
|
||||||
|
.map(|p| p.trim())
|
||||||
|
.filter(|p| !p.is_empty())
|
||||||
|
.collect::<Box<[_]>>();
|
||||||
|
parts.len() >= 3
|
||||||
|
&& parts[0] == "games"
|
||||||
|
&& Uuid::parse_str(parts[1]).is_ok()
|
||||||
|
&& parts[2] == "big"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
use thiserror::Error;
|
||||||
|
use werewolves_proto::error::ServerError;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Error)]
|
||||||
|
pub enum WolfError {
|
||||||
|
#[error("{0}")]
|
||||||
|
Server(#[from] ServerError),
|
||||||
|
#[error("username must be between {min} and {max} characters")]
|
||||||
|
UsernameLimits { min: usize, max: usize },
|
||||||
|
#[error("password must be between {min} and {max} characters")]
|
||||||
|
PasswordLimits { min: usize, max: usize },
|
||||||
|
#[error("current password must be between {min} and {max} characters")]
|
||||||
|
CurrentPasswordLimits { min: usize, max: usize },
|
||||||
|
#[error("game cancelled")]
|
||||||
|
GameCancelled,
|
||||||
|
#[error("password confirmation does not match")]
|
||||||
|
PasswordConfirmNoMatch,
|
||||||
|
#[error("please set a seat number")]
|
||||||
|
NoSeatNumber,
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ use werewolves_proto::message::{ClientMessage, host::HostMessage};
|
||||||
use werewolves_proto::message::{IntoClientResponse, WrappedServerMessage};
|
use werewolves_proto::message::{IntoClientResponse, WrappedServerMessage};
|
||||||
|
|
||||||
use crate::app::components::ErrorBox;
|
use crate::app::components::ErrorBox;
|
||||||
|
use crate::app::error::WolfError;
|
||||||
use crate::{
|
use crate::{
|
||||||
ConsoleLogError,
|
ConsoleLogError,
|
||||||
app::{
|
app::{
|
||||||
|
|
@ -25,7 +26,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn GamePage(error: WriteSignal<Option<String>>) -> impl IntoView {
|
pub fn GamePage(error: WriteSignal<Option<WolfError>>) -> impl IntoView {
|
||||||
move || {
|
move || {
|
||||||
let params = hooks::use_params_map();
|
let params = hooks::use_params_map();
|
||||||
let auth = expect_context::<Store<AuthContext>>();
|
let auth = expect_context::<Store<AuthContext>>();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,191 @@
|
||||||
|
werewolves_macros::include_path!("werewolves/src/app/pages/game/big");
|
||||||
|
use core::str::FromStr;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
ConsoleLogError,
|
||||||
|
app::{error::WolfError, pages::NotFound, storage::user::AuthContextStoreFields},
|
||||||
|
};
|
||||||
|
use codee::binary::MsgpackSerdeCodec;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
|
use leptos_router::hooks;
|
||||||
|
use leptos_use::{
|
||||||
|
ReconnectLimit, UseWebSocketOptions, UseWebSocketReturn, core::ConnectionReadyState,
|
||||||
|
use_websocket_with_options,
|
||||||
|
};
|
||||||
|
use reactive_stores::Store;
|
||||||
|
use werewolves_proto::{
|
||||||
|
game::GameId,
|
||||||
|
message::{IntoClientResponse, WrappedServerMessage, host::ServerToHostMessage},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::app::storage::user::AuthContext;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
enum BigScreenPage {
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
Setup,
|
||||||
|
RoleReveal,
|
||||||
|
QrCode,
|
||||||
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn BigScreen() -> impl IntoView {}
|
pub fn BigScreen() -> impl IntoView {
|
||||||
|
move || {
|
||||||
|
let params = hooks::use_params_map();
|
||||||
|
let auth = expect_context::<Store<AuthContext>>();
|
||||||
|
let opened = RwSignal::new(false);
|
||||||
|
let disconnect = RwSignal::new(false);
|
||||||
|
let players = RwSignal::new(Default::default());
|
||||||
|
let settings = RwSignal::new(Default::default());
|
||||||
|
let page = RwSignal::new(BigScreenPage::default());
|
||||||
|
Effect::new(move || {
|
||||||
|
params.read().get("id").unwrap_or_default();
|
||||||
|
});
|
||||||
|
let Ok(game_id) = GameId::from_str(params.read().get("id").unwrap_or_default().as_str())
|
||||||
|
else {
|
||||||
|
return view! {<NotFound />}.into_any();
|
||||||
|
};
|
||||||
|
let url = RwSignal::new(format!("/api/games/{game_id}"));
|
||||||
|
Effect::watch(
|
||||||
|
move || url.get(),
|
||||||
|
move |_, _, _| {
|
||||||
|
#[cfg(feature = "hydrate")]
|
||||||
|
{
|
||||||
|
use crate::ConsoleLogError;
|
||||||
|
gloo::utils::window().location().reload().console_log_warn()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
let UseWebSocketReturn {
|
||||||
|
ready_state,
|
||||||
|
message,
|
||||||
|
send,
|
||||||
|
close,
|
||||||
|
..
|
||||||
|
} = use_websocket_with_options::<
|
||||||
|
WrappedServerMessage,
|
||||||
|
IntoClientResponse,
|
||||||
|
MsgpackSerdeCodec,
|
||||||
|
_,
|
||||||
|
_,
|
||||||
|
>(
|
||||||
|
#[cfg(not(feature = "ssr"))]
|
||||||
|
url.read().as_str(),
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
"",
|
||||||
|
UseWebSocketOptions::default().reconnect_limit(ReconnectLimit::Infinite),
|
||||||
|
);
|
||||||
|
Effect::new({
|
||||||
|
let close = close.clone();
|
||||||
|
move || {
|
||||||
|
if disconnect.get() {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Effect::new(move || match ready_state.get() {
|
||||||
|
ConnectionReadyState::Connecting => log::debug!("socket connecting"),
|
||||||
|
ConnectionReadyState::Closing => {
|
||||||
|
log::debug!("closing socket");
|
||||||
|
opened.set(false);
|
||||||
|
}
|
||||||
|
ConnectionReadyState::Open => {
|
||||||
|
Effect::new({
|
||||||
|
let send = send.clone();
|
||||||
|
let close = close.clone();
|
||||||
|
move || {
|
||||||
|
let o = opened.get();
|
||||||
|
if o {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(token) = auth.token().get() {
|
||||||
|
let auth_message = WrappedServerMessage::Authentication(token.token);
|
||||||
|
log::debug!("sending auth message: {auth_message:?}");
|
||||||
|
send(&auth_message);
|
||||||
|
opened.set(true);
|
||||||
|
} else {
|
||||||
|
close();
|
||||||
|
|
||||||
|
#[cfg(feature = "hydrate")]
|
||||||
|
gloo::utils::window()
|
||||||
|
.location()
|
||||||
|
.set_href("/")
|
||||||
|
.console_log_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ConnectionReadyState::Closed => log::debug!("connection closed"),
|
||||||
|
});
|
||||||
|
let _ = send;
|
||||||
|
Effect::new(move || {
|
||||||
|
let Some(msg) = message.get() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let msg = match msg {
|
||||||
|
IntoClientResponse::Player(msg) => {
|
||||||
|
log::error!("got client message?? {msg:?}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IntoClientResponse::Host(msg) => msg,
|
||||||
|
};
|
||||||
|
match msg {
|
||||||
|
ServerToHostMessage::GameCancelled => {
|
||||||
|
log::error!("game cancelled");
|
||||||
|
if gloo::utils::window().close().is_err() {
|
||||||
|
gloo::utils::window()
|
||||||
|
.location()
|
||||||
|
.replace("/")
|
||||||
|
.console_log_warn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ServerToHostMessage::Disconnect => disconnect.set(true),
|
||||||
|
ServerToHostMessage::Daytime {
|
||||||
|
characters,
|
||||||
|
marked,
|
||||||
|
day,
|
||||||
|
settings,
|
||||||
|
} => todo!(),
|
||||||
|
ServerToHostMessage::PlayerStates(character_states) => todo!(),
|
||||||
|
ServerToHostMessage::ActionPrompt(action_prompt, _) => todo!(),
|
||||||
|
ServerToHostMessage::ActionResult(character_identity, action_result) => todo!(),
|
||||||
|
ServerToHostMessage::Lobby {
|
||||||
|
players: p,
|
||||||
|
settings: s,
|
||||||
|
qr_mode,
|
||||||
|
} => {
|
||||||
|
if qr_mode {
|
||||||
|
page.set(BigScreenPage::QrCode);
|
||||||
|
} else {
|
||||||
|
players.set(p);
|
||||||
|
settings.set(s);
|
||||||
|
page.set(BigScreenPage::Setup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ServerToHostMessage::Error(err) => log::error!("server error: {err}"),
|
||||||
|
ServerToHostMessage::GameOver(game_over) => todo!(),
|
||||||
|
ServerToHostMessage::WaitingForRoleRevealAcks { ackd, waiting } => todo!(),
|
||||||
|
ServerToHostMessage::Story { story, page } => todo!(),
|
||||||
|
ServerToHostMessage::DeadChat(dead_chat_messages) => todo!(),
|
||||||
|
ServerToHostMessage::DeadChatMessage(dead_chat_message) => todo!(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let content = move || match page.get() {
|
||||||
|
BigScreenPage::None => ().into_any(),
|
||||||
|
BigScreenPage::Setup => {
|
||||||
|
view! { <SetupView settings=settings.read_only() /> }.into_any()
|
||||||
|
}
|
||||||
|
BigScreenPage::RoleReveal => todo!(),
|
||||||
|
BigScreenPage::QrCode => view! {
|
||||||
|
<QrView game_id=game_id/>
|
||||||
|
}
|
||||||
|
.into_any(),
|
||||||
|
};
|
||||||
|
|
||||||
|
view! { <div class="big-screen-wrapper">{content}</div> }.into_any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,170 @@
|
||||||
|
use core::ops::Not;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use convert_case::{Case, Casing};
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use werewolves_proto::game::{Category, GameId, GameSettings, SetupRole, SetupRoleTitle};
|
||||||
|
|
||||||
|
use crate::app::{
|
||||||
|
class::{AsClasses, Class},
|
||||||
|
components::{AssociatedIcon, Icon, IconSource, IconType},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn QrView(game_id: GameId) -> impl IntoView {
|
||||||
|
let qrcode_url = format!("/api/games/{game_id}/qr");
|
||||||
|
view! {
|
||||||
|
<div class="qrcode">
|
||||||
|
<img src=qrcode_url alt="qr code to join" />
|
||||||
|
<div class="details">
|
||||||
|
<h3>{"scan the qrcode to join"}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn SetupView(settings: ReadSignal<GameSettings>) -> impl IntoView {
|
||||||
|
move || {
|
||||||
|
let mut by_category: HashMap<Category, Vec<SetupRole>> =
|
||||||
|
Category::ALL.into_iter().map(|c| (c, Vec::new())).collect();
|
||||||
|
for slot in settings.read().slots() {
|
||||||
|
by_category
|
||||||
|
.get_mut(&slot.role.category())
|
||||||
|
.unwrap()
|
||||||
|
.push(slot.role.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut categories = by_category.into_iter().collect::<Box<[_]>>();
|
||||||
|
categories.sort_by_key(|(c, _)| match c {
|
||||||
|
Category::Wolves => 1u8,
|
||||||
|
Category::Intel => 2,
|
||||||
|
Category::Villager => 4,
|
||||||
|
Category::Defensive => 3,
|
||||||
|
Category::Offensive => 5,
|
||||||
|
Category::StartsAsVillager => 6,
|
||||||
|
});
|
||||||
|
|
||||||
|
let categories = categories
|
||||||
|
.into_iter()
|
||||||
|
.map(|(cat, members)| {
|
||||||
|
let hide = match cat {
|
||||||
|
Category::Wolves => CategoryMode::ShowExactRoleCount,
|
||||||
|
Category::Villager => CategoryMode::ShowExactRoleCount,
|
||||||
|
Category::Intel
|
||||||
|
| Category::Defensive
|
||||||
|
| Category::Offensive
|
||||||
|
| Category::StartsAsVillager => CategoryMode::HideAllInfo,
|
||||||
|
};
|
||||||
|
view! { <SetupCategory category=cat roles=members.into_boxed_slice() mode=hide /> }
|
||||||
|
})
|
||||||
|
.collect_view();
|
||||||
|
|
||||||
|
let power_roles_count = settings
|
||||||
|
.read()
|
||||||
|
.slots()
|
||||||
|
.iter()
|
||||||
|
.filter(|r| !matches!(r.role.category(), Category::Villager | Category::Wolves))
|
||||||
|
.count();
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="setup-screen">
|
||||||
|
<div class="setup">
|
||||||
|
//
|
||||||
|
{categories} <div class="setup-category big">
|
||||||
|
<span class="slot">
|
||||||
|
<span class="count">{power_roles_count}</span>
|
||||||
|
<span class="title village box">{"Power roles from..."}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||||
|
#[allow(unused)]
|
||||||
|
pub enum CategoryMode {
|
||||||
|
#[default]
|
||||||
|
HideAllInfo,
|
||||||
|
ShowTotalCount,
|
||||||
|
ShowExactRoleCount,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn SetupCategory(
|
||||||
|
category: Category,
|
||||||
|
#[allow(clippy::boxed_local)] roles: Box<[SetupRole]>,
|
||||||
|
#[prop(optional)] mode: CategoryMode,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let mut roles_in_category = SetupRoleTitle::ALL
|
||||||
|
.into_iter()
|
||||||
|
.filter(|r| r.category() == category)
|
||||||
|
.collect::<Box<[_]>>();
|
||||||
|
roles_in_category.sort_by_key(|l| l.into_role().wakes_night_zero());
|
||||||
|
|
||||||
|
let all_roles = roles_in_category
|
||||||
|
.into_iter()
|
||||||
|
.map(|r| (r, roles.iter().filter(|sr| sr.title() == r).count()))
|
||||||
|
.filter(|(_, count)| !(matches!(category, Category::Wolves) && *count == 0))
|
||||||
|
.map(|(r, count)| {
|
||||||
|
let as_role = r.into_role();
|
||||||
|
|
||||||
|
let show_count = matches!(mode, CategoryMode::ShowExactRoleCount) && count > 0;
|
||||||
|
let count = if show_count {
|
||||||
|
count.to_string()
|
||||||
|
} else {
|
||||||
|
"?".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="slot">
|
||||||
|
<span class="count" class:invisible=!show_count>
|
||||||
|
{count}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class=["role", r.category().class(), "box"].as_classes()
|
||||||
|
class:wakes=as_role.wakes_night_zero()
|
||||||
|
>
|
||||||
|
{r.to_string().to_case(Case::Title)}
|
||||||
|
</span>
|
||||||
|
<div class="attributes">
|
||||||
|
<div class="alignment">
|
||||||
|
<Icon source=as_role.alignment().icon() r#type=IconType::Small />
|
||||||
|
</div>
|
||||||
|
<div class="killer" class:inactive=as_role.killer().killer().not()>
|
||||||
|
<Icon source=IconSource::Killer r#type=IconType::Small />
|
||||||
|
</div>
|
||||||
|
<div class="poweful" class:inactive=as_role.powerful().powerful().not()>
|
||||||
|
<Icon source=IconSource::Powerful r#type=IconType::Small />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect_view();
|
||||||
|
|
||||||
|
let show_role_count = matches!(
|
||||||
|
mode,
|
||||||
|
CategoryMode::ShowTotalCount | CategoryMode::ShowExactRoleCount
|
||||||
|
);
|
||||||
|
let roles_count = if show_role_count {
|
||||||
|
roles.len().to_string()
|
||||||
|
} else {
|
||||||
|
"?".to_string()
|
||||||
|
};
|
||||||
|
view! {
|
||||||
|
<div class="setup-category big">
|
||||||
|
<div class="slot">
|
||||||
|
<span class="count" class:invisible=!show_role_count>
|
||||||
|
{roles_count}
|
||||||
|
</span>
|
||||||
|
<div class=["title", category.class(), "box"]
|
||||||
|
.as_classes()>{category.to_string().to_case(Case::Title)}</div>
|
||||||
|
</div>
|
||||||
|
{all_roles}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,8 +11,8 @@ use werewolves_proto::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::ConsoleLogError;
|
|
||||||
use crate::app::{Preferences, components::DialogModal};
|
use crate::app::{Preferences, components::DialogModal};
|
||||||
|
use crate::{ConsoleLogError, app::error::WolfError};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
enum HostPage {
|
enum HostPage {
|
||||||
|
|
@ -24,7 +24,7 @@ enum HostPage {
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn HostGamePage(
|
pub fn HostGamePage(
|
||||||
error: WriteSignal<Option<String>>,
|
error: WriteSignal<Option<WolfError>>,
|
||||||
message: Signal<Option<Srv2Host>>,
|
message: Signal<Option<Srv2Host>>,
|
||||||
reply: WriteSignal<Option<HostMessage>>,
|
reply: WriteSignal<Option<HostMessage>>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
|
|
@ -68,14 +68,14 @@ pub fn HostGamePage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Srv2Host::GameCancelled => {
|
Srv2Host::GameCancelled => {
|
||||||
error.set(Some("game was cancelled".into()));
|
error.set(Some(WolfError::GameCancelled));
|
||||||
gloo::utils::window()
|
gloo::utils::window()
|
||||||
.location()
|
.location()
|
||||||
.replace("/")
|
.replace("/")
|
||||||
.console_log_warn();
|
.console_log_warn();
|
||||||
}
|
}
|
||||||
Srv2Host::Error(err) => {
|
Srv2Host::Error(err) => {
|
||||||
error.set(Some(err.to_string()));
|
error.set(Some(err.into()));
|
||||||
}
|
}
|
||||||
Srv2Host::WaitingForRoleRevealAcks { ackd, waiting } => {
|
Srv2Host::WaitingForRoleRevealAcks { ackd, waiting } => {
|
||||||
let mut reveals = ackd
|
let mut reveals = ackd
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,14 @@ use crate::app::{
|
||||||
DialogModal, DialogMode, Equals, IdentificationInline, Sample, TutorialBox,
|
DialogModal, DialogMode, Equals, IdentificationInline, Sample, TutorialBox,
|
||||||
input::ChangePlayerNumber,
|
input::ChangePlayerNumber,
|
||||||
},
|
},
|
||||||
|
error::WolfError,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn HostPlayerList(
|
pub fn HostPlayerList(
|
||||||
players: ReadSignal<Box<[PlayerState]>>,
|
players: ReadSignal<Box<[PlayerState]>>,
|
||||||
reply: WriteSignal<Option<HostMessage>>,
|
reply: WriteSignal<Option<HostMessage>>,
|
||||||
error: WriteSignal<Option<String>>,
|
error: WriteSignal<Option<WolfError>>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let players = move || {
|
let players = move || {
|
||||||
let mut players = players.get();
|
let mut players = players.get();
|
||||||
|
|
@ -88,7 +89,7 @@ pub fn HostPlayerList(
|
||||||
fn HostPlayerDialogBody(
|
fn HostPlayerDialogBody(
|
||||||
player: PlayerState,
|
player: PlayerState,
|
||||||
reply: WriteSignal<Option<HostMessage>>,
|
reply: WriteSignal<Option<HostMessage>>,
|
||||||
error: WriteSignal<Option<String>>,
|
error: WriteSignal<Option<WolfError>>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let pid = player.identification.player_id;
|
let pid = player.identification.player_id;
|
||||||
let kick = move |_| reply.set(Some(HostMessage::Lobby(HostLobbyMessage::Kick(pid))));
|
let kick = move |_| reply.set(Some(HostMessage::Lobby(HostLobbyMessage::Kick(pid))));
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use core::ops::Not;
|
||||||
use leptos::{ev::MouseEvent, prelude::*};
|
use leptos::{ev::MouseEvent, prelude::*};
|
||||||
use werewolves_proto::message::{CharacterIdentity, host::HostMessage};
|
use werewolves_proto::message::{CharacterIdentity, host::HostMessage};
|
||||||
|
|
||||||
use crate::app::components::IdentityInline;
|
use crate::app::{components::IdentityInline, error::WolfError};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct RoleRevealCharacter {
|
pub struct RoleRevealCharacter {
|
||||||
|
|
@ -15,7 +15,7 @@ pub struct RoleRevealCharacter {
|
||||||
pub fn RoleRevealAcks(
|
pub fn RoleRevealAcks(
|
||||||
reply: WriteSignal<Option<HostMessage>>,
|
reply: WriteSignal<Option<HostMessage>>,
|
||||||
acks: ReadSignal<Box<[RoleRevealCharacter]>>,
|
acks: ReadSignal<Box<[RoleRevealCharacter]>>,
|
||||||
error: WriteSignal<Option<String>>,
|
error: WriteSignal<Option<WolfError>>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let acks = move || {
|
let acks = move || {
|
||||||
acks.get()
|
acks.get()
|
||||||
|
|
@ -47,7 +47,11 @@ pub fn RoleRevealAcks(
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
reply.set(Some(HostMessage::ForceAllRoleAcks));
|
reply.set(Some(HostMessage::ForceAllRoleAcks));
|
||||||
};
|
};
|
||||||
view! { <button on:click=force class="force-all">"force all"</button> }
|
view! {
|
||||||
|
<button on:click=force class="force-all">
|
||||||
|
"force all"
|
||||||
|
</button>
|
||||||
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ use werewolves_proto::{
|
||||||
use crate::app::{
|
use crate::app::{
|
||||||
class::{AsClasses, Class, PartialClass},
|
class::{AsClasses, Class, PartialClass},
|
||||||
components::{DialogModal, DialogMode, IdentityInline},
|
components::{DialogModal, DialogMode, IdentityInline},
|
||||||
|
error::WolfError,
|
||||||
pages::game::host::HostPlayerList,
|
pages::game::host::HostPlayerList,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -27,7 +28,7 @@ pub fn Settings(
|
||||||
dialog_open: RwSignal<HashMap<SlotId, bool>>,
|
dialog_open: RwSignal<HashMap<SlotId, bool>>,
|
||||||
open_categories: RwSignal<HashMap<Category, bool>>,
|
open_categories: RwSignal<HashMap<Category, bool>>,
|
||||||
reply: WriteSignal<Option<HostMessage>>,
|
reply: WriteSignal<Option<HostMessage>>,
|
||||||
error: WriteSignal<Option<String>>,
|
error: WriteSignal<Option<WolfError>>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let slots = move || {
|
let slots = move || {
|
||||||
settings
|
settings
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,17 @@ use convert_case::{Case, Casing};
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use sorted_vec::SortedSet;
|
use sorted_vec::SortedSet;
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
|
error::ServerError,
|
||||||
message::{
|
message::{
|
||||||
ClientDeadChat, ClientMessage, ServerToClientMessage as Srv2Client, dead::DeadChatMessage,
|
ClientDeadChat, ClientMessage, ServerToClientMessage as Srv2Client, dead::DeadChatMessage,
|
||||||
},
|
},
|
||||||
role::RoleTitle,
|
role::RoleTitle,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{ConsoleLogError, app::components::ErrorBox};
|
use crate::{
|
||||||
|
ConsoleLogError,
|
||||||
|
app::{components::ErrorBox, error::WolfError},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
enum Page {
|
enum Page {
|
||||||
|
|
@ -31,7 +35,7 @@ enum Page {
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn PlayerGamePage(
|
pub fn PlayerGamePage(
|
||||||
error: WriteSignal<Option<String>>,
|
error: WriteSignal<Option<WolfError>>,
|
||||||
message: Signal<Option<Srv2Client>>,
|
message: Signal<Option<Srv2Client>>,
|
||||||
reply: WriteSignal<Option<ClientMessage>>,
|
reply: WriteSignal<Option<ClientMessage>>,
|
||||||
disconnect: RwSignal<bool>,
|
disconnect: RwSignal<bool>,
|
||||||
|
|
@ -44,7 +48,7 @@ pub fn PlayerGamePage(
|
||||||
};
|
};
|
||||||
match message {
|
match message {
|
||||||
Srv2Client::GameCancelled => {
|
Srv2Client::GameCancelled => {
|
||||||
error.set(Some("game was cancelled".into()));
|
error.set(Some(WolfError::GameCancelled));
|
||||||
gloo::utils::window()
|
gloo::utils::window()
|
||||||
.location()
|
.location()
|
||||||
.replace("/")
|
.replace("/")
|
||||||
|
|
@ -80,7 +84,7 @@ pub fn PlayerGamePage(
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
gloo::utils::window().location().reload().console_log_err();
|
gloo::utils::window().location().reload().console_log_err();
|
||||||
}
|
}
|
||||||
Srv2Client::Error(err) => error.set(Some(err.to_string())),
|
Srv2Client::Error(err) => error.set(Some(ServerError::GameError(err).into())),
|
||||||
}
|
}
|
||||||
// match message {}
|
// match message {}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,14 @@ use leptos::{
|
||||||
};
|
};
|
||||||
use werewolves_proto::message::{ClientMessage, UpdateSelf};
|
use werewolves_proto::message::{ClientMessage, UpdateSelf};
|
||||||
|
|
||||||
|
use crate::app::error::WolfError;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn PlayerLobby(
|
pub fn PlayerLobby(
|
||||||
reply: WriteSignal<Option<ClientMessage>>,
|
reply: WriteSignal<Option<ClientMessage>>,
|
||||||
joined: ReadSignal<bool>,
|
joined: ReadSignal<bool>,
|
||||||
current_number: ReadSignal<Option<NonZeroU8>>,
|
current_number: ReadSignal<Option<NonZeroU8>>,
|
||||||
error: WriteSignal<Option<String>>,
|
error: WriteSignal<Option<WolfError>>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let click = move |ev: MouseEvent| {
|
let click = move |ev: MouseEvent| {
|
||||||
ev.prevent_default();
|
ev.prevent_default();
|
||||||
|
|
@ -61,7 +63,7 @@ pub fn PlayerLobby(
|
||||||
let submit = move |ev: SubmitEvent| {
|
let submit = move |ev: SubmitEvent| {
|
||||||
ev.prevent_default();
|
ev.prevent_default();
|
||||||
let Some(num) = number.get() else {
|
let Some(num) = number.get() else {
|
||||||
error.set(Some("please set a number".into()));
|
error.set(Some(WolfError::NoSeatNumber));
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
reply.set(Some(ClientMessage::UpdateSelf(UpdateSelf::Number(num))));
|
reply.set(Some(ClientMessage::UpdateSelf(UpdateSelf::Number(num))));
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,17 @@ use leptos::prelude::*;
|
||||||
use reactive_stores::Store;
|
use reactive_stores::Store;
|
||||||
|
|
||||||
use crate::app::{
|
use crate::app::{
|
||||||
|
error::WolfError,
|
||||||
pages::{Signin, Signup},
|
pages::{Signin, Signup},
|
||||||
storage::user::{AuthContext, AuthContextStoreFields, UserSession},
|
storage::user::{AuthContext, AuthContextStoreFields, UserSession},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Main() -> impl IntoView {
|
pub fn Main(error: WriteSignal<Option<WolfError>>) -> impl IntoView {
|
||||||
let auth = expect_context::<Store<AuthContext>>();
|
let auth = expect_context::<Store<AuthContext>>();
|
||||||
move || match auth.session().get() {
|
move || match auth.session().get() {
|
||||||
Some(session) => view! { <SignedInMain session=session /> }.into_any(),
|
Some(session) => view! { <SignedInMain session=session /> }.into_any(),
|
||||||
None => view! { <SignedOutMain /> }.into_any(),
|
None => view! { <SignedOutMain error=error /> }.into_any(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[component]
|
#[component]
|
||||||
|
|
@ -20,19 +21,19 @@ pub fn SignedInMain(session: UserSession) -> impl IntoView {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn SignedOutMain() -> impl IntoView {
|
pub fn SignedOutMain(error: WriteSignal<Option<WolfError>>) -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
<h1>"welcome"</h1>
|
<h1>"welcome"</h1>
|
||||||
<div class="welcome-columns">
|
<div class="welcome-columns">
|
||||||
<div>
|
<div>
|
||||||
<h2>"create a new account"</h2>
|
<h2>"create a new account"</h2>
|
||||||
<h4>"with just a username"</h4>
|
<h4>"with just a username"</h4>
|
||||||
<Signup redirect=false header=false />
|
<Signup error=error redirect=false header=false />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2>"sign in"</h2>
|
<h2>"sign in"</h2>
|
||||||
<h4>"with an existing account"</h4>
|
<h4>"with an existing account"</h4>
|
||||||
<Signin redirect=false header=false />
|
<Signin error=error redirect=false header=false />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
use core::ops::Not;
|
use core::ops::Not;
|
||||||
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use gloo::history::History;
|
|
||||||
use leptos::{ev::MouseEvent, prelude::*};
|
use leptos::{ev::MouseEvent, prelude::*};
|
||||||
use werewolves_proto::player::{Password, Username};
|
use werewolves_proto::player::{Password, Username};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{
|
app::{
|
||||||
components::ErrorBox,
|
components::ErrorBox,
|
||||||
|
error::WolfError,
|
||||||
storage::user::{AuthContext, UserToken},
|
storage::user::{AuthContext, UserToken},
|
||||||
},
|
},
|
||||||
auth::Signin,
|
auth::Signin,
|
||||||
|
|
@ -16,12 +16,12 @@ use reactive_stores::Store;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Signin(
|
pub fn Signin(
|
||||||
|
error: WriteSignal<Option<WolfError>>,
|
||||||
#[prop(default = true)] redirect: bool,
|
#[prop(default = true)] redirect: bool,
|
||||||
#[prop(optional)] header: bool,
|
#[prop(optional)] header: bool,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let submit = ServerAction::<Signin>::new();
|
let submit = ServerAction::<Signin>::new();
|
||||||
use crate::app::storage::user::AuthContextStoreFields;
|
use crate::app::storage::user::AuthContextStoreFields;
|
||||||
let error: RwSignal<Option<String>> = RwSignal::new(None);
|
|
||||||
|
|
||||||
let auth = expect_context::<Store<AuthContext>>();
|
let auth = expect_context::<Store<AuthContext>>();
|
||||||
|
|
||||||
|
|
@ -51,22 +51,20 @@ pub fn Signin(
|
||||||
let username = match Username::new(user.get()) {
|
let username = match Username::new(user.get()) {
|
||||||
Ok(u) => u,
|
Ok(u) => u,
|
||||||
Err(exp_range) => {
|
Err(exp_range) => {
|
||||||
error.set(Some(format!(
|
error.set(Some(WolfError::UsernameLimits {
|
||||||
"username must be between {} and {} characters",
|
min: *exp_range.start(),
|
||||||
exp_range.start(),
|
max: exp_range.max().unwrap_or_default(),
|
||||||
exp_range.end()
|
}));
|
||||||
)));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let password = match Password::new(pass.get()) {
|
let password = match Password::new(pass.get()) {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(exp_range) => {
|
Err(exp_range) => {
|
||||||
error.set(Some(format!(
|
error.set(Some(WolfError::PasswordLimits {
|
||||||
"password must be between {} and {} characters",
|
min: *exp_range.start(),
|
||||||
exp_range.start(),
|
max: exp_range.max().unwrap_or_default(),
|
||||||
exp_range.end()
|
}));
|
||||||
)));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -85,7 +83,7 @@ pub fn Signin(
|
||||||
submit.clear();
|
submit.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Err(err)) => error.set(Some(err.to_string())),
|
Some(Err(err)) => error.set(Some(err.into())),
|
||||||
None => {}
|
None => {}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -122,7 +120,6 @@ pub fn Signin(
|
||||||
<label for="password">"password"</label>
|
<label for="password">"password"</label>
|
||||||
{pass_input}
|
{pass_input}
|
||||||
{submit}
|
{submit}
|
||||||
<ErrorBox msg=error />
|
|
||||||
</form>
|
</form>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,7 @@ use core::ops::Not;
|
||||||
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use gloo::history::History;
|
use gloo::history::History;
|
||||||
use leptos::{
|
use leptos::{ev::MouseEvent, prelude::*};
|
||||||
ev::MouseEvent,
|
|
||||||
prelude::*,
|
|
||||||
};
|
|
||||||
use rand::distr::SampleString;
|
use rand::distr::SampleString;
|
||||||
use reactive_stores::Store;
|
use reactive_stores::Store;
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
|
|
@ -17,6 +14,7 @@ use werewolves_proto::{
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{
|
app::{
|
||||||
components::{ErrorBox, input::TextInput},
|
components::{ErrorBox, input::TextInput},
|
||||||
|
error::WolfError,
|
||||||
storage::user::{AuthContext, AuthContextStoreFields, PasswordlessUser, UserToken},
|
storage::user::{AuthContext, AuthContextStoreFields, PasswordlessUser, UserToken},
|
||||||
},
|
},
|
||||||
auth::Signin,
|
auth::Signin,
|
||||||
|
|
@ -42,6 +40,7 @@ pub async fn create_user(
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Signup(
|
pub fn Signup(
|
||||||
|
error: WriteSignal<Option<WolfError>>,
|
||||||
#[prop(default = true)] redirect: bool,
|
#[prop(default = true)] redirect: bool,
|
||||||
#[prop(default = true)] header: bool,
|
#[prop(default = true)] header: bool,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
|
|
@ -62,7 +61,6 @@ pub fn Signup(
|
||||||
}
|
}
|
||||||
|
|
||||||
let submit = ServerAction::<CreateUser>::new();
|
let submit = ServerAction::<CreateUser>::new();
|
||||||
let error = RwSignal::new(None);
|
|
||||||
let generated_password = Password::new(
|
let generated_password = Password::new(
|
||||||
rand::distr::Alphanumeric.sample_string(&mut rand::rng(), Password::MAX_LEN),
|
rand::distr::Alphanumeric.sample_string(&mut rand::rng(), Password::MAX_LEN),
|
||||||
)
|
)
|
||||||
|
|
@ -76,11 +74,10 @@ pub fn Signup(
|
||||||
let username = match Username::new(user.get().clone()) {
|
let username = match Username::new(user.get().clone()) {
|
||||||
Ok(u) => u,
|
Ok(u) => u,
|
||||||
Err(exp_range) => {
|
Err(exp_range) => {
|
||||||
error.set(Some(format!(
|
error.set(Some(WolfError::UsernameLimits {
|
||||||
"username must be between {} and {} characters",
|
min: *exp_range.start(),
|
||||||
exp_range.start(),
|
max: *exp_range.end(),
|
||||||
exp_range.end()
|
}));
|
||||||
)));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -117,7 +114,7 @@ pub fn Signup(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error.write().replace(err.to_string());
|
error.write().replace(err.into());
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -131,7 +128,7 @@ pub fn Signup(
|
||||||
auth.session().set(Some(sess.into()));
|
auth.session().set(Some(sess.into()));
|
||||||
}
|
}
|
||||||
Some(Err(err)) => {
|
Some(Err(err)) => {
|
||||||
error.set(Some(format!("error signing in after signup: {err}")));
|
error.set(Some(err.into()));
|
||||||
}
|
}
|
||||||
None => {}
|
None => {}
|
||||||
});
|
});
|
||||||
|
|
@ -147,7 +144,6 @@ pub fn Signup(
|
||||||
view! { <button on:click=click.clone()>"sign up"</button> }
|
view! { <button on:click=click.clone()>"sign up"</button> }
|
||||||
}}
|
}}
|
||||||
{result}
|
{result}
|
||||||
<ErrorBox msg=error />
|
|
||||||
</form>
|
</form>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,12 @@ use reactive_stores::Store;
|
||||||
|
|
||||||
use crate::app::{
|
use crate::app::{
|
||||||
Preferences,
|
Preferences,
|
||||||
|
error::WolfError,
|
||||||
storage::user::{AuthContext, AuthContextStoreFields},
|
storage::user::{AuthContext, AuthContextStoreFields},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn UserSettings() -> impl IntoView {
|
pub fn UserSettings(error: WriteSignal<Option<WolfError>>) -> impl IntoView {
|
||||||
let auth = expect_context::<Store<AuthContext>>();
|
let auth = expect_context::<Store<AuthContext>>();
|
||||||
let (prefs_read, prefs_write) =
|
let (prefs_read, prefs_write) =
|
||||||
expect_context::<(Signal<Preferences>, WriteSignal<Preferences>)>();
|
expect_context::<(Signal<Preferences>, WriteSignal<Preferences>)>();
|
||||||
|
|
@ -51,10 +52,10 @@ pub fn UserSettings() -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
<ul class="user-settings-list">
|
<ul class="user-settings-list">
|
||||||
<li>
|
<li>
|
||||||
<UpdateProfileButton />
|
<UpdateProfileButton error=error />
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<ChangePasswordButton />
|
<ChangePasswordButton error=error />
|
||||||
</li>
|
</li>
|
||||||
<li>{tutorial_toggle_button}</li>
|
<li>{tutorial_toggle_button}</li>
|
||||||
<li>{cancel_game_toggle_button}</li>
|
<li>{cancel_game_toggle_button}</li>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ use werewolves_proto::{cbor_leptos::CborPost, error::ServerError, player::Passwo
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{
|
app::{
|
||||||
components::{DialogModal, ErrorBox},
|
components::{DialogModal, ErrorBox},
|
||||||
|
error::WolfError,
|
||||||
storage::user::{AuthContext, AuthContextStoreFields},
|
storage::user::{AuthContext, AuthContextStoreFields},
|
||||||
},
|
},
|
||||||
db::AppState,
|
db::AppState,
|
||||||
|
|
@ -24,9 +25,8 @@ pub async fn change_password(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ChangePasswordButton() -> impl IntoView {
|
pub fn ChangePasswordButton(error: WriteSignal<Option<WolfError>>) -> impl IntoView {
|
||||||
let auth = expect_context::<Store<AuthContext>>();
|
let auth = expect_context::<Store<AuthContext>>();
|
||||||
let error = RwSignal::new(None);
|
|
||||||
let action = ServerAction::<ChangePassword>::new();
|
let action = ServerAction::<ChangePassword>::new();
|
||||||
let current = RwSignal::new(String::new());
|
let current = RwSignal::new(String::new());
|
||||||
Effect::new(move || {
|
Effect::new(move || {
|
||||||
|
|
@ -42,26 +42,24 @@ pub fn ChangePasswordButton() -> impl IntoView {
|
||||||
let current = match Password::new(current.get()) {
|
let current = match Password::new(current.get()) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(exp_range) => {
|
Err(exp_range) => {
|
||||||
error.set(Some(format!(
|
error.set(Some(WolfError::CurrentPasswordLimits {
|
||||||
"current password must be between {} and {} characters",
|
min: *exp_range.start(),
|
||||||
exp_range.start(),
|
max: *exp_range.end(),
|
||||||
exp_range.end()
|
}));
|
||||||
)));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if new.get() != new_confirm.get() {
|
if new.get() != new_confirm.get() {
|
||||||
error.set(Some("password confirmation does not match".into()));
|
error.set(Some(WolfError::PasswordConfirmNoMatch));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let new = match Password::new(new.get()) {
|
let new = match Password::new(new.get()) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(exp_range) => {
|
Err(exp_range) => {
|
||||||
error.set(Some(format!(
|
error.set(Some(WolfError::PasswordLimits {
|
||||||
"new password must be between {} and {} characters",
|
min: *exp_range.start(),
|
||||||
exp_range.start(),
|
max: *exp_range.end(),
|
||||||
exp_range.end()
|
}));
|
||||||
)));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -87,7 +85,7 @@ pub fn ChangePasswordButton() -> impl IntoView {
|
||||||
auth.passwordless().set(None);
|
auth.passwordless().set(None);
|
||||||
}
|
}
|
||||||
Some(Err(err)) => {
|
Some(Err(err)) => {
|
||||||
error.set(Some(err.to_string()));
|
error.set(Some(err.into()));
|
||||||
}
|
}
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
|
|
@ -110,7 +108,6 @@ pub fn ChangePasswordButton() -> impl IntoView {
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<DialogModal button_content="change password" close_backdrop=false>
|
<DialogModal button_content="change password" close_backdrop=false>
|
||||||
<ErrorBox msg=error />
|
|
||||||
<form>
|
<form>
|
||||||
<div class="form-fields">
|
<div class="form-fields">
|
||||||
<label for="current-password">"current password"</label>
|
<label for="current-password">"current password"</label>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ use werewolves_proto::{
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{
|
app::{
|
||||||
components::{DialogModal, ErrorBox},
|
components::{DialogModal, ErrorBox},
|
||||||
|
error::WolfError,
|
||||||
storage::user::{AuthContext, AuthContextStoreFields},
|
storage::user::{AuthContext, AuthContextStoreFields},
|
||||||
},
|
},
|
||||||
db::AppState,
|
db::AppState,
|
||||||
|
|
@ -27,9 +28,8 @@ fn trim_or_none(s: String) -> Option<String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn UpdateProfileButton() -> impl IntoView {
|
pub fn UpdateProfileButton(error: WriteSignal<Option<WolfError>>) -> impl IntoView {
|
||||||
let auth = expect_context::<Store<AuthContext>>();
|
let auth = expect_context::<Store<AuthContext>>();
|
||||||
let error = RwSignal::new(None);
|
|
||||||
let display_name = RwSignal::new(String::new());
|
let display_name = RwSignal::new(String::new());
|
||||||
let pronouns = RwSignal::new(String::new());
|
let pronouns = RwSignal::new(String::new());
|
||||||
Effect::new(move || {
|
Effect::new(move || {
|
||||||
|
|
@ -69,7 +69,7 @@ pub fn UpdateProfileButton() -> impl IntoView {
|
||||||
sess.pronouns = input.update.pronouns;
|
sess.pronouns = input.update.pronouns;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Err(err)) => error.set(Some(err.to_string())),
|
Some(Err(err)) => error.set(Some(err.into())),
|
||||||
None => {}
|
None => {}
|
||||||
});
|
});
|
||||||
let on_success = move || {
|
let on_success = move || {
|
||||||
|
|
@ -82,7 +82,6 @@ pub fn UpdateProfileButton() -> impl IntoView {
|
||||||
};
|
};
|
||||||
view! {
|
view! {
|
||||||
<DialogModal button_content="update profile" close_backdrop=false>
|
<DialogModal button_content="update profile" close_backdrop=false>
|
||||||
<ErrorBox msg=error />
|
|
||||||
<form>
|
<form>
|
||||||
<label for="display-name">"display name"</label>
|
<label for="display-name">"display name"</label>
|
||||||
<input
|
<input
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue