tournament manager

pull/1770/merge
Thibault Duplessis 2016-03-28 18:08:48 +07:00
parent b7733b9aee
commit e7aed405ea
21 changed files with 755 additions and 41 deletions

View File

@ -0,0 +1,58 @@
package controllers
import play.api.data.Form
import play.api.libs.json._
import play.api.mvc._
import lila.api.Context
import lila.app._
import lila.common.HTTPRequest
import lila.game.{ Pov, GameRepo }
import lila.tournament.{ System, TournamentRepo, PairingRepo, Tournament => Tourney, VisibleTournaments }
import lila.user.UserRepo
import views._
object TournamentCrud extends LilaController {
private def env = Env.tournament
private def crud = Env.tournament.crudApi
def index = Secure(_.ManageTournament) { implicit ctx =>
me =>
crud.list map { tours =>
html.tournament.crud.index(tours)
}
}
def edit(id: String) = Secure(_.ManageTournament) { implicit ctx =>
me =>
OptionOk(crud one id) { tour =>
html.tournament.crud.edit(tour, crud editForm tour)
}
}
def update(id: String) = SecureBody(_.ManageTournament) { implicit ctx =>
me =>
OptionFuResult(crud one id) { tour =>
implicit val req = ctx.body
crud.editForm(tour).bindFromRequest.fold(
err => BadRequest(html.tournament.crud.edit(tour, err)).fuccess,
data => crud.update(tour, data) inject Redirect(routes.TournamentCrud.index)
)
}
}
def form = Secure(_.ManageTournament) { implicit ctx =>
me =>
Ok(html.tournament.crud.create(crud.createForm)).fuccess
}
def create = SecureBody(_.ManageTournament) { implicit ctx =>
me =>
implicit val req = ctx.body
crud.createForm.bindFromRequest.fold(
err => BadRequest(html.tournament.crud.create(err)).fuccess,
data => crud.create(data) inject Redirect(routes.TournamentCrud.index)
)
}
}

View File

@ -6,7 +6,7 @@ import scala.collection.mutable
import org.joda.time.format._
import org.joda.time.format.ISODateTimeFormat
import org.joda.time.{ Period, PeriodType, DurationFieldType, DateTime }
import org.joda.time.{ Period, PeriodType, DurationFieldType, DateTime, DateTimeZone }
import play.twirl.api.Html
import lila.api.Context
@ -48,6 +48,12 @@ trait DateHelper { self: I18nHelper =>
def showDateTime(date: DateTime)(implicit ctx: Context): String =
dateTimeFormatter(ctx) print date
def showDateTimeZone(date: DateTime, zone: DateTimeZone)(implicit ctx: Context): String =
dateTimeFormatter(ctx) print date.toDateTime(zone)
def showDateTimeUTC(date: DateTime)(implicit ctx: Context): String =
showDateTimeZone(date, DateTimeZone.UTC)
def showDate(date: DateTime)(implicit ctx: Context): String =
dateFormatter(ctx) print date

View File

@ -0,0 +1,10 @@
@(form: Form[_])(implicit ctx: Context)
@layout(title = "New tournament", active = "form") {
<div class="tour_crud content_box small_box no_padding">
<h1 class="lichess_title">New tournament</h1>
<form class="content_box_content material form" action="@routes.TournamentCrud.create" method="POST">
@inForm(form)
</form>
</div>
}

View File

@ -0,0 +1,10 @@
@(tour: lila.tournament.Tournament, form: Form[_])(implicit ctx: Context)
@layout(title = tour.fullName) {
<div class="tour_crud content_box small_box no_padding">
<h1 class="lichess_title">@tour.fullName</h1>
<form class="content_box_content material form" action="@routes.TournamentCrud.update(tour.id)" method="POST">
@inForm(form)
</form>
</div>
}

View File

@ -0,0 +1,66 @@
@(form: Form[_])(implicit ctx: Context)
@import lila.tournament.DataForm._
@import lila.tournament.crud.CrudForm._
@group(field: play.api.data.Field, name: Html, half: Boolean = false)(html: Html) = {
<div class="form-group@if(half){ half}@if(field.hasErrors){ has-error}">
@html
<label for="@field.id" class="control-label">@name</label>
<i class="bar"></i>
</div>
}
<div>
@group(form("date"), Html("Date <strong>yyyy-mm-dd</strong>"), half = true) {
@base.input(form("date"))
}
<div class="form-group half">
@group(form("dateHour"), Html("Hour <strong>UTC</strong>"), half = true) {
@base.input(form("dateHour"))
}
@group(form("dateMinute"), Html("Minute <strong>UTC</strong>"), half = true) {
@base.input(form("dateMinute"))
}
</div>
</div>
@group(form("name"), Html("Name")) {
@base.input(form("name"))
}
<div>
@group(form("homepageHours"), Html("Hours on homepage (0 to 24)"), half = true) {
@base.input(form("homepageHours"))
}
@group(form("image"), Html("Custom icon"), half = true) {
@base.select(form("image"), imageChoices)
}
@group(form("headline"), Html("Homepage headline")) {
@base.input(form("headline"))
}
</div>
@group(form("description"), Html("Full description")) {
<textarea name="@form("description").name" id="@form("description").id">@form("description").value</textarea>
}
<div>
@group(form("variant"), Html("Variant"), half = true) {
@base.select(form("variant"), translatedVariantChoicesWithVariants.map(x => x._1 -> x._2))
}
@group(form("minutes"), Html("Duration in minutes"), half = true) {
@base.input(form("minutes"))
}
</div>
<div>
@group(form("clockTime"), Html("Clock time"), half = true) {
@base.select(form("clockTime"), clockTimeChoices)
}
@group(form("clockIncrement"), Html("Clock increment"), half = true) {
@base.select(form("clockIncrement"), clockIncrementChoices)
}
</div>
@group(form("mode"), Html("Mode")) {
@base.select(form("mode"), translatedModeChoices.map(x => x._1 -> x._2))
}
<div class="button-container">
<button type="submit" class="submit button text" data-icon="E">Apply now</button>
</div>

View File

@ -0,0 +1,31 @@
@(tours: List[lila.tournament.Tournament])(implicit ctx: Context)
@title = {Tournament manager}
@layout(title = title.body, active = "index") {
<div class="tour_crud content_box no_padding">
<h1 class="lichess_title">@title</h1>
<table class="slist">
<thead>
<tr>
<th></th>
<th>Variant</th>
<th>Clock</th>
<th>Duration</th>
<th>UTC Date</th>
</tr>
</thead>
<tbody>
@tours.map { tour =>
<tr>
<td><a href="@routes.TournamentCrud.edit(tour.id)">@tour.fullName</a></td>
<td>@tour.variant.name</td>
<td>@tour.clock</td>
<td>@{tour.minutes}m</td>
<td>@showDateTimeUTC(tour.startsAt)</td>
</tr>
}
</tbody>
</table>
</div>
}

View File

@ -0,0 +1,20 @@
@(title: String, active: String = "")(body: Html)(implicit ctx: Context)
@menu = {
<a class="@active.active("index")" href="@routes.TournamentCrud.index">Recent tournaments</a>
<a class="@active.active("form")" href="@routes.TournamentCrud.form">Create a tournament</a>
}
@moreCss = {
@cssTag("material.form.css")
@cssTag("tournament.crud.css")
}
@moreJs = {
@jsTag("tournament.crud.js")
}
@base.layout(
title = title,
menu = menu.some,
moreCss = moreCss,
moreJs = moreJs)(body)

View File

@ -1,6 +1,7 @@
@(form: Form[_], config: lila.tournament.DataForm)(implicit ctx: Context)
@import config._
@import lila.tournament.DataForm._
@moreJs = {
@jsTag("tournamentForm.js")
@ -64,7 +65,7 @@ moreJs = moreJs) {
</tr>
<tr>
<th><label for="@form("waitMinutes").id">@trans.timeBeforeTournamentStarts()</label></th>
<td>@base.select(form("waitMinutes"), config.waitMinuteChoices)</td>
<td>@base.select(form("waitMinutes"), waitMinuteChoices)</td>
</tr>
<tr>
<th></th>
@ -79,20 +80,20 @@ moreJs = moreJs) {
</table>
</form>
<div class="none private_time">
@base.select(form("clockTime"), config.clockTimePrivateChoices)
@base.select(form("clockTime"), clockTimePrivateChoices)
+
@base.select(form("clockIncrement"), config.clockIncrementPrivateChoices)
@base.select(form("clockIncrement"), clockIncrementPrivateChoices)
</div>
<div class="none public_time">
@base.select(form("clockTime"), config.clockTimeChoices)
@base.select(form("clockTime"), clockTimeChoices)
+
@base.select(form("clockIncrement"), config.clockIncrementChoices)
@base.select(form("clockIncrement"), clockIncrementChoices)
</div>
<div class="none private_minutes">
@base.select(form("minutes"), config.minutePrivateChoices)
@base.select(form("minutes"), minutePrivateChoices)
</div>
<div class="none public_minutes">
@base.select(form("minutes"), config.minuteChoices)
@base.select(form("minutes"), minuteChoices)
</div>
</div>
<div class="content_box small_box faq_box tournament_box">

View File

@ -166,6 +166,13 @@ GET /tournament/$id<\w{8}>/player/:user controllers.Tournament.player(id:
POST /tournament/$id<\w{8}>/terminate controllers.Tournament.terminate(id: String)
GET /tournament/help controllers.Tournament.help(system: Option[String] ?= None)
# Tournament CRUD
GET /tournament/manager controllers.TournamentCrud.index
GET /tournament/manager/$id<\w{8}> controllers.TournamentCrud.edit(id: String)
POST /tournament/manager/$id<\w{8}> controllers.TournamentCrud.update(id: String)
GET /tournament/manager/new controllers.TournamentCrud.form
POST /tournament/manager controllers.TournamentCrud.create
# Simul
GET /simul controllers.Simul.home
GET /simul/new controllers.Simul.form

View File

@ -29,7 +29,7 @@ object Form {
of[Double].verifying(hasKey(choices, _))
def stringIn(choices: Iterable[(String, String)]) =
nonEmptyText.verifying(hasKey(choices, _))
text.verifying(hasKey(choices, _))
def hasKey[A](choices: Iterable[(A, _)], key: A) =
choices.map(_._1).toList contains key

View File

@ -32,6 +32,7 @@ object Permission {
case object UserSearch extends Permission("ROLE_USER_SEARCH")
case object CloseTeam extends Permission("ROLE_CLOSE_TEAM")
case object TerminateTournament extends Permission("ROLE_TERMINATE_TOURNAMENT")
case object ManageTournament extends Permission("ROLE_MANAGE_TOURNAMENT")
case object Hunter extends Permission("ROLE_HUNTER", List(
ViewBlurs, MarkEngine, MarkBooster, StaffForum,
@ -41,14 +42,14 @@ object Permission {
case object Admin extends Permission("ROLE_ADMIN", List(
Hunter, ModerateForum, IpBan, CloseAccount, ReopenAccount,
MarkTroll, SetTitle, SetEmail, ModerateQa, StreamConfig,
MessageAnyone, CloseTeam, TerminateTournament))
MessageAnyone, CloseTeam, TerminateTournament, ManageTournament))
case object SuperAdmin extends Permission("ROLE_SUPER_ADMIN", List(Admin))
private lazy val all: List[Permission] = List(
SuperAdmin, Admin, Hunter, ViewBlurs, StaffForum, ModerateForum,
UserSpy, MarkTroll, MarkEngine, MarkBooster, IpBan, ModerateQa, StreamConfig,
Beta, MessageAnyone, UserSearch, CloseTeam, TerminateTournament)
Beta, MessageAnyone, UserSearch, CloseTeam, TerminateTournament, ManageTournament)
private lazy val allByName: Map[String, Permission] = all map { p => (p.name, p) } toMap

View File

@ -12,6 +12,33 @@ final class DataForm {
import DataForm._
lazy val create = Form(mapping(
"clockTime" -> numberInDouble(clockTimePrivateChoices),
"clockIncrement" -> numberIn(clockIncrementPrivateChoices),
"minutes" -> numberIn(minutePrivateChoices),
"waitMinutes" -> numberIn(waitMinuteChoices),
"variant" -> number.verifying(validVariantIds contains _),
"position" -> nonEmptyText.verifying(positions contains _),
"mode" -> optional(number.verifying(Mode.all map (_.id) contains _)),
"private" -> optional(text.verifying("on" == _))
)(TournamentSetup.apply)(TournamentSetup.unapply)
.verifying("Invalid clock", _.validClock)
.verifying("Increase tournament duration, or decrease game clock", _.validTiming)
) fill TournamentSetup(
clockTime = clockTimeDefault,
clockIncrement = clockIncrementDefault,
minutes = minuteDefault,
waitMinutes = waitMinuteDefault,
variant = chess.variant.Standard.id,
position = StartingPosition.initial.eco,
`private` = None,
mode = Mode.Rated.id.some)
}
object DataForm {
import chess.variant._
val clockTimes: Seq[Double] = Seq(0d, 1 / 2d, 3 / 4d, 1d, 3 / 2d) ++ (2d to 7d by 1d)
val clockTimesPrivate: Seq[Double] = clockTimes ++ (10d to 30d by 5d) ++ (40d to 60d by 10d)
val clockTimeDefault = 2d
@ -44,33 +71,6 @@ final class DataForm {
}
val positionDefault = StartingPosition.initial.eco
lazy val create = Form(mapping(
"clockTime" -> numberInDouble(clockTimePrivateChoices),
"clockIncrement" -> numberIn(clockIncrementPrivateChoices),
"minutes" -> numberIn(minutePrivateChoices),
"waitMinutes" -> numberIn(waitMinuteChoices),
"variant" -> number.verifying(validVariantIds contains _),
"position" -> nonEmptyText.verifying(positions contains _),
"mode" -> optional(number.verifying(Mode.all map (_.id) contains _)),
"private" -> optional(text.verifying("on" == _))
)(TournamentSetup.apply)(TournamentSetup.unapply)
.verifying("Invalid clock", _.validClock)
.verifying("Increase tournament duration, or decrease game clock", _.validTiming)
) fill TournamentSetup(
clockTime = clockTimeDefault,
clockIncrement = clockIncrementDefault,
minutes = minuteDefault,
waitMinutes = waitMinuteDefault,
variant = chess.variant.Standard.id,
position = StartingPosition.initial.eco,
`private` = None,
mode = Mode.Rated.id.some)
}
object DataForm {
import chess.variant._
val validVariants = List(Standard, Chess960, KingOfTheHill, ThreeCheck, Antichess, Atomic, Horde, RacingKings, Crazyhouse)
val validVariantIds = validVariants.map(_.id).toSet

View File

@ -69,6 +69,8 @@ final class Env(
roundMap = roundMap,
roundSocketHub = roundSocketHub)
lazy val crudApi = new crud.CrudApi
val tourAndRanks = api tourAndRanks _
private lazy val performance = new Performance

View File

@ -72,6 +72,13 @@ object Schedule {
case (Bullet, HyperBullet) => true
case _ => false
}
def fromClock(clock: TournamentClock) = {
val time = clock.chessClock.estimateTotalTime
if (time < 60) HyperBullet
else if (time < 180) Bullet
else if (time < 480) Blitz
else Classical
}
}
sealed trait Season

View File

@ -101,7 +101,7 @@ object Tournament {
val minPlayers = 2
def make(
createdBy: User,
createdByUserId: String,
clock: TournamentClock,
minutes: Int,
system: System,
@ -116,7 +116,7 @@ object Tournament {
system = system,
clock = clock,
minutes = minutes,
createdBy = createdBy.id,
createdBy = createdByUserId,
createdAt = DateTime.now,
nbPlayers = 0,
variant = variant,

View File

@ -42,7 +42,7 @@ private[tournament] final class TournamentApi(
def createTournament(setup: TournamentSetup, me: User): Fu[Tournament] = {
var variant = chess.variant.Variant orDefault setup.variant
val tour = Tournament.make(
createdBy = me,
createdByUserId = me.id,
clock = TournamentClock((setup.clockTime * 60).toInt, setup.clockIncrement),
minutes = setup.minutes,
waitMinutes = setup.waitMinutes,

View File

@ -30,6 +30,7 @@ object TournamentRepo {
if (variant.standard) BSONDocument("variant" -> BSONDocument("$exists" -> false))
else BSONDocument("variant" -> variant.id)
private val nonEmptySelect = BSONDocument("nbPlayers" -> BSONDocument("$ne" -> 0))
private val selectUnique = BSONDocument("schedule.freq" -> "unique")
def byId(id: String): Fu[Option[Tournament]] = coll.find(selectId(id)).one[Tournament]
@ -37,6 +38,9 @@ object TournamentRepo {
coll.find(BSONDocument("_id" -> BSONDocument("$in" -> ids)))
.cursor[Tournament]().collect[List]()
def uniqueById(id: String): Fu[Option[Tournament]] =
coll.find(selectId(id) ++ selectUnique).one[Tournament]
def recentAndNext: Fu[List[Tournament]] =
coll.find(sinceSelect(DateTime.now minusDays 1))
.cursor[Tournament]().collect[List]()
@ -170,6 +174,12 @@ object TournamentRepo {
}.reverse
}
def uniques(max: Int): Fu[List[Tournament]] =
coll.find(selectUnique)
.sort(BSONDocument("startsAt" -> -1))
.hint(BSONDocument("startsAt" -> -1))
.cursor[Tournament]().collect[List]()
def scheduledUnfinished: Fu[List[Tournament]] =
coll.find(scheduledSelect ++ unfinishedSelect)
.sort(BSONDocument("startsAt" -> 1)).cursor[Tournament]().collect[List]()

View File

@ -0,0 +1,68 @@
package lila.tournament
package crud
final class CrudApi {
def list = TournamentRepo uniques 30
def one(id: String) = TournamentRepo uniqueById id
def editForm(tour: Tournament) = CrudForm.apply fill CrudForm.Data(
name = tour.name,
homepageHours = ~tour.spotlight.flatMap(_.homepageHours),
clockTime = tour.clock.limitInMinutes,
clockIncrement = tour.clock.increment,
minutes = tour.minutes,
variant = tour.variant.id,
mode = tour.mode.id,
date = CrudForm.dateFormatter.print(tour.startsAt),
dateHour = tour.startsAt.getHourOfDay,
dateMinute = tour.startsAt.getMinuteOfHour,
image = ~tour.spotlight.flatMap(_.iconImg),
headline = tour.spotlight.??(_.headline),
description = tour.spotlight.??(_.description))
def update(old: Tournament, data: CrudForm.Data) =
TournamentRepo update updateTour(old, data) void
def createForm = CrudForm.apply
def create(data: CrudForm.Data) =
TournamentRepo insert updateTour(empty, data) void
private val empty = Tournament.make(
createdByUserId = "lichess",
clock = TournamentClock(0, 0),
minutes = 0,
system = System.Arena,
variant = chess.variant.Standard,
position = chess.StartingPosition.initial,
mode = chess.Mode.Rated,
`private` = false,
waitMinutes = 0)
private def updateTour(tour: Tournament, data: CrudForm.Data) = {
import data._
val clock = TournamentClock((clockTime * 60).toInt, clockIncrement)
val v = chess.variant.Variant.orDefault(variant)
tour.copy(
name = name,
clock = clock,
minutes = minutes,
variant = v,
mode = chess.Mode.orDefault(mode),
startsAt = actualDate,
schedule = Schedule(
freq = Schedule.Freq.Unique,
speed = Schedule.Speed.fromClock(clock),
variant = v,
position = chess.StartingPosition.initial,
at = actualDate).some,
spotlight = Spotlight(
headline = headline,
description = description,
homepageHours = homepageHours.some.filterNot(0 ==),
iconFont = none,
iconImg = image.some).some)
}
}

View File

@ -0,0 +1,87 @@
package lila.tournament
package crud
import org.joda.time.format.DateTimeFormat
import org.joda.time.{ DateTime, DateTimeZone }
import play.api.data._
import play.api.data.Forms._
import play.api.data.validation.Constraints._
import scala.util.Try
import chess.Mode
import chess.StartingPosition
import lila.common.Form._
object CrudForm {
import DataForm._
lazy val apply = Form(mapping(
"name" -> nonEmptyText(minLength = 3, maxLength = 40),
"homepageHours" -> number(min = 0, max = 24),
"clockTime" -> numberInDouble(clockTimePrivateChoices),
"clockIncrement" -> numberIn(clockIncrementPrivateChoices),
"minutes" -> numberIn(minutePrivateChoices),
"variant" -> number.verifying(validVariantIds contains _),
"mode" -> number.verifying(Mode.all map (_.id) contains _),
"date" -> nonEmptyText.verifying(s => parseDateUTC(s).isDefined),
"dateHour" -> number(min = 0, max = 23),
"dateMinute" -> number(min = 0, max = 59),
"image" -> stringIn(imageChoices),
"headline" -> nonEmptyText(minLength = 5, maxLength = 30),
"description" -> nonEmptyText(minLength = 10, maxLength = 400)
)(CrudForm.Data.apply)(CrudForm.Data.unapply)
.verifying("Invalid clock", _.validClock)
.verifying("Increase tournament duration, or decrease game clock", _.validTiming)
) fill CrudForm.Data(
name = "",
homepageHours = 0,
clockTime = clockTimeDefault,
clockIncrement = clockIncrementDefault,
minutes = minuteDefault,
variant = chess.variant.Standard.id,
mode = Mode.Rated.id,
date = dateFormatter.print(DateTime.now),
dateHour = 0,
dateMinute = 0,
image = "",
headline = "",
description = "")
case class Data(
name: String,
homepageHours: Int,
clockTime: Double,
clockIncrement: Int,
minutes: Int,
variant: Int,
mode: Int,
date: String,
dateHour: Int,
dateMinute: Int,
image: String,
headline: String,
description: String) {
def actualDate = parseDateUTC(date).err(s"Invalid date $date")
.withHourOfDay(dateHour)
.withMinuteOfHour(dateMinute)
def validClock = (clockTime + clockIncrement) > 0
def validTiming = (minutes * 60) >= (3 * estimatedGameDuration)
private def estimatedGameDuration = 60 * clockTime + 30 * clockIncrement
}
val imageChoices = List(
"" -> "Lichess",
"chesswhiz.logo.png" -> "ChessWhiz",
"chessat3.logo.png" -> "Chessat3")
val imageDefault = ""
val dateFormatter = DateTimeFormat forPattern "yyyy-MM-dd" withZone DateTimeZone.UTC
private def parseDateUTC(str: String): Option[DateTime] =
Try(dateFormatter parseDateTime str).toOption
}

View File

@ -0,0 +1,308 @@
.material.form * {
box-sizing: border-box;
}
.material.form *::after,
.material.form *::before {
box-sizing: border-box;
}
.material.form .button-container {
text-align: center;
}
.material.form fieldset {
margin: 0 0 3rem;
padding: 0;
border: none;
}
.material.form .form-radio,
.material.form .form-group {
position: relative;
margin-bottom: 3.25rem;
}
.material.form .form-group.half {
display: inline-block;
width: 47%;
}
.material.form .form-group.half .form-group.half {
width: 46.5%;
margin-bottom: 0;
}
.material.form .form-group.half:nth-child(odd) {
margin-right: 5%;
}
.material.form .form-inline > .form-group,
.material.form .form-inline > .btn {
display: inline-block;
margin-bottom: 0;
}
.material.form .form-help {
margin-top: 0.125rem;
margin-left: 0.125rem;
opacity: 0.6;
font-size: 0.8rem;
}
.material.form .checkbox .form-help, .form-radio .form-help, .form-group .form-help {
position: absolute;
width: 100%;
}
.material.form .checkbox .form-help {
position: relative;
margin-bottom: 1rem;
}
.material.form .form-radio .form-help {
padding-top: 0.25rem;
margin-top: -1rem;
}
.material.form .form-group input {
height: 1.9rem;
}
.material.form .form-group textarea {
resize: none;
}
.material.form .form-group select {
width: 100%;
font-size: 1rem;
height: 1.6rem;
padding: 0.125rem 0.125rem 0.0625rem;
background: none;
border: none;
line-height: 1.6;
box-shadow: none;
}
.material.form .form-group .control-label {
position: absolute;
top: 0.25rem;
pointer-events: none;
padding-left: 0.125rem;
z-index: 1;
opacity: 0.6;
font-size: 1rem;
font-weight: normal;
-webkit-transition: all 0.28s ease;
transition: all 0.28s ease;
}
.material.form .form-group .bar {
position: relative;
border-bottom: 0.0625rem solid #999;
display: block;
}
.material.form .form-group .bar::before {
content: '';
height: 0.125rem;
width: 0;
left: 50%;
bottom: -0.0625rem;
position: absolute;
background: #337ab7;
-webkit-transition: left 0.28s ease, width 0.28s ease;
transition: left 0.28s ease, width 0.28s ease;
z-index: 2;
}
.material.form .form-group input,
.material.form .form-group textarea {
display: block;
background: none;
padding: 0.125rem 0.125rem 0.0625rem;
font-size: 1rem;
border-width: 0;
border-color: transparent;
line-height: 1.9;
width: 100%;
-webkit-transition: all 0.28s ease;
transition: all 0.28s ease;
box-shadow: none;
}
.material.form .form-group input[type="file"] {
line-height: 1;
}
.material.form .form-group input[type="file"] ~ .bar {
display: none;
}
.material.form .form-group * {
color: #333;
}
.material.form .form-group select ~ .control-label,
.material.form .form-group input:focus ~ .control-label,
.material.form .form-group input:valid ~ .control-label,
.material.form .form-group input.form-file ~ .control-label,
.material.form .form-group input.has-value ~ .control-label,
.material.form .form-group textarea:focus ~ .control-label,
.material.form .form-group textarea:valid ~ .control-label,
.material.form .form-group textarea.form-file ~ .control-label,
.material.form .form-group textarea.has-value ~ .control-label {
font-size: 0.8rem;
opacity: 0.6;
top: -1rem;
left: 0;
}
.material.form .form-group select:focus,
.material.form .form-group input:focus,
.material.form .form-group textarea:focus {
outline: none;
}
.material.form .form-group select:focus ~ .control-label,
.material.form .form-group input:focus ~ .control-label,
.material.form .form-group textarea:focus ~ .control-label {
color: #337ab7;
}
.material.form .form-group select:focus ~ .bar::before,
.material.form .form-group input:focus ~ .bar::before,
.material.form .form-group textarea:focus ~ .bar::before {
width: 100%;
left: 0;
}
.material.form .checkbox label,
.material.form .form-radio label {
position: relative;
cursor: pointer;
padding-left: 2rem;
text-align: left;
display: block;
}
.material.form .checkbox input,
.material.form .form-radio input {
width: auto;
opacity: 0.00000001;
position: absolute;
left: 0;
}
.material.form .radio {
margin-bottom: 1rem;
}
.material.form .radio .helper {
position: absolute;
top: -0.25rem;
left: -0.25rem;
cursor: pointer;
display: block;
font-size: 1rem;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
opacity: 0.6;
}
.material.form .radio .helper::before, .radio .helper::after {
content: '';
position: absolute;
left: 0;
top: 0;
margin: 0.25rem;
width: 1rem;
height: 1rem;
-webkit-transition: -webkit-transform 0.28s ease;
transition: -webkit-transform 0.28s ease;
transition: transform 0.28s ease;
transition: transform 0.28s ease, -webkit-transform 0.28s ease;
border-radius: 50%;
border: 0.125rem solid currentColor;
}
.material.form .radio .helper::after {
-webkit-transform: scale(0);
transform: scale(0);
background-color: #337ab7;
border-color: #337ab7;
}
.material.form .radio label:hover .helper {
color: #337ab7;
}
.material.form .radio input:checked ~ .helper::after {
-webkit-transform: scale(0.5);
transform: scale(0.5);
}
.material.form .radio input:checked ~ .helper::before {
color: #337ab7;
}
.material.form .checkbox {
margin-top: 3rem;
margin-bottom: 1rem;
}
.material.form .checkbox .helper {
opacity: 0.6;
position: absolute;
top: 0;
left: 0;
width: 1rem;
height: 1rem;
z-index: 0;
border: 0.125rem solid currentColor;
border-radius: 0.0625rem;
-webkit-transition: border-color 0.28s ease;
transition: border-color 0.28s ease;
}
.material.form .checkbox .helper::before, .checkbox .helper::after {
position: absolute;
height: 0;
width: 0.2rem;
background-color: #337ab7;
display: block;
-webkit-transform-origin: left top;
transform-origin: left top;
border-radius: 0.25rem;
content: '';
-webkit-transition: opacity 0.28s ease, height 0s linear 0.28s;
transition: opacity 0.28s ease, height 0s linear 0.28s;
opacity: 0;
}
.material.form .checkbox .helper::before {
top: 0.65rem;
left: 0.38rem;
-webkit-transform: rotate(-135deg);
transform: rotate(-135deg);
box-shadow: 0 0 0 0.0625rem #fff;
}
.material.form .checkbox .helper::after {
top: 0.3rem;
left: 0;
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg);
}
.material.form .checkbox label:hover .helper {
color: #337ab7;
}
.material.form .checkbox input:checked ~ .helper {
color: #337ab7;
}
.material.form .checkbox input:checked ~ .helper::after, .checkbox input:checked ~ .helper::before {
opacity: 1;
-webkit-transition: height 0.28s ease;
transition: height 0.28s ease;
}
.material.form .checkbox input:checked ~ .helper::after {
height: 0.5rem;
}
.material.form .checkbox input:checked ~ .helper::before {
height: 1.2rem;
-webkit-transition-delay: 0.28s;
transition-delay: 0.28s;
}
.material.form .radio + .radio,
.material.form .checkbox + .checkbox {
margin-top: 1rem;
}
.material.form .has-error .legend.legend, .has-error.form-group .control-label.control-label {
color: #d9534f;
}
.material.form .has-error.form-group .form-help,
.material.form .has-error.form-group label strong,
.material.form .has-error.form-group .helper, .has-error.checkbox .form-help,
.material.form .has-error.checkbox .helper, .has-error.radio .form-help,
.material.form .has-error.radio .helper, .has-error.form-radio .form-help,
.material.form .has-error.form-radio .helper {
color: #d9534f;
}
.material.form .has-error .bar::before {
background: #d9534f;
left: 0;
width: 100%;
}

View File

@ -0,0 +1,22 @@
h1.lichess_title {
text-align: center;
}
table.slist {
font-size: 1.2em;
width: 100%;
}
table.slist td {
padding: 20px 10px;
}
.material.form button {
font-size: 1.7em;
font-weight: normal;
padding: 10px 20px;
margin-bottom: 20px;
}
.material.form textarea {
height: 10em;
}
.tour_crud a {
color: #3893E8!important;
}