error enum for errorbox instead of String + setup
This commit is contained in:
parent
d3d2819e12
commit
9bba472917
|
|
@ -4061,6 +4061,7 @@ dependencies = [
|
|||
"serde",
|
||||
"sorted-vec",
|
||||
"sqlx",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tower-http",
|
||||
"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_total_height: $host_nav_height + $host_nav_top_pad + $host_nav_bottom_pad;
|
||||
|
||||
$wolves_color: rgba(255, 0, 0, 0.7);
|
||||
$village_color: rgba(0, 0, 255, 0.7);
|
||||
$red1: #f62b5a;
|
||||
$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);
|
||||
$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);
|
||||
$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);
|
||||
$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);
|
||||
$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);
|
||||
$damned_color: color.adjust($village_color, $hue: 45deg);
|
||||
$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);
|
||||
|
||||
@import 'faction';
|
||||
@import 'setup';
|
||||
@import 'icon';
|
||||
|
||||
@mixin flexbox() {
|
||||
display: -webkit-box;
|
||||
|
|
@ -57,6 +72,16 @@ body {
|
|||
color: white;
|
||||
}
|
||||
|
||||
.big-screen-wrapper {
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.error_container {
|
||||
position: fixed;
|
||||
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 {
|
||||
type Err = uuid::Error;
|
||||
type Err = ::uuid::Error;
|
||||
|
||||
fn from_str(s: &str) -> core::result::Result<Self, Self::Err> {
|
||||
Ok(Self(uuid::Uuid::from_str(s)?))
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
uuid::Uuid::parse_str(s).map(Self)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ werewolves-macros.workspace = true
|
|||
werewolves-proto.workspace = true
|
||||
codee.workspace = true
|
||||
convert_case.workspace = true
|
||||
thiserror.workspace = true
|
||||
sorted-vec.workspace = true
|
||||
|
||||
[features]
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ pub mod components {
|
|||
}
|
||||
|
||||
pub mod class;
|
||||
pub mod error;
|
||||
pub mod storage;
|
||||
|
||||
use codee::string::JsonSerdeCodec;
|
||||
use gloo::storage::Storage;
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::{Link, MetaTags, Stylesheet, Title, provide_meta_context};
|
||||
use leptos_router::{
|
||||
|
|
@ -27,7 +27,8 @@ use serde::{Deserialize, Serialize};
|
|||
use crate::{
|
||||
app::{
|
||||
components::{ErrorBox, Nav},
|
||||
pages::{GamePage, Main, NotFound, Signin, Signup, UserSettings},
|
||||
error::WolfError,
|
||||
pages::{GamePage, Main, NotFound, Signin, Signup, UserSettings, big::BigScreen},
|
||||
storage::{
|
||||
Stored,
|
||||
user::{AuthContext, AuthContextStoreFields},
|
||||
|
|
@ -96,7 +97,7 @@ pub fn App() -> impl IntoView {
|
|||
.then_some(auth_store.session().get().is_some())
|
||||
};
|
||||
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! {
|
||||
<Stylesheet id="leptos" href="/pkg/werewolves.css" />
|
||||
|
|
@ -109,22 +110,25 @@ pub fn App() -> impl IntoView {
|
|||
<Nav />
|
||||
<ErrorBox msg=error />
|
||||
<Routes fallback=NotFound>
|
||||
<Route path=path!("/") view=Main />
|
||||
<Route
|
||||
path=path!("/")
|
||||
view=move || view! { <Main error=error.write_only() /> }
|
||||
/>
|
||||
<ProtectedRoute
|
||||
path=path!("/signin")
|
||||
view=|| view! { <Signin /> }
|
||||
view=move || view! { <Signin error=error.write_only() /> }
|
||||
condition=not_logged_in
|
||||
redirect_path=|| "/"
|
||||
/>
|
||||
<ProtectedRoute
|
||||
path=path!("/signup")
|
||||
view=|| view! { <Signup /> }
|
||||
view=move || view! { <Signup error=error.write_only() /> }
|
||||
condition=not_logged_in
|
||||
redirect_path=|| "/"
|
||||
/>
|
||||
<ProtectedRoute
|
||||
path=path!("/user/settings")
|
||||
view=UserSettings
|
||||
view=move || view! { <UserSettings error=error.write_only() /> }
|
||||
condition=is_logged_in
|
||||
redirect_path=|| "/"
|
||||
/>
|
||||
|
|
@ -132,6 +136,7 @@ pub fn App() -> impl IntoView {
|
|||
path=path!("/games/:id")
|
||||
view=move || view! { <GamePage error=error.write_only() /> }
|
||||
/>
|
||||
<Route path=path!("/games/:id/big") view=BigScreen />
|
||||
</Routes>
|
||||
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@
|
|||
// 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 core::ops::{Deref, DerefMut};
|
||||
|
||||
use werewolves_proto::{
|
||||
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>);
|
||||
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
|
||||
where
|
||||
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 {
|
||||
type AsyncOutput = Self;
|
||||
type State = (leptos::tachys::renderer::types::Element, Self);
|
||||
|
|
|
|||
|
|
@ -3,12 +3,10 @@ use leptos_use::{
|
|||
UseDraggableOptions, UseDraggableReturn, core::Position, use_draggable_with_options,
|
||||
};
|
||||
|
||||
pub trait ViewError: core::error::Error {
|
||||
fn view(&self) -> impl IntoView;
|
||||
}
|
||||
use crate::app::error::WolfError;
|
||||
|
||||
#[component]
|
||||
pub fn ErrorBox(msg: RwSignal<Option<String>>) -> impl IntoView {
|
||||
pub fn ErrorBox(msg: RwSignal<Option<WolfError>>) -> impl IntoView {
|
||||
let el = NodeRef::<Div>::new();
|
||||
|
||||
// `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 }),
|
||||
);
|
||||
let content = move || {
|
||||
msg.get().map(|text| {
|
||||
msg.get().map(|err| {
|
||||
view! {
|
||||
<div class="error_container" hidden=move || msg.get().is_none()>
|
||||
<div node_ref=el style=move || style.get() class="error">
|
||||
<h5>"error"</h5>
|
||||
<p>{text.to_string()}</p>
|
||||
<p>{err.to_string()}</p>
|
||||
<button on:click=move |ev| {
|
||||
ev.prevent_default();
|
||||
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::{
|
||||
ev::{Event, MouseEvent, SubmitEvent, Targeted},
|
||||
html::Form,
|
||||
prelude::*,
|
||||
tachys::html::node_ref::NodeRefContainer,
|
||||
web_sys::HtmlInputElement,
|
||||
};
|
||||
|
||||
use crate::ConsoleLogError;
|
||||
use crate::app::error::WolfError;
|
||||
|
||||
#[component]
|
||||
pub fn ChangePlayerNumber(
|
||||
submitted_number: WriteSignal<Option<NonZeroU8>>,
|
||||
error: WriteSignal<Option<String>>,
|
||||
error: WriteSignal<Option<WolfError>>,
|
||||
) -> impl IntoView {
|
||||
let number: RwSignal<Option<NonZeroU8>> = RwSignal::new(None);
|
||||
let update = move |e: Targeted<Event, HtmlInputElement>| {
|
||||
|
|
@ -43,7 +41,7 @@ pub fn ChangePlayerNumber(
|
|||
ev.prevent_default();
|
||||
log::warn!("called submit with number: {:?}", number.get());
|
||||
let Some(num) = number.get() else {
|
||||
error.set(Some("please set a number".into()));
|
||||
error.set(Some(WolfError::NoSeatNumber));
|
||||
return;
|
||||
};
|
||||
submitted_number.set(Some(num));
|
||||
|
|
@ -52,7 +50,7 @@ pub fn ChangePlayerNumber(
|
|||
ev.prevent_default();
|
||||
log::warn!("called submit with number: {:?}", number.get());
|
||||
let Some(num) = number.get() else {
|
||||
error.set(Some("please set a number".into()));
|
||||
error.set(Some(WolfError::NoSeatNumber));
|
||||
return;
|
||||
};
|
||||
submitted_number.set(Some(num));
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use core::ops::Not;
|
|||
use leptos::{ev::MouseEvent, prelude::*};
|
||||
use leptos_router::hooks::use_url;
|
||||
use reactive_stores::Store;
|
||||
use uuid::Uuid;
|
||||
use werewolves_proto::{error::ServerError, game::GameId, token::TokenString};
|
||||
|
||||
use crate::{
|
||||
|
|
@ -113,6 +114,10 @@ pub fn Nav() -> impl IntoView {
|
|||
}
|
||||
})
|
||||
};
|
||||
move || {
|
||||
if is_big_screen_path(url.get().path()) {
|
||||
return ().into_any();
|
||||
}
|
||||
view! {
|
||||
<nav class="header">
|
||||
<Show when=move || auth.session().get().is_some() fallback=default_view>
|
||||
|
|
@ -120,4 +125,18 @@ pub fn Nav() -> impl IntoView {
|
|||
</Show>
|
||||
</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 crate::app::components::ErrorBox;
|
||||
use crate::app::error::WolfError;
|
||||
use crate::{
|
||||
ConsoleLogError,
|
||||
app::{
|
||||
|
|
@ -25,7 +26,7 @@ use crate::{
|
|||
};
|
||||
|
||||
#[component]
|
||||
pub fn GamePage(error: WriteSignal<Option<String>>) -> impl IntoView {
|
||||
pub fn GamePage(error: WriteSignal<Option<WolfError>>) -> impl IntoView {
|
||||
move || {
|
||||
let params = hooks::use_params_map();
|
||||
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_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]
|
||||
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::{ConsoleLogError, app::error::WolfError};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
enum HostPage {
|
||||
|
|
@ -24,7 +24,7 @@ enum HostPage {
|
|||
|
||||
#[component]
|
||||
pub fn HostGamePage(
|
||||
error: WriteSignal<Option<String>>,
|
||||
error: WriteSignal<Option<WolfError>>,
|
||||
message: Signal<Option<Srv2Host>>,
|
||||
reply: WriteSignal<Option<HostMessage>>,
|
||||
) -> impl IntoView {
|
||||
|
|
@ -68,14 +68,14 @@ pub fn HostGamePage(
|
|||
}
|
||||
}
|
||||
Srv2Host::GameCancelled => {
|
||||
error.set(Some("game was cancelled".into()));
|
||||
error.set(Some(WolfError::GameCancelled));
|
||||
gloo::utils::window()
|
||||
.location()
|
||||
.replace("/")
|
||||
.console_log_warn();
|
||||
}
|
||||
Srv2Host::Error(err) => {
|
||||
error.set(Some(err.to_string()));
|
||||
error.set(Some(err.into()));
|
||||
}
|
||||
Srv2Host::WaitingForRoleRevealAcks { ackd, waiting } => {
|
||||
let mut reveals = ackd
|
||||
|
|
|
|||
|
|
@ -12,13 +12,14 @@ use crate::app::{
|
|||
DialogModal, DialogMode, Equals, IdentificationInline, Sample, TutorialBox,
|
||||
input::ChangePlayerNumber,
|
||||
},
|
||||
error::WolfError,
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn HostPlayerList(
|
||||
players: ReadSignal<Box<[PlayerState]>>,
|
||||
reply: WriteSignal<Option<HostMessage>>,
|
||||
error: WriteSignal<Option<String>>,
|
||||
error: WriteSignal<Option<WolfError>>,
|
||||
) -> impl IntoView {
|
||||
let players = move || {
|
||||
let mut players = players.get();
|
||||
|
|
@ -88,7 +89,7 @@ pub fn HostPlayerList(
|
|||
fn HostPlayerDialogBody(
|
||||
player: PlayerState,
|
||||
reply: WriteSignal<Option<HostMessage>>,
|
||||
error: WriteSignal<Option<String>>,
|
||||
error: WriteSignal<Option<WolfError>>,
|
||||
) -> impl IntoView {
|
||||
let pid = player.identification.player_id;
|
||||
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 werewolves_proto::message::{CharacterIdentity, host::HostMessage};
|
||||
|
||||
use crate::app::components::IdentityInline;
|
||||
use crate::app::{components::IdentityInline, error::WolfError};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RoleRevealCharacter {
|
||||
|
|
@ -15,7 +15,7 @@ pub struct RoleRevealCharacter {
|
|||
pub fn RoleRevealAcks(
|
||||
reply: WriteSignal<Option<HostMessage>>,
|
||||
acks: ReadSignal<Box<[RoleRevealCharacter]>>,
|
||||
error: WriteSignal<Option<String>>,
|
||||
error: WriteSignal<Option<WolfError>>,
|
||||
) -> impl IntoView {
|
||||
let acks = move || {
|
||||
acks.get()
|
||||
|
|
@ -47,7 +47,11 @@ pub fn RoleRevealAcks(
|
|||
#[cfg(debug_assertions)]
|
||||
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::{
|
||||
class::{AsClasses, Class, PartialClass},
|
||||
components::{DialogModal, DialogMode, IdentityInline},
|
||||
error::WolfError,
|
||||
pages::game::host::HostPlayerList,
|
||||
};
|
||||
|
||||
|
|
@ -27,7 +28,7 @@ pub fn Settings(
|
|||
dialog_open: RwSignal<HashMap<SlotId, bool>>,
|
||||
open_categories: RwSignal<HashMap<Category, bool>>,
|
||||
reply: WriteSignal<Option<HostMessage>>,
|
||||
error: WriteSignal<Option<String>>,
|
||||
error: WriteSignal<Option<WolfError>>,
|
||||
) -> impl IntoView {
|
||||
let slots = move || {
|
||||
settings
|
||||
|
|
|
|||
|
|
@ -7,13 +7,17 @@ use convert_case::{Case, Casing};
|
|||
use leptos::prelude::*;
|
||||
use sorted_vec::SortedSet;
|
||||
use werewolves_proto::{
|
||||
error::ServerError,
|
||||
message::{
|
||||
ClientDeadChat, ClientMessage, ServerToClientMessage as Srv2Client, dead::DeadChatMessage,
|
||||
},
|
||||
role::RoleTitle,
|
||||
};
|
||||
|
||||
use crate::{ConsoleLogError, app::components::ErrorBox};
|
||||
use crate::{
|
||||
ConsoleLogError,
|
||||
app::{components::ErrorBox, error::WolfError},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Page {
|
||||
|
|
@ -31,7 +35,7 @@ enum Page {
|
|||
|
||||
#[component]
|
||||
pub fn PlayerGamePage(
|
||||
error: WriteSignal<Option<String>>,
|
||||
error: WriteSignal<Option<WolfError>>,
|
||||
message: Signal<Option<Srv2Client>>,
|
||||
reply: WriteSignal<Option<ClientMessage>>,
|
||||
disconnect: RwSignal<bool>,
|
||||
|
|
@ -44,7 +48,7 @@ pub fn PlayerGamePage(
|
|||
};
|
||||
match message {
|
||||
Srv2Client::GameCancelled => {
|
||||
error.set(Some("game was cancelled".into()));
|
||||
error.set(Some(WolfError::GameCancelled));
|
||||
gloo::utils::window()
|
||||
.location()
|
||||
.replace("/")
|
||||
|
|
@ -80,7 +84,7 @@ pub fn PlayerGamePage(
|
|||
#[cfg(feature = "hydrate")]
|
||||
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 {}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,12 +6,14 @@ use leptos::{
|
|||
};
|
||||
use werewolves_proto::message::{ClientMessage, UpdateSelf};
|
||||
|
||||
use crate::app::error::WolfError;
|
||||
|
||||
#[component]
|
||||
pub fn PlayerLobby(
|
||||
reply: WriteSignal<Option<ClientMessage>>,
|
||||
joined: ReadSignal<bool>,
|
||||
current_number: ReadSignal<Option<NonZeroU8>>,
|
||||
error: WriteSignal<Option<String>>,
|
||||
error: WriteSignal<Option<WolfError>>,
|
||||
) -> impl IntoView {
|
||||
let click = move |ev: MouseEvent| {
|
||||
ev.prevent_default();
|
||||
|
|
@ -61,7 +63,7 @@ pub fn PlayerLobby(
|
|||
let submit = move |ev: SubmitEvent| {
|
||||
ev.prevent_default();
|
||||
let Some(num) = number.get() else {
|
||||
error.set(Some("please set a number".into()));
|
||||
error.set(Some(WolfError::NoSeatNumber));
|
||||
return;
|
||||
};
|
||||
reply.set(Some(ClientMessage::UpdateSelf(UpdateSelf::Number(num))));
|
||||
|
|
|
|||
|
|
@ -2,16 +2,17 @@ use leptos::prelude::*;
|
|||
use reactive_stores::Store;
|
||||
|
||||
use crate::app::{
|
||||
error::WolfError,
|
||||
pages::{Signin, Signup},
|
||||
storage::user::{AuthContext, AuthContextStoreFields, UserSession},
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn Main() -> impl IntoView {
|
||||
pub fn Main(error: WriteSignal<Option<WolfError>>) -> impl IntoView {
|
||||
let auth = expect_context::<Store<AuthContext>>();
|
||||
move || match auth.session().get() {
|
||||
Some(session) => view! { <SignedInMain session=session /> }.into_any(),
|
||||
None => view! { <SignedOutMain /> }.into_any(),
|
||||
None => view! { <SignedOutMain error=error /> }.into_any(),
|
||||
}
|
||||
}
|
||||
#[component]
|
||||
|
|
@ -20,19 +21,19 @@ pub fn SignedInMain(session: UserSession) -> impl IntoView {
|
|||
}
|
||||
|
||||
#[component]
|
||||
pub fn SignedOutMain() -> impl IntoView {
|
||||
pub fn SignedOutMain(error: WriteSignal<Option<WolfError>>) -> impl IntoView {
|
||||
view! {
|
||||
<h1>"welcome"</h1>
|
||||
<div class="welcome-columns">
|
||||
<div>
|
||||
<h2>"create a new account"</h2>
|
||||
<h4>"with just a username"</h4>
|
||||
<Signup redirect=false header=false />
|
||||
<Signup error=error redirect=false header=false />
|
||||
</div>
|
||||
<div>
|
||||
<h2>"sign in"</h2>
|
||||
<h4>"with an existing account"</h4>
|
||||
<Signin redirect=false header=false />
|
||||
<Signin error=error redirect=false header=false />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
use core::ops::Not;
|
||||
|
||||
use chrono::Utc;
|
||||
use gloo::history::History;
|
||||
use leptos::{ev::MouseEvent, prelude::*};
|
||||
use werewolves_proto::player::{Password, Username};
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
components::ErrorBox,
|
||||
error::WolfError,
|
||||
storage::user::{AuthContext, UserToken},
|
||||
},
|
||||
auth::Signin,
|
||||
|
|
@ -16,12 +16,12 @@ use reactive_stores::Store;
|
|||
|
||||
#[component]
|
||||
pub fn Signin(
|
||||
error: WriteSignal<Option<WolfError>>,
|
||||
#[prop(default = true)] redirect: bool,
|
||||
#[prop(optional)] header: bool,
|
||||
) -> impl IntoView {
|
||||
let submit = ServerAction::<Signin>::new();
|
||||
use crate::app::storage::user::AuthContextStoreFields;
|
||||
let error: RwSignal<Option<String>> = RwSignal::new(None);
|
||||
|
||||
let auth = expect_context::<Store<AuthContext>>();
|
||||
|
||||
|
|
@ -51,22 +51,20 @@ pub fn Signin(
|
|||
let username = match Username::new(user.get()) {
|
||||
Ok(u) => u,
|
||||
Err(exp_range) => {
|
||||
error.set(Some(format!(
|
||||
"username must be between {} and {} characters",
|
||||
exp_range.start(),
|
||||
exp_range.end()
|
||||
)));
|
||||
error.set(Some(WolfError::UsernameLimits {
|
||||
min: *exp_range.start(),
|
||||
max: exp_range.max().unwrap_or_default(),
|
||||
}));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let password = match Password::new(pass.get()) {
|
||||
Ok(p) => p,
|
||||
Err(exp_range) => {
|
||||
error.set(Some(format!(
|
||||
"password must be between {} and {} characters",
|
||||
exp_range.start(),
|
||||
exp_range.end()
|
||||
)));
|
||||
error.set(Some(WolfError::PasswordLimits {
|
||||
min: *exp_range.start(),
|
||||
max: exp_range.max().unwrap_or_default(),
|
||||
}));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
|
@ -85,7 +83,7 @@ pub fn Signin(
|
|||
submit.clear();
|
||||
}
|
||||
}
|
||||
Some(Err(err)) => error.set(Some(err.to_string())),
|
||||
Some(Err(err)) => error.set(Some(err.into())),
|
||||
None => {}
|
||||
});
|
||||
|
||||
|
|
@ -122,7 +120,6 @@ pub fn Signin(
|
|||
<label for="password">"password"</label>
|
||||
{pass_input}
|
||||
{submit}
|
||||
<ErrorBox msg=error />
|
||||
</form>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,7 @@ use core::ops::Not;
|
|||
|
||||
use chrono::Utc;
|
||||
use gloo::history::History;
|
||||
use leptos::{
|
||||
ev::MouseEvent,
|
||||
prelude::*,
|
||||
};
|
||||
use leptos::{ev::MouseEvent, prelude::*};
|
||||
use rand::distr::SampleString;
|
||||
use reactive_stores::Store;
|
||||
use werewolves_proto::{
|
||||
|
|
@ -17,6 +14,7 @@ use werewolves_proto::{
|
|||
use crate::{
|
||||
app::{
|
||||
components::{ErrorBox, input::TextInput},
|
||||
error::WolfError,
|
||||
storage::user::{AuthContext, AuthContextStoreFields, PasswordlessUser, UserToken},
|
||||
},
|
||||
auth::Signin,
|
||||
|
|
@ -42,6 +40,7 @@ pub async fn create_user(
|
|||
|
||||
#[component]
|
||||
pub fn Signup(
|
||||
error: WriteSignal<Option<WolfError>>,
|
||||
#[prop(default = true)] redirect: bool,
|
||||
#[prop(default = true)] header: bool,
|
||||
) -> impl IntoView {
|
||||
|
|
@ -62,7 +61,6 @@ pub fn Signup(
|
|||
}
|
||||
|
||||
let submit = ServerAction::<CreateUser>::new();
|
||||
let error = RwSignal::new(None);
|
||||
let generated_password = Password::new(
|
||||
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()) {
|
||||
Ok(u) => u,
|
||||
Err(exp_range) => {
|
||||
error.set(Some(format!(
|
||||
"username must be between {} and {} characters",
|
||||
exp_range.start(),
|
||||
exp_range.end()
|
||||
)));
|
||||
error.set(Some(WolfError::UsernameLimits {
|
||||
min: *exp_range.start(),
|
||||
max: *exp_range.end(),
|
||||
}));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
|
@ -117,7 +114,7 @@ pub fn Signup(
|
|||
})
|
||||
}
|
||||
Err(err) => {
|
||||
error.write().replace(err.to_string());
|
||||
error.write().replace(err.into());
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
@ -131,7 +128,7 @@ pub fn Signup(
|
|||
auth.session().set(Some(sess.into()));
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
error.set(Some(format!("error signing in after signup: {err}")));
|
||||
error.set(Some(err.into()));
|
||||
}
|
||||
None => {}
|
||||
});
|
||||
|
|
@ -147,7 +144,6 @@ pub fn Signup(
|
|||
view! { <button on:click=click.clone()>"sign up"</button> }
|
||||
}}
|
||||
{result}
|
||||
<ErrorBox msg=error />
|
||||
</form>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@ use reactive_stores::Store;
|
|||
|
||||
use crate::app::{
|
||||
Preferences,
|
||||
error::WolfError,
|
||||
storage::user::{AuthContext, AuthContextStoreFields},
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn UserSettings() -> impl IntoView {
|
||||
pub fn UserSettings(error: WriteSignal<Option<WolfError>>) -> impl IntoView {
|
||||
let auth = expect_context::<Store<AuthContext>>();
|
||||
let (prefs_read, prefs_write) =
|
||||
expect_context::<(Signal<Preferences>, WriteSignal<Preferences>)>();
|
||||
|
|
@ -51,10 +52,10 @@ pub fn UserSettings() -> impl IntoView {
|
|||
view! {
|
||||
<ul class="user-settings-list">
|
||||
<li>
|
||||
<UpdateProfileButton />
|
||||
<UpdateProfileButton error=error />
|
||||
</li>
|
||||
<li>
|
||||
<ChangePasswordButton />
|
||||
<ChangePasswordButton error=error />
|
||||
</li>
|
||||
<li>{tutorial_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::{
|
||||
app::{
|
||||
components::{DialogModal, ErrorBox},
|
||||
error::WolfError,
|
||||
storage::user::{AuthContext, AuthContextStoreFields},
|
||||
},
|
||||
db::AppState,
|
||||
|
|
@ -24,9 +25,8 @@ pub async fn change_password(
|
|||
}
|
||||
|
||||
#[component]
|
||||
pub fn ChangePasswordButton() -> impl IntoView {
|
||||
pub fn ChangePasswordButton(error: WriteSignal<Option<WolfError>>) -> impl IntoView {
|
||||
let auth = expect_context::<Store<AuthContext>>();
|
||||
let error = RwSignal::new(None);
|
||||
let action = ServerAction::<ChangePassword>::new();
|
||||
let current = RwSignal::new(String::new());
|
||||
Effect::new(move || {
|
||||
|
|
@ -42,26 +42,24 @@ pub fn ChangePasswordButton() -> impl IntoView {
|
|||
let current = match Password::new(current.get()) {
|
||||
Ok(c) => c,
|
||||
Err(exp_range) => {
|
||||
error.set(Some(format!(
|
||||
"current password must be between {} and {} characters",
|
||||
exp_range.start(),
|
||||
exp_range.end()
|
||||
)));
|
||||
error.set(Some(WolfError::CurrentPasswordLimits {
|
||||
min: *exp_range.start(),
|
||||
max: *exp_range.end(),
|
||||
}));
|
||||
return;
|
||||
}
|
||||
};
|
||||
if new.get() != new_confirm.get() {
|
||||
error.set(Some("password confirmation does not match".into()));
|
||||
error.set(Some(WolfError::PasswordConfirmNoMatch));
|
||||
return;
|
||||
}
|
||||
let new = match Password::new(new.get()) {
|
||||
Ok(c) => c,
|
||||
Err(exp_range) => {
|
||||
error.set(Some(format!(
|
||||
"new password must be between {} and {} characters",
|
||||
exp_range.start(),
|
||||
exp_range.end()
|
||||
)));
|
||||
error.set(Some(WolfError::PasswordLimits {
|
||||
min: *exp_range.start(),
|
||||
max: *exp_range.end(),
|
||||
}));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
|
@ -87,7 +85,7 @@ pub fn ChangePasswordButton() -> impl IntoView {
|
|||
auth.passwordless().set(None);
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
error.set(Some(err.to_string()));
|
||||
error.set(Some(err.into()));
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
|
@ -110,7 +108,6 @@ pub fn ChangePasswordButton() -> impl IntoView {
|
|||
|
||||
view! {
|
||||
<DialogModal button_content="change password" close_backdrop=false>
|
||||
<ErrorBox msg=error />
|
||||
<form>
|
||||
<div class="form-fields">
|
||||
<label for="current-password">"current password"</label>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use werewolves_proto::{
|
|||
use crate::{
|
||||
app::{
|
||||
components::{DialogModal, ErrorBox},
|
||||
error::WolfError,
|
||||
storage::user::{AuthContext, AuthContextStoreFields},
|
||||
},
|
||||
db::AppState,
|
||||
|
|
@ -27,9 +28,8 @@ fn trim_or_none(s: String) -> Option<String> {
|
|||
}
|
||||
|
||||
#[component]
|
||||
pub fn UpdateProfileButton() -> impl IntoView {
|
||||
pub fn UpdateProfileButton(error: WriteSignal<Option<WolfError>>) -> impl IntoView {
|
||||
let auth = expect_context::<Store<AuthContext>>();
|
||||
let error = RwSignal::new(None);
|
||||
let display_name = RwSignal::new(String::new());
|
||||
let pronouns = RwSignal::new(String::new());
|
||||
Effect::new(move || {
|
||||
|
|
@ -69,7 +69,7 @@ pub fn UpdateProfileButton() -> impl IntoView {
|
|||
sess.pronouns = input.update.pronouns;
|
||||
}
|
||||
}
|
||||
Some(Err(err)) => error.set(Some(err.to_string())),
|
||||
Some(Err(err)) => error.set(Some(err.into())),
|
||||
None => {}
|
||||
});
|
||||
let on_success = move || {
|
||||
|
|
@ -82,7 +82,6 @@ pub fn UpdateProfileButton() -> impl IntoView {
|
|||
};
|
||||
view! {
|
||||
<DialogModal button_content="update profile" close_backdrop=false>
|
||||
<ErrorBox msg=error />
|
||||
<form>
|
||||
<label for="display-name">"display name"</label>
|
||||
<input
|
||||
|
|
|
|||
Loading…
Reference in New Issue