diff --git a/app/controllers/Swiss.scala b/app/controllers/Swiss.scala
index 0ea4d9c414..077821c71c 100644
--- a/app/controllers/Swiss.scala
+++ b/app/controllers/Swiss.scala
@@ -29,6 +29,7 @@ final class Swiss(
me = ctx.me,
reqPage = page,
socketVersion = version.some,
+ playerInfo = none,
isInTeam = isInTeam
)
canChat <- canHaveChat(swiss)
@@ -45,11 +46,13 @@ final class Swiss(
for {
socketVersion <- getBool("socketVersion").??(env.swiss version swiss.id dmap some)
isInTeam <- isCtxInTheTeam(swiss.teamId)
+ playerInfo <- get("playerInfo").?? { env.swiss.api.playerInfo(swiss, _) }
json <- env.swiss.json(
swiss = swiss,
me = ctx.me,
reqPage = page,
socketVersion = socketVersion,
+ playerInfo = playerInfo,
isInTeam = isInTeam
)
} yield Ok(json)
@@ -133,7 +136,17 @@ final class Swiss(
}
}
- private def WithSwiss(id: String)(f: SwissModel => Fu[Result])(implicit ctx: Context): Fu[Result] =
+ def player(id: String, userId: String) = Action.async {
+ WithSwiss(id) { swiss =>
+ env.swiss.api.playerInfo(swiss, userId) flatMap {
+ _.fold(notFoundJson()) { player =>
+ JsonOk(fuccess(lila.swiss.SwissJson.playerJsonExt(swiss, player)))
+ }
+ }
+ }
+ }
+
+ private def WithSwiss(id: String)(f: SwissModel => Fu[Result]): Fu[Result] =
env.swiss.api.byId(SwissId(id)) flatMap { _ ?? f }
private def WithEditableSwiss(id: String, me: lila.user.User)(
diff --git a/app/controllers/Tournament.scala b/app/controllers/Tournament.scala
index 791492ee2b..aa5e35b3e6 100644
--- a/app/controllers/Tournament.scala
+++ b/app/controllers/Tournament.scala
@@ -152,7 +152,7 @@ final class Tournament(
}
}
- def player(tourId: String, userId: String) = Open { _ =>
+ def player(tourId: String, userId: String) = Action.async {
env.tournament.tournamentRepo byId tourId flatMap {
_ ?? { tour =>
JsonOk {
diff --git a/app/views/swiss/bits.scala b/app/views/swiss/bits.scala
index 33a3ca54da..43ae1595b3 100644
--- a/app/views/swiss/bits.scala
+++ b/app/views/swiss/bits.scala
@@ -77,6 +77,11 @@ object bits {
trans.withdraw,
trans.youArePlaying,
trans.joinTheGame,
- trans.signIn
+ trans.signIn,
+ trans.averageElo,
+ trans.gamesPlayed,
+ trans.winRate,
+ trans.performance,
+ trans.averageOpponent
).map(_.key)
}
diff --git a/app/views/tournament/bits.scala b/app/views/tournament/bits.scala
index c79a5da2ed..eb4b5469ec 100644
--- a/app/views/tournament/bits.scala
+++ b/app/views/tournament/bits.scala
@@ -108,7 +108,6 @@ object bits {
trans.blackWins,
trans.draws,
trans.nextXTournament,
- trans.viewMoreTournaments,
trans.averageOpponent,
trans.ratedTournament,
trans.casualTournament
diff --git a/conf/logback.xml b/conf/logback.xml
index 32ddf2390c..692a5ee331 100644
--- a/conf/logback.xml
+++ b/conf/logback.xml
@@ -48,6 +48,12 @@
%date [%level] %message%n%xException
+
+
+ ${application.home}/logs/swiss.log
+ %date [%level] %message%n%xException
+
+
${application.home}/logs/relay.log
diff --git a/conf/prod-logger.xml b/conf/prod-logger.xml
index 27ae190276..b78f6c1d3d 100644
--- a/conf/prod-logger.xml
+++ b/conf/prod-logger.xml
@@ -64,7 +64,7 @@
-
+
/var/log/lichess/puzzle.log
%date %-5level %logger{30} %message%n%xException
@@ -74,7 +74,17 @@
-
+
+
+ /var/log/lichess/swiss.log
+ %date %-5level %logger{30} %message%n%xException
+
+ /var/log/lichess/swiss-log-%d{yyyy-MM-dd}.gz
+ 7
+
+
+
+
/var/log/lichess/relay.log
%date %-5level %logger{30} %message%n%xException
@@ -84,7 +94,7 @@
-
+
/var/log/lichess/lobby.log
%date %-5level %logger{30} %message%n%xException
@@ -94,7 +104,7 @@
-
+
/var/log/lichess/pool.log
%date %-5level %logger{30} %message%n%xException
@@ -104,7 +114,7 @@
-
+
/var/log/lichess/tournament.log
%date %-5level %logger{30} %message%n%xException
@@ -114,7 +124,7 @@
-
+
/var/log/lichess/ratelimit.log
%date %-5level %logger{30} %message%n%xException
@@ -134,7 +144,7 @@
-
+
/var/log/lichess/cheat.log
%date %-5level %logger{30} %message%n%xException
@@ -144,7 +154,7 @@
-
+
/var/log/lichess/fishnet.log
%date %-5level %logger{30} %message%n%xException
@@ -164,7 +174,7 @@
-
+
/var/log/lichess/csrf.log
%date %-5level %logger{30} %message%n%xException
@@ -174,7 +184,7 @@
-
+
/var/log/lichess/http.log
%date %-5level %logger{30} %message%n%xException
@@ -184,7 +194,7 @@
-
+
/var/log/lichess/auth.log
%date %-5level %logger{30} %message%n%xException
@@ -194,7 +204,7 @@
-
+
/var/log/lichess/report.log
%date %-5level %logger{30} %message%n%xException
@@ -204,7 +214,7 @@
-
+
/var/log/lichess/sandbag.log
%date %-5level %logger{30} %message%n%xException
@@ -214,7 +224,7 @@
-
+
/var/log/lichess/security.log
%date %-5level %logger{30} %message%n%xException
diff --git a/conf/routes b/conf/routes
index c30d7e3be6..bce3b93a6e 100644
--- a/conf/routes
+++ b/conf/routes
@@ -253,6 +253,7 @@ GET /swiss/$id<\w{8}>/edit controllers.Swiss.edit(id: String)
POST /swiss/$id<\w{8}>/edit controllers.Swiss.update(id: String)
POST /swiss/$id<\w{8}>/terminate controllers.Swiss.terminate(id: String)
GET /swiss/$id<\w{8}>/standing/:page controllers.Swiss.standing(id: String, page: Int)
+GET /swiss/$id<\w{8}>/player/:user controllers.Swiss.player(id: String, user: String)
# Simul
GET /simul controllers.Simul.home
diff --git a/modules/i18n/src/main/I18nKeys.scala b/modules/i18n/src/main/I18nKeys.scala
index 21375c335e..2aa61317fe 100644
--- a/modules/i18n/src/main/I18nKeys.scala
+++ b/modules/i18n/src/main/I18nKeys.scala
@@ -317,7 +317,6 @@ val `whiteWins` = new I18nKey("whiteWins")
val `blackWins` = new I18nKey("blackWins")
val `draws` = new I18nKey("draws")
val `nextXTournament` = new I18nKey("nextXTournament")
-val `viewMoreTournaments` = new I18nKey("viewMoreTournaments")
val `averageOpponent` = new I18nKey("averageOpponent")
val `membersOnly` = new I18nKey("membersOnly")
val `boardEditor` = new I18nKey("boardEditor")
diff --git a/modules/swiss/src/main/Env.scala b/modules/swiss/src/main/Env.scala
index 9de3fbcbb7..259962d700 100644
--- a/modules/swiss/src/main/Env.scala
+++ b/modules/swiss/src/main/Env.scala
@@ -13,6 +13,7 @@ final class Env(
appConfig: Configuration,
db: lila.db.Db,
gameRepo: lila.game.GameRepo,
+ userRepo: lila.user.UserRepo,
onStart: lila.round.OnStart,
remoteSocketApi: lila.socket.RemoteSocket,
chatApi: lila.chat.ChatApi,
diff --git a/modules/swiss/src/main/SwissApi.scala b/modules/swiss/src/main/SwissApi.scala
index 846d577d1f..f43a81150d 100644
--- a/modules/swiss/src/main/SwissApi.scala
+++ b/modules/swiss/src/main/SwissApi.scala
@@ -7,19 +7,21 @@ import reactivemongo.api.bson._
import scala.concurrent.duration._
import lila.chat.Chat
-import lila.common.{ Bus, GreatPlayer, WorkQueues }
+import lila.common.{ Bus, GreatPlayer, LightUser, WorkQueues }
import lila.db.dsl._
import lila.game.Game
import lila.hub.LightTeam.TeamID
import lila.round.actorApi.round.QuietFlag
-import lila.user.User
+import lila.user.{ User, UserRepo }
final class SwissApi(
colls: SwissColls,
+ userRepo: UserRepo,
socket: SwissSocket,
director: SwissDirector,
scoring: SwissScoring,
- chatApi: lila.chat.ChatApi
+ chatApi: lila.chat.ChatApi,
+ lightUserApi: lila.user.LightUserApi
)(
implicit ec: scala.concurrent.ExecutionContext,
mat: akka.stream.Materializer,
@@ -96,6 +98,55 @@ final class SwissApi(
def featuredInTeam(teamId: TeamID): Fu[List[Swiss]] =
colls.swiss.ext.find($doc("teamId" -> teamId)).sort($sort desc "startsAt").list[Swiss](5)
+ def playerInfo(swiss: Swiss, userId: User.ID): Fu[Option[SwissPlayer.ViewExt]] =
+ userRepo named userId flatMap {
+ _ ?? { user =>
+ colls.player.byId[SwissPlayer](SwissPlayer.makeId(swiss.id, user.id).value) flatMap {
+ _ ?? { player =>
+ SwissPairing.fields { f =>
+ colls.pairing.ext
+ .find($doc(f.swissId -> swiss.id, f.players -> player.number))
+ .sort($sort desc f.date)
+ .list[SwissPairing]()
+ } flatMap {
+ pairingViews(_, player)
+ } flatMap { pairings =>
+ SwissPlayer.fields { f =>
+ colls.player.countSel($doc(f.swissId -> swiss.id, f.score $gt player.score)).dmap(1.+)
+ } map { rank =>
+ SwissPlayer
+ .ViewExt(player, rank, user.light, pairings.view.map { p =>
+ p.pairing.round -> p
+ }.toMap)
+ .some
+ }
+ }
+ }
+ }
+ }
+ }
+
+ def pairingViews(pairings: Seq[SwissPairing], player: SwissPlayer): Fu[Seq[SwissPairing.View]] =
+ pairings.headOption ?? { first =>
+ SwissPlayer.fields { f =>
+ colls.player.ext
+ .find($doc(f.swissId -> first.swissId, f.number $in pairings.map(_ opponentOf player.number)))
+ .list[SwissPlayer]()
+ } flatMap { opponents =>
+ lightUserApi asyncMany opponents.map(_.userId) map { users =>
+ opponents.zip(users) map {
+ case (o, u) => SwissPlayer.WithUser(o, u | LightUser.fallback(o.userId))
+ }
+ } map { opponents =>
+ pairings flatMap { pairing =>
+ opponents.find(_.player.number == pairing.opponentOf(player.number)) map {
+ SwissPairing.View(pairing, _)
+ }
+ }
+ }
+ }
+ }
+
private[swiss] def finishGame(game: Game): Funit = game.swissId ?? { swissId =>
Sequencing(Swiss.Id(swissId))(startedById) { swiss =>
colls.pairing.byId[SwissPairing](game.id).dmap(_.filter(_.isOngoing)) flatMap {
@@ -186,12 +237,15 @@ final class SwissApi(
swiss.id,
s"Not enough players for round ${swiss.round.value + 1}; terminating tournament."
)
- ) { s =>
- scoring.recompute(s) >>-
- systemChat(
- swiss.id,
- s"Round ${swiss.round.value + 1} started."
- )
+ ) {
+ case s if s.nextRoundAt.isEmpty =>
+ scoring.recompute(s) >>-
+ systemChat(swiss.id, s"Round ${swiss.round.value + 1} started.")
+ case s =>
+ colls.swiss.update
+ .one($id(swiss.id), $set("nextRoundAt" -> DateTime.now.plusSeconds(61)))
+ .void >>-
+ systemChat(swiss.id, s"Round ${swiss.round.value + 1} failed.", true)
}
} else {
if (swiss.startsAt isBefore DateTime.now.minusMinutes(60)) destroy(swiss)
diff --git a/modules/swiss/src/main/SwissJson.scala b/modules/swiss/src/main/SwissJson.scala
index 5a4d79be90..5ad3c7b268 100644
--- a/modules/swiss/src/main/SwissJson.scala
+++ b/modules/swiss/src/main/SwissJson.scala
@@ -29,7 +29,8 @@ final class SwissJson(
me: Option[User],
reqPage: Option[Int], // None = focus on me
socketVersion: Option[SocketVersion],
- isInTeam: Boolean
+ isInTeam: Boolean,
+ playerInfo: Option[SwissPlayer.ViewExt]
)(implicit lang: Lang): Fu[JsObject] =
for {
myInfo <- me.?? { fetchMyInfo(swiss, _) }
@@ -69,19 +70,23 @@ final class SwissJson(
.add("greatPlayer" -> GreatPlayer.wikiUrl(swiss.name).map { url =>
Json.obj("name" -> swiss.name, "url" -> url)
})
+ .add("playerInfo" -> playerInfo.map { playerJsonExt(swiss, _) })
def fetchMyInfo(swiss: Swiss, me: User): Fu[Option[MyInfo]] =
colls.player.byId[SwissPlayer](SwissPlayer.makeId(swiss.id, me.id).value) flatMap {
_ ?? { player =>
SwissPairing.fields { f =>
- colls.pairing
- .find(
- $doc(f.swissId -> swiss.id, f.players -> player.number, f.status -> SwissPairing.ongoing),
- $doc(f.id -> true).some
- )
- .sort($sort desc f.date)
- .one[Bdoc]
- .dmap { _.flatMap(_.getAsOpt[Game.ID](f.id)) }
+ (swiss.nbOngoing > 0)
+ .?? {
+ colls.pairing
+ .find(
+ $doc(f.swissId -> swiss.id, f.players -> player.number, f.status -> SwissPairing.ongoing),
+ $doc(f.id -> true).some
+ )
+ .sort($sort desc f.date)
+ .one[Bdoc]
+ .dmap { _.flatMap(_.getAsOpt[Game.ID](f.id)) }
+ }
.flatMap { gameId =>
getOrGuessRank(swiss, player) dmap { rank =>
MyInfo(rank + 1, gameId, me).some
@@ -117,32 +122,46 @@ final class SwissJson(
object SwissJson {
- private[swiss] def playerJson(
- swiss: Swiss,
- rankedPlayer: SwissPlayer.Ranked,
- user: lila.common.LightUser,
- pairings: Map[SwissRound.Number, SwissPairing]
- ): JsObject = {
- val p = rankedPlayer.player
+ private[swiss] def playerJson(swiss: Swiss, view: SwissPlayer.View): JsObject =
+ playerJsonBase(swiss, view) ++ Json.obj(
+ "pairings" -> swiss.allRounds.map(view.pairings.get).map(_ map pairingJson(view.player))
+ )
+
+ def playerJsonExt(swiss: Swiss, view: SwissPlayer.ViewExt): JsObject =
+ playerJsonBase(swiss, view) ++ Json.obj(
+ "pairings" -> swiss.allRounds.map(view.pairings.get).map {
+ _ map { p =>
+ pairingJson(view.player)(p.pairing) ++ Json.obj(
+ "user" -> p.player.user,
+ "rating" -> p.player.player.rating
+ )
+ }
+ }
+ )
+
+ private def playerJsonBase(swiss: Swiss, view: SwissPlayer.Viewish): JsObject = {
+ val p = view.player
Json
.obj(
- "rank" -> rankedPlayer.rank,
- "user" -> user,
+ "rank" -> view.rank,
+ "user" -> view.user,
"rating" -> p.rating,
"points" -> p.points,
- "tieBreak" -> p.tieBreak,
- "pairings" -> swiss.allRounds.map(pairings.get).map {
- _ map { pairing =>
- Json
- .obj("g" -> pairing.gameId)
- .add("o" -> pairing.isOngoing)
- .add("w" -> pairing.resultFor(p.number))
- }
- }
+ "tieBreak" -> p.tieBreak
)
+ .add("performance" -> p.performance)
.add("provisional" -> p.provisional)
}
+ private def pairingJson(player: SwissPlayer)(pairing: SwissPairing) =
+ Json
+ .obj(
+ "g" -> pairing.gameId,
+ "c" -> (pairing.white == player.number)
+ )
+ .add("o" -> pairing.isOngoing)
+ .add("w" -> pairing.resultFor(player.number))
+
implicit private val roundNumberWriter: Writes[SwissRound.Number] = Writes[SwissRound.Number] { n =>
JsNumber(n.value)
}
@@ -155,6 +174,9 @@ object SwissJson {
implicit private val tieBreakWriter: Writes[Swiss.TieBreak] = Writes[Swiss.TieBreak] { t =>
JsNumber(t.value)
}
+ implicit private val performanceWriter: Writes[Swiss.Performance] = Writes[Swiss.Performance] { t =>
+ JsNumber(t.value.toInt)
+ }
implicit private val clockWrites: OWrites[chess.Clock.Config] = OWrites { clock =>
Json.obj(
diff --git a/modules/swiss/src/main/SwissPairing.scala b/modules/swiss/src/main/SwissPairing.scala
index 897ce1b1ab..8f4d288a43 100644
--- a/modules/swiss/src/main/SwissPairing.scala
+++ b/modules/swiss/src/main/SwissPairing.scala
@@ -38,6 +38,8 @@ object SwissPairing {
type PairingMap = Map[SwissPlayer.Number, Map[SwissRound.Number, SwissPairing]]
+ case class View(pairing: SwissPairing, player: SwissPlayer.WithUser)
+
object Fields {
val id = "_id"
val swissId = "s"
diff --git a/modules/swiss/src/main/SwissPlayer.scala b/modules/swiss/src/main/SwissPlayer.scala
index aaa4b03126..292de3cd49 100644
--- a/modules/swiss/src/main/SwissPlayer.scala
+++ b/modules/swiss/src/main/SwissPlayer.scala
@@ -1,5 +1,6 @@
package lila.swiss
+import lila.common.LightUser
import lila.rating.Perf
import lila.user.{ Perfs, User }
@@ -56,6 +57,28 @@ object SwissPlayer {
override def toString = s"$rank. ${player.userId}[${player.rating}]"
}
+ case class WithUser(player: SwissPlayer, user: LightUser)
+
+ sealed trait Viewish {
+ val player: SwissPlayer
+ val rank: Int
+ val user: lila.common.LightUser
+ }
+
+ case class View(
+ player: SwissPlayer,
+ rank: Int,
+ user: lila.common.LightUser,
+ pairings: Map[SwissRound.Number, SwissPairing]
+ ) extends Viewish
+
+ case class ViewExt(
+ player: SwissPlayer,
+ rank: Int,
+ user: lila.common.LightUser,
+ pairings: Map[SwissRound.Number, SwissPairing.View]
+ ) extends Viewish
+
def toMap(players: List[SwissPlayer]): Map[SwissPlayer.Number, SwissPlayer] =
players.view.map(p => p.number -> p).toMap
diff --git a/modules/swiss/src/main/SwissStandingApi.scala b/modules/swiss/src/main/SwissStandingApi.scala
index 0c95afb929..8d5dbcfdc9 100644
--- a/modules/swiss/src/main/SwissStandingApi.scala
+++ b/modules/swiss/src/main/SwissStandingApi.scala
@@ -83,12 +83,15 @@ final class SwissStandingApi(
} yield Json.obj(
"page" -> page,
"players" -> rankedPlayers.zip(users).map {
- case (p, u) =>
+ case (SwissPlayer.Ranked(rank, player), user) =>
SwissJson.playerJson(
swiss,
- p,
- u | LightUser.fallback(p.player.userId),
- ~pairings.get(p.player.number)
+ SwissPlayer.View(
+ player,
+ rank,
+ user | LightUser.fallback(player.userId),
+ ~pairings.get(player.number)
+ )
)
}
)
diff --git a/translation/source/site.xml b/translation/source/site.xml
index aad485a59b..e57ed9657f 100644
--- a/translation/source/site.xml
+++ b/translation/source/site.xml
@@ -426,7 +426,6 @@ computer analysis, game chat and shareable URL.
Black wins
Draws
Next %s tournament:
- View more tournaments
Average opponent
Members only
Board editor
diff --git a/ui/common/css/component/_crosstable.scss b/ui/common/css/component/_crosstable.scss
index c4b48a71db..9edb1f1019 100644
--- a/ui/common/css/component/_crosstable.scss
+++ b/ui/common/css/component/_crosstable.scss
@@ -59,7 +59,7 @@
&.current a {
background: mix($c-accent, $c-bg-box, 70%);
color: #fff;
- opacity: 1;
+ opacity: 1!important;
}
&.new {
border: $c-border;
diff --git a/ui/swiss/css/_layout.scss b/ui/swiss/css/_layout.scss
index 4e1bb4b8a8..ade381868e 100644
--- a/ui/swiss/css/_layout.scss
+++ b/ui/swiss/css/_layout.scss
@@ -11,29 +11,32 @@ $chat-optimal-size: calc(100vh - #{$site-header-outer-height} - #{$block-gap} -
display: grid;
&__side { grid-area: side }
+ &__table { grid-area: table }
&__main { grid-area: main }
.chat__members { grid-area: uchat; }
grid-template-areas:
'main'
'side'
- 'uchat';
+ 'uchat'
+ 'table';
grid-gap: $block-gap;
@include breakpoint($mq-col2) {
- grid-template-columns: $col2-uniboard-default-width;
+ grid-template-columns: $col2-uniboard-default-width $col2-uniboard-table;
grid-template-rows: $chat-optimal-size min-content;
grid-template-areas:
'main side'
- 'main uchat';
+ 'main uchat'
+ 'table table';
.mchat__messages {
max-height: inherit;
}
}
@include breakpoint($mq-col3) {
- grid-template-columns: $col3-uniboard-side $col3-uniboard-default-width;
+ grid-template-columns: $col3-uniboard-side $col3-uniboard-default-width $col3-uniboard-table;
grid-template-rows: $chat-optimal-size auto;
grid-template-areas:
'side main table'
diff --git a/ui/swiss/css/_player-info.scss b/ui/swiss/css/_player-info.scss
new file mode 100644
index 0000000000..9ce48cf415
--- /dev/null
+++ b/ui/swiss/css/_player-info.scss
@@ -0,0 +1,123 @@
+.swiss__player-info {
+ @extend %box-neat-force;
+ background: $c-bg-box;
+ position: relative;
+ align-self: flex-start;
+
+ .spinner {
+ margin: 5em auto;
+ }
+
+ .close {
+ position: absolute;
+ top: 4px;
+ right: 5px;
+ opacity: .6;
+ @include transition();
+ color: $c-red;
+ &:hover {
+ opacity: 1;
+ }
+ }
+
+ .stats {
+ @extend %flex-column;
+ justify-content: center;
+ h2 {
+ @extend %metal;
+ font-size: 1.4em;
+ padding: .6rem 1rem;
+ border-bottom: $border;
+ }
+ table {
+ margin: 1em auto;
+ }
+ td {
+ font-weight: bold;
+ padding-left: 10px;
+ text-align: right;
+ line-height: 1.8em;
+ &:last-child {
+ @extend %roboto;
+ }
+ }
+ }
+
+ .sublist {
+ width: 100%;
+ tr {
+ cursor: pointer;
+ @include transition(background-color);
+ &:nth-child(odd) {
+ background: $c-bg-zebra;
+ }
+ &:hover {
+ background: mix($c-link, $c-bg-box, 10%);
+ }
+ }
+ th, td {
+ padding: .3em;
+ }
+ th {
+ @extend %roboto;
+ padding-left: 7px;
+ }
+ .title {
+ color: $c-brag;
+ font-weight: bold;
+ }
+ }
+
+ .pairings {
+ width: 100%;
+ tr {
+ cursor: pointer;
+ @include transition(background-color);
+ &:nth-child(odd) {
+ background: $c-bg-zebra;
+ }
+ &:hover {
+ background: mix($c-link, $c-bg-box, 10%);
+ }
+ }
+ th, td {
+ padding: .3em;
+ }
+ th {
+ border-left: 3px solid transparent;
+ @include transition();
+ }
+ tr:hover th {
+ border-color: $c-font-dimmer;
+ }
+ tr.win:hover th {
+ border-color: $c-good;
+ }
+ tr.loss:hover th {
+ border-color: $c-bad;
+ }
+ td:nth-child(2) {
+ @extend %nowrap-ellipsis;
+ max-width: 200px;
+ }
+ td:last-child {
+ font-weight: bold;
+ opacity: .8;
+ }
+ tr.win td:last-child {
+ color: $c-good;
+ opacity: 1;
+ }
+ tr.loss td:last-child {
+ color: $c-bad;
+ opacity: 1;
+ }
+ .bye {
+ font-style: italic;
+ color: $c-font-dim;
+ }
+ }
+ .color-icon {
+ opacity: .6;
+ }
+}
diff --git a/ui/swiss/css/_show.scss b/ui/swiss/css/_show.scss
index ada5b38a4b..eb92efbcc0 100644
--- a/ui/swiss/css/_show.scss
+++ b/ui/swiss/css/_show.scss
@@ -13,7 +13,7 @@ $mq-col3: $mq-col3-uniboard;
// @import 'stats';
// @import 'duel';
// @import 'actor-info';
-// @import 'player-info';
+@import 'player-info';
// @import 'team-info';
.swiss {
diff --git a/ui/swiss/css/build/_swiss.show.scss b/ui/swiss/css/build/_swiss.show.scss
index 7147521e36..bf7cddb2c1 100644
--- a/ui/swiss/css/build/_swiss.show.scss
+++ b/ui/swiss/css/build/_swiss.show.scss
@@ -3,5 +3,6 @@
@import '../../../common/css/component/bar-glider';
@import '../../../common/css/component/slist';
@import '../../../common/css/component/quote';
+@import '../../../common/css/component/color-icon';
@import '../../../chat/css/chat';
@import '../show';
diff --git a/ui/swiss/src/ctrl.ts b/ui/swiss/src/ctrl.ts
index c3521856f7..10c05c28d6 100644
--- a/ui/swiss/src/ctrl.ts
+++ b/ui/swiss/src/ctrl.ts
@@ -15,6 +15,7 @@ export default class SwissCtrl {
lastPageDisplayed: number | undefined;
focusOnMe: boolean;
joinSpinner: boolean = false;
+ playerInfoId?: string;
disableClicks: boolean = true;
searching: boolean = false;
redraw: () => void;
@@ -41,8 +42,6 @@ export default class SwissCtrl {
this.data = {...this.data, ...data};
this.data.me = data.me; // to account for removal on withdraw
this.data.nextRound = data.nextRound; // to account for removal
- // if (data.playerInfo && data.playerInfo.player.id === this.playerInfo.id)
- // this.playerInfo.data = data.playerInfo;
this.loadPage(data.standing);
if (this.focusOnMe) this.scrollToMe();
// if (data.featured) this.startWatching(data.featured.id);
@@ -121,6 +120,8 @@ export default class SwissCtrl {
userLastPage = () => this.userSetPage(players(this).nbPages);
showPlayerInfo = (player: Player) => {
+ this.playerInfoId = this.playerInfoId === player.user.id ? undefined : player.user.id;
+ if (this.playerInfoId) xhr.playerInfo(this, this.playerInfoId);
};
askReload = () => xhr.reloadNow(this);
diff --git a/ui/swiss/src/interfaces.ts b/ui/swiss/src/interfaces.ts
index 2b4617c3f4..50f2eaeaef 100644
--- a/ui/swiss/src/interfaces.ts
+++ b/ui/swiss/src/interfaces.ts
@@ -31,6 +31,7 @@ export interface SwissData {
nbOngoing: number;
status: Status;
standing: Standing;
+ playerInfo?: PlayerExt;
isStarted?: boolean;
isFinished?: boolean;
socketVersion?: number;
@@ -60,9 +61,14 @@ export interface MyInfo {
export interface Pairing {
g: string; // game
+ c: boolean; // color
w?: boolean; // won
o?: boolean; // ongoing
}
+export interface PairingExt extends Pairing {
+ user: LightUser;
+ rating: number;
+}
export interface Standing {
page: number;
@@ -77,6 +83,7 @@ export interface Player {
withdraw?: boolean;
points: number;
tieBreak: number;
+ performance: number;
rank: number;
pairings: [Pairing | null];
}
@@ -96,3 +103,7 @@ export type Page = Player[];
export interface Pages {
[n: number]: Page
}
+
+export interface PlayerExt extends Player {
+ pairings: [PairingExt | null];
+}
diff --git a/ui/swiss/src/view/main.ts b/ui/swiss/src/view/main.ts
index 8b1f0fc39f..0469ff41cb 100644
--- a/ui/swiss/src/view/main.ts
+++ b/ui/swiss/src/view/main.ts
@@ -6,6 +6,7 @@ import * as pagination from '../pagination';
import { MaybeVNodes } from '../interfaces';
import header from './header';
import standing from './standing';
+import playerInfo from './playerInfo';
export default function(ctrl: SwissCtrl) {
const d = ctrl.data;
@@ -22,6 +23,7 @@ export default function(ctrl: SwissCtrl) {
$(el).replaceWith($('.swiss__underchat.none').removeClass('none'));
})
}),
+ playerInfo(ctrl),
h('div.swiss__main',
h('div.box.swiss__main-' + d.status, content)
),
diff --git a/ui/swiss/src/view/playerInfo.ts b/ui/swiss/src/view/playerInfo.ts
new file mode 100644
index 0000000000..a50bdbbd13
--- /dev/null
+++ b/ui/swiss/src/view/playerInfo.ts
@@ -0,0 +1,100 @@
+import { h } from 'snabbdom'
+import { VNode } from 'snabbdom/vnode';
+import { spinner, bind, userName, dataIcon, player as renderPlayer, numberRow } from './util';
+import { Player, PlayerExt, Pairing } from '../interfaces';
+import SwissCtrl from '../ctrl';
+
+export default function(ctrl: SwissCtrl): VNode {
+ if (!ctrl.playerInfoId) return;
+ const data = ctrl.data.playerInfo;
+ const noarg = ctrl.trans.noarg;
+ const tag = 'div.swiss__player-info.swiss__table';
+ if (data?.user.id !== ctrl.playerInfoId) return h(tag, [
+ h('div.stats', [
+ h('h2', ctrl.playerInfoId),
+ spinner()
+ ])
+ ]);
+ const games = data.pairings.filter(p => p).length;
+ const wins = data.pairings.filter(p => p?.w).length;
+ const avgOp: number | undefined = games ?
+ Math.round(data.pairings.reduce((r, p) => r + (p ? p.rating : 0), 0) / games) :
+ undefined;
+ return h(tag, {
+ hook: {
+ insert: setup,
+ postpatch(_, vnode) { setup(vnode) }
+ }
+ }, [
+ h('a.close', {
+ attrs: dataIcon('L'),
+ hook: bind('click', () => ctrl.showPlayerInfo(data), ctrl.redraw)
+ }),
+ h('div.stats', [
+ h('h2', [
+ h('span.rank', data.rank + '. '),
+ renderPlayer(data, true, false)
+ ]),
+ h('table', [
+ numberRow('Points', data.points, 'raw'),
+ numberRow('Tie break', data.tieBreak, 'raw'),
+ ...(games ? [
+ data.performance ? numberRow(
+ noarg('performance'),
+ data.performance + (games < 3 ? '?' : ''),
+ 'raw') : null,
+ numberRow(noarg('winRate'), [wins, games], 'percent'),
+ numberRow(noarg('averageOpponent'), avgOp, 'raw')
+ ] : [])
+ ])
+ ]),
+ h('div', [
+ h('table.pairings.sublist', {
+ hook: bind('click', e => {
+ const href = ((e.target as HTMLElement).parentNode as HTMLElement).getAttribute('data-href');
+ if (href) window.open(href, '_blank');
+ })
+ }, data.pairings.map((p, i) => {
+ const round = i + 1;
+ if (!p) return h('tr', [
+ h('th', '' + round),
+ h('td.bye', {
+ attrs: { colspan: 3},
+ }, 'Bye'),
+ h('td', '½')
+ ]);
+ const res = result(p);
+ return h('tr.glpt.' + (res === '1' ? ' win' : (res === '0' ? ' loss' : '')), {
+ key: p.g,
+ attrs: { 'data-href': '/' + p.g + (p.c ? '' : '/black') },
+ hook: {
+ destroy: vnode => $.powerTip.destroy(vnode.elm as HTMLElement)
+ }
+ }, [
+ h('th', '' + round),
+ h('td', userName(p.user)),
+ h('td', '' + p.rating),
+ h('td.is.color-icon.' + (p.c ? 'white' : 'black')),
+ h('td', res)
+ ]);
+ }))
+ ])
+ ]);
+};
+
+function result(p: Pairing): string {
+ switch (p.w) {
+ case true:
+ return '1';
+ case false:
+ return '0';
+ default:
+ return p.o ? '*' : '½';
+ }
+}
+
+function setup(vnode: VNode) {
+ const el = vnode.elm as HTMLElement, p = window.lichess.powertip;
+ p.manualUserIn(el);
+ p.manualGameIn(el);
+}
diff --git a/ui/swiss/src/view/standing.ts b/ui/swiss/src/view/standing.ts
index 983f20dde2..f57c68c2f1 100644
--- a/ui/swiss/src/view/standing.ts
+++ b/ui/swiss/src/view/standing.ts
@@ -12,7 +12,8 @@ function playerTr(ctrl: SwissCtrl, player: Player) {
return h('tr', {
key: userId,
class: {
- me: ctrl.data.me?.id == userId
+ me: ctrl.data.me?.id == userId,
+ active: ctrl.playerInfoId === userId
},
hook: bind('click', _ => ctrl.showPlayerInfo(player), ctrl.redraw)
}, [
@@ -89,7 +90,11 @@ export default function standing(ctrl: SwissCtrl, pag, klass?: string): VNode {
pag.currentPageResults.map(res => playerTr(ctrl, res)) : lastBody;
if (pag.currentPageResults) lastBody = tableBody;
return h('table.slist.swiss__standing' + (klass ? '.' + klass : ''), {
- class: { loading: !pag.currentPageResults },
+ class: {
+ loading: !pag.currentPageResults,
+ long: ctrl.data.round > 35,
+ xlong: ctrl.data.round > 80,
+ },
}, [
h('tbody', {
hook: {
diff --git a/ui/swiss/src/view/util.ts b/ui/swiss/src/view/util.ts
index b520129bf4..86d0bee0fc 100644
--- a/ui/swiss/src/view/util.ts
+++ b/ui/swiss/src/view/util.ts
@@ -65,6 +65,14 @@ export function player(p: Player, asLink: boolean, withRating: boolean) {
]);
}
+export function numberRow(name: string, value: any, typ?: string) {
+ return h('tr', [h('th', name), h('td',
+ typ === 'raw' ? value : (typ === 'percent' ? (
+ value[1] > 0 ? ratio2percent(value[0] / value[1]) : 0
+ ) : window.lichess.numberFormat(value))
+ )]);
+}
+
export function spinner(): VNode {
return h('div.spinner', [
h('svg', { attrs: { viewBox: '0 0 40 40' } }, [
diff --git a/ui/swiss/src/xhr.ts b/ui/swiss/src/xhr.ts
index 4b7ebdf8ba..48a1fcf22c 100644
--- a/ui/swiss/src/xhr.ts
+++ b/ui/swiss/src/xhr.ts
@@ -22,20 +22,16 @@ const loadPageOf = (ctrl: SwissCtrl, userId: string): Promise =>
json(`/swiss/${ctrl.data.id}/page-of/${userId}`);
const reload = (ctrl: SwissCtrl) =>
- json(`/swiss/${ctrl.data.id}?page=${ctrl.focusOnMe ? 0 : ctrl.page}`).then(data => {
+ json(`/swiss/${ctrl.data.id}?page=${ctrl.focusOnMe ? 0 : ctrl.page}&playerInfo=${ctrl.playerInfoId}`).then(data => {
ctrl.reload(data);
ctrl.redraw();
}).catch(onFail);
-// function playerInfo(ctrl: SwissCtrl, userId: string) {
-// return $.ajax({
-// url: ['/swiss', ctrl.data.id, 'player', userId].join('/'),
-// headers
-// }).then(data => {
-// ctrl.setPlayerInfoData(data);
-// ctrl.redraw();
-// }, onFail);
-// }
+const playerInfo = (ctrl: SwissCtrl, userId: string) =>
+ json(`/swiss/${ctrl.data.id}/player/${userId}`).then(data => {
+ ctrl.data.playerInfo = data;
+ ctrl.redraw();
+ }).catch(onFail);
export default {
join: throttle(1000, join),
@@ -43,5 +39,5 @@ export default {
loadPageOf,
reloadSoon: throttle(4000, reload),
reloadNow: reload,
- // playerInfo
+ playerInfo
};