myheats

Live heats, scoring and leaderboard for sport events
git clone https://git.in0rdr.ch/myheats.git
Log | Files | Refs | Pull requests | README | LICENSE

commit 8310238b465a554af4b95b3774eaf8a0b471c09b
parent e5de22b2cbc35ab64f22d39728b215813f690f36
Author: Andreas Gruhler <andreas.gruhler@adfinis.com>
Date:   Sun,  2 Jul 2023 18:04:25 +0200

feat: update layout

Diffstat:
Msrc/App.css | 142+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Msrc/App.jsx | 63+++++++++++++++++++++++++++++++++++++++++++++++----------------
Msrc/Athletes.jsx | 36++++++++++++++++++------------------
Msrc/Auth.jsx | 16++++++----------
Msrc/Heats.jsx | 38+++++++++++++-------------------------
Msrc/Leaderboard.jsx | 119+++++++++++++++++++++++++++++++++++++++----------------------------------------
Msrc/Score.jsx | 63+++++++++++++++++++++++++++++++++++++++++++--------------------
Msrc/Startlist.jsx | 27+++++++++++++--------------
Msrc/index.css | 78++++++++++++++++++++----------------------------------------------------------
9 files changed, 326 insertions(+), 256 deletions(-)

diff --git a/src/App.css b/src/App.css @@ -1,43 +1,96 @@ +#root { + display: flex; + flex-direction: column; + min-height: 100vh; + justify-content: space-between; +} + +main { + flex: 1; +} + +nav { + padding: 0; + margin: 0 0 20px 0; +} + +nav ul { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + padding: 0; +} + +nav li { + list-style-type: none; + flex: 1; + white-space: nowrap; +} + +nav li a { + text-transform: uppercase; + text-align: center; + font-size: 0.8em; + border: 5px solid white; + background: #f7f7f7; + text-decoration: none; + padding: 10px; + display: block; +} +nav a.active { + font-weight: bold; + color: white; + background: #353535; +} + table { border-collapse: collapse; } th, td { - border: 1px solid black; padding: 5px; + text-align: left; + /* border: 1px solid #e4e4e4; */ } -.right { - text-align: right; +tbody tr:nth-child(even) { + background-color: #f7f7f7; } -/* set medal icon for first three rows */ -table.leaderboard tr:first-child td:first-child::after { - content: "🥇"; +footer { + padding: 20px; + display: flex; + flex-wrap: wrap; + justify-content: space-between; } -table.leaderboard tr:nth-child(2) td:first-child::after { - content: "🥈"; +footer span { + flex: 1; } -table.leaderboard tr:nth-child(3) td:first-child::after { - content: "🥉"; +footer .login { + text-align: right; +} +footer .login button { + width: auto; } -/* increment rank row number */ -table.leaderboard tr { - counter-increment: rowNumber; +.right { + text-align: right; } -table.leaderboard tr td:first-child::before { - content: counter(rowNumber); - min-width: 1em; - margin-right: 0.5em; + +.scoreInput { + font-size: 25px; } -button:disabled { - visibility: hidden; +.loginForm { + margin: 30px; +} +.loginForm button, .loginForm input { + width: 250px; + display: block; } .heatInfo { - border-left: 10px solid rgb(255, 242, 166); + margin-bottom: 20px; } .heatInfo ul { padding: 5px; @@ -46,53 +99,72 @@ button:disabled { list-style: none; } -.newHeatForm table th { - text-align: left; +.newHeatForm { + margin-top: 20px; } -.newHeatForm tr, .newHeatForm th, .newHeatForm td { - border: none; + +/* increment rank row number */ +table.leaderboard tr{ + counter-increment: rowNumber; +} +table.leaderboard tr td:first-child::before { + content: counter(rowNumber); + min-width: 1em; + margin-right: 0.5em; +} + +/* highlight totals */ +table.leaderboard td:last-child { + font-weight: bold; } /* https://css-tricks.com/making-tables-responsive-with-minimal-css */ @media(max-width: 800px) { - table.leaderboard thead { + table thead { left: -9999px; position: absolute; visibility: hidden; } - table.leaderboard tr { + table tr { border-bottom: 0; display: flex; flex-direction: row; flex-wrap: wrap; - margin-bottom: 40px; + margin-bottom: 20px; } - table.leaderboard td { + table td { margin: 0 -1px -1px 0; padding-top: 35px; + margin-bottom: 25px; position: relative; width: 45%; + text-align: left !important; } - table.leaderboard td:before { + table td:before { content: attr(data-title); position: absolute; top: 3px; left: 3px; - font-weight: bold; font-size: 0.8em; - color: rgb(112, 112, 112); text-transform: uppercase; + background: rgb(255, 247, 208); } table.leaderboard td:nth-child(-n+6) { - background-color: rgb(243, 243, 243); + /* background: rgb(236, 236, 236); */ + } + + table td button { + position: absolute; + bottom: 0; + height: 50px; + width: 100%; } - table.leaderboard td:nth-last-child(-n+3) { - font-weight: bold; - font-size: 1.5em; + table td:empty { + display: none; } } diff --git a/src/App.jsx b/src/App.jsx @@ -1,6 +1,6 @@ import './App.css' -import { Suspense, lazy, useState, useEffect } from 'react' -import { BrowserRouter as Router, Routes, Route, Outlet, Link } from 'react-router-dom' +import { Suspense, lazy, useState, useEffect, Fragment } from 'react' +import { BrowserRouter as Router, Routes, Route, Outlet, Link, NavLink } from 'react-router-dom' import { supabase } from './supabaseClient' @@ -15,25 +15,56 @@ document.title = import.meta.env.VITE_APP_DOC_TITLE ? import.meta.env.VITE_APP_D function Layout({session}) { return ( - <div> + <Fragment> <nav> <ul> - <li><Link to="/">Leaderboard</Link></li> - {session ? <li><Link to="/score">Scoring</Link></li> : ''} - {session ? <li><Link to="/heats">Heats and Startlists</Link></li> : ''} - {session ? <li> <Link to="/athletes">Athletes</Link></li> : ''} <li> - {session ? <button onClick={() => supabase.auth.signOut()}> Sign out {session.user.email} </button> : - <Link to="/auth">Login</Link>} + <NavLink + className={({ isActive, isPending }) => isPending ? "pending" : isActive ? "active" : ""} + to="/"> + Leaderboard + </NavLink> </li> + {session ? <li> + <NavLink + className={({ isActive, isPending }) => isPending ? "pending" : isActive ? "active" : ""} + to="/score"> + Scoring + </NavLink> + </li> : ''} + {session ? <li> + <NavLink + className={({ isActive, isPending }) => isPending ? "pending" : isActive ? "active" : ""} + to="/heats"> + Heats and Startlists + </NavLink> + </li> : ''} + {session ? <li> + <NavLink + className={({ isActive, isPending }) => isPending ? "pending" : isActive ? "active" : ""} + to="/athletes"> + Athletes + </NavLink> + </li> : ''} </ul> </nav> - <Outlet /> - <div> + <main> + <Outlet /> + </main> + <footer> <br /> - MyHeats <a href="https://code.in0rdr.ch/myheats/refs.html">v0.4-nightly</a> - </div> - </div> + <span className='version'>MyHeats <a href="https://code.in0rdr.ch/myheats/refs.html">v0.4-nightly</a></span> + <span className='login'> + {session ? <button onClick={() => supabase.auth.signOut()}> Sign out {session.user.email} </button> : + <NavLink + className={({ isActive, isPending }) => isPending ? "pending" : isActive ? "active" : ""} + to="/auth"> + Login + </NavLink> + } + </span> + </footer> + </Fragment> ) } @@ -60,7 +91,7 @@ function App() { }, []) return ( - <div className="App"> + <Fragment> <Router> <Suspense fallback={<div>Loading...</div>}> <Routes> @@ -76,7 +107,7 @@ function App() { </Routes> </Suspense> </Router> - </div> + </Fragment> ); } diff --git a/src/Athletes.jsx b/src/Athletes.jsx @@ -36,7 +36,7 @@ async function addAthlete(e) { function defaultsSet({nr, firstname, lastname, birthday, school}) { return ( nr === '00' - || firstname === 'Firstname' + || firstname === 'Firstname *' || lastname === 'Lastname' || birthday === '0000-00-00' || school === 'School/team' @@ -72,13 +72,13 @@ function AthleteForm({session}) { return ( <div> - <h1>Athletes <button disabled={!loading}>{loading ? '🔄 loading' : ''}</button></h1> + <button disabled={!loading}>{loading ? '🔄 loading' : ''}</button> <form method='post' onSubmit={addAthlete}> <table> <thead> <tr> <th>Created at</th> - <th>Nr</th> + <th>Start Nr.</th> <th>Firstname</th> <th>Lastname</th> <th>Birthday</th> @@ -89,35 +89,35 @@ function AthleteForm({session}) { <tbody> {athletes.map(a => ( <tr key={a.id}> - <td className='right'>{new Date(a.created_at).toLocaleString()}</td> - <td className='right'>{a.nr}</td> - <td>{a.firstname}</td> - <td>{a.lastname}</td> - <td className='right'>{a.birthday}</td> - <td>{a.school}</td> - <td className='right'><button onClick={e => deleteAthlete(e, a.id, a.firstname, a.lastname)}>🗑️ delete</button></td> + <td data-title='Created at' className='right'>{new Date(a.created_at).toLocaleString()}</td> + <td data-title='Start Nr.' className='right'>{a.nr}</td> + <td data-title='Firstname'>{a.firstname}</td> + <td data-title='Lastname'>{a.lastname}</td> + <td data-title='Birthday'>{a.birthday}</td> + <td data-title='School'>{a.school}</td> + <td><button onClick={e => deleteAthlete(e, a.id, a.firstname, a.lastname)}>🗑️ delete</button></td> </tr> ))} <tr> <td className='right'><i>* required</i></td> - <td className='right'> - <input type='number' name='nr' defaultValue='00' /> * + <td data-title='Start Nr. *' className='right'> + <input type='number' name='nr' defaultValue='00' /> </td> - <td> - <input type='text' name='firstname' defaultValue='Firstname' /> * + <td data-title='Firstname *'> + <input type='text' name='firstname' defaultValue='Firstname *' /> </td> - <td> + <td data-title='Lastname'> <input type='text' name='lastname' defaultValue='Lastname' /> </td> - <td className='right'> + <td data-title='Birthday' className='right'> <input type='date' name='birthday' /> </td> - <td> + <td data-title='School'> <input type='text' name='school' defaultValue='School/team' /> </td> - <td className='right'> + <td> <button type='submit'>➕ new</button> </td> </tr> diff --git a/src/Auth.jsx b/src/Auth.jsx @@ -14,7 +14,7 @@ function Auth() { email: email, options: { shouldCreateUser: false, - // emailRedirectTo: 'http://localhost:5173' + emailRedirectTo: 'http://localhost:5173' }}) if (error) throw error alert('Check your email for the login link!') @@ -28,13 +28,11 @@ function Auth() { return ( <div className="Auth"> <div> + <button disabled={!loading}>{loading ? ' sending magic link' : ''}</button> <h1>Login</h1> - <p>Sign in via magic link with your email below</p> - {loading ? ( - 'Sending magic link...' - ) : ( + <p>Sign in with a magic link.</p> + <div className='loginForm'> <form onSubmit={handleLogin}> - <label htmlFor="email">Email</label> <input id="email" type="email" @@ -42,11 +40,9 @@ function Auth() { value={email} onChange={(e) => setEmail(e.target.value)} /> - <button> - Send magic link - </button> + <button>🛸 Send magic link</button> </form> - )} + </div> <h1>Registration</h1> <p>No account yet? Contact event administrator to sign up.</p> </div> diff --git a/src/Heats.jsx b/src/Heats.jsx @@ -1,5 +1,5 @@ import { lazy, useEffect, useState } from 'react' -import { useNavigate, generatePath } from 'react-router-dom' +import { generatePath, Link } from 'react-router-dom' import { supabase } from './supabaseClient' const Auth = lazy(() => import('./Auth')) @@ -35,12 +35,7 @@ async function addHeat(e) { } export function defaultsSet({name, location}) { - return (name === 'Name' || location === 'Location') -} - -function navigateToHeat(e, n, heatId) { - e.preventDefault() - n(generatePath('/startlist/:heatId', {heatId:heatId})) + return (name === 'Name *' || location === 'Location') } async function deleteHeat(e, heatId, heatName) { @@ -59,8 +54,6 @@ function HeatForm({session}) { const [loading, setLoading] = useState(false) const [heats, setHeats] = useState([]) - const navigate = useNavigate() - useEffect(() => { (async () => { setLoading(true) @@ -73,7 +66,7 @@ function HeatForm({session}) { return ( <div> - <h1>Heats and Startlists <button disabled={!loading}>{loading ? '🔄 loading' : ''}</button></h1> + <button disabled={!loading}>{loading ? '🔄 loading' : ''}</button> <form method='post' onSubmit={addHeat}> <table> <thead> @@ -82,38 +75,33 @@ function HeatForm({session}) { <th>Name</th> <th>Location</th> <th>Planned start</th> - <th>Startlist</th> <th>New/delete</th> </tr> </thead> <tbody> {heats.map(h => ( <tr key={h.id}> - <td className='right'>{new Date(h.created_at).toLocaleString()}</td> - <td>{h.name}</td> - <td>{h.location}</td> - <td className='right'>{h.planned_start}</td> - <td className='right'> - <button onClick={e => navigateToHeat(e, navigate, h.id)}>🏁 startlist</button> - </td> - <td className='right'><button onClick={e => deleteHeat(e, h.id, h.name)}>🗑️ delete</button></td> + <td data-title='Created at' className='right'>{new Date(h.created_at).toLocaleString()}</td> + <td data-title='Name'><Link to={generatePath('/startlist/:heatId', {heatId:h.id})}>{h.name}</Link></td> + <td data-title='Location'>{h.location}</td> + <td data-title='Planned start' className='right'>{h.planned_start}</td> + <td><button onClick={e => deleteHeat(e, h.id, h.name)}>🗑️ delete</button></td> </tr> ))} <tr> <td className='right'><i>* required</i></td> - <td> - <input type='text' name='name' defaultValue='Name' /> * + <td data-title='Name'> + <input type='text' name='name' defaultValue='Name *' /> </td> - <td> + <td data-title='Location'> <input type='text' name='location' defaultValue='Location' /> </td> - <td className='right'> + <td data-title='Planned start' className='right'> <input type='time' name='planned_start' /> </td> - <td></td> - <td className='right'> + <td> <button type='submit'>➕ new</button> </td> </tr> diff --git a/src/Leaderboard.jsx b/src/Leaderboard.jsx @@ -94,6 +94,11 @@ function rankByHeat(rankingComp) { } } +function getScores(i, h) { + const scores = i.heats.find(heat => heat.heatId === h.value)?.scores?.map(s => s.score).join(" + ") + return scores === "" ? 0 : scores +} + async function newHeatFromLeaderboard(e, {leaderboard, rankingComp, selectHeatRef, selectRankRef}) { e.preventDefault() @@ -148,12 +153,9 @@ async function newHeatFromLeaderboard(e, {leaderboard, rankingComp, selectHeatRe function NewHeatForm(leaderboard, rankingComp, selectHeatRef, selectRankRef) { return ( <div className='newHeatForm'> - <h2>New Heat from Leaderboard</h2> - <p> - Create new heat from the sorted leaderboard above. - </p> + <h2>New Heat from top N</h2> <p> - Includes favorite "top N" athletes in the next 🔥 heat. + Create new heat with top N athletes from the sorted leaderboard (<i>* required</i>). </p> <form method='post' onSubmit={e => newHeatFromLeaderboard( e, @@ -163,35 +165,31 @@ function NewHeatForm(leaderboard, rankingComp, selectHeatRef, selectRankRef) { selectRankRef )}> <table> - <tbody> + <thead> <tr> - <th>New heat name: *</th> - <td> - <input type='text' name='name' defaultValue='Name' /> - </td> + <th>New heat name</th> + <th>Location</th> + <th>Planned start</th> + <th>Include top N</th> + <td></td> </tr> + </thead> + <tbody> <tr> - <th>Location:</th> - <td> + <td data-title='New heat name'> + <input type='text' name='name' defaultValue='Name *' /> + </td> + <td data-title='Location'> <input type='text' name='location' defaultValue='Location' /> </td> - </tr> - <tr> - <th>Planned start:</th> - <td> + <td data-title='Planned start'> <input type='time' name='planned_start' /> </td> - </tr> - <tr> - <th>Include top N:</th> - <td> + <td data-title='Include top N'> <input type='number' name='size' defaultValue='10' /> </td> - </tr> - <tr> - <td>(<i>* required</i>)</td> <td> <button type='submit'>➕ new</button> </td> @@ -306,27 +304,39 @@ function Leaderboard({session}) { return ( <div> + <button disabled={!loading}>{loading ? '🔄 loading' : ''}</button> <div className='Leaderboard'> <header> - <div> - Heats to display: - <Select - closeMenuOnSelect={false} - isMulti - options={heatOpts} - onChange={h => setHeatSelection(h)} - ref={selectHeatRef} - /> - Rank by (in this order): - <Select - closeMenuOnSelect={false} - isMulti - options={rankOpts} - onChange={h => setRankingComp(h)} - ref={selectRankRef} - /> - </div> - <h1>Leaderboard <button disabled={!loading}>{loading ? '🔄 loading' : ''}</button></h1> + <table> + <thead> + <tr> + <th>Heats to display</th> + <th>Rank by</th> + </tr> + </thead> + <tbody> + <tr> + <td data-title='Heats to display'> + <Select + closeMenuOnSelect={false} + isMulti + options={heatOpts} + onChange={h => setHeatSelection(h)} + ref={selectHeatRef} + /> + </td> + <td data-title='Rank by'> + <Select + closeMenuOnSelect={false} + isMulti + options={rankOpts} + onChange={h => setRankingComp(h)} + ref={selectRankRef} + /> + </td> + </tr> + </tbody> + </table> </header> <table className='leaderboard'> <thead> @@ -337,21 +347,11 @@ function Leaderboard({session}) { <th>Lastname</th> <th>Birthday</th> <th>School</th> - <th colSpan={heatSelection.length * 2}>Heat Scores & Sum</th> - <th colSpan={3}>Summary (all heats)</th> - </tr> - <tr> - <th></th> - <th></th> - <th></th> - <th></th> - <th></th> - <th></th> {heatSelection.map(h => ( - <th key={h.value} colSpan={2}>{h.label}</th> + <th key={h.value}>{h.label}</th> ))} - <th>👍 Best</th> - <th>👎 Worst</th> + <th>Best</th> + <th>Worst</th> <th>Total</th> </tr> </thead> @@ -366,13 +366,12 @@ function Leaderboard({session}) { <td data-title='School'>{i.school}</td> {heatSelection.map(h => ( <Fragment key={h.value}> - <td data-title={'Scores ' + h.label} className='right'>{i.heats.find(heat => heat.heatId === h.value)?.scores?.map(s => s.score).join(" / ")}</td> - <td data-title='Sum' className='right'>{i.heats.find(heat => heat.heatId === h.value)?.summary}</td> + <td className='right' data-title={h.label}>{getScores(i, h)} = {i.heats.find(heat => heat.heatId === h.value)?.summary}</td> </Fragment> ))} - <td data-title='👍 Best' className='right'>{i.bestHeat}</td> - <td data-title='👎 Worst' className='right'>{i.worstHeat}</td> - <td data-title='Total' className='right'>{i.sum}</td> + <td className='right' data-title='Best'>{i.bestHeat}</td> + <td className='right' data-title='Worst'>{i.worstHeat}</td> + <td className='right' data-title='Total'>{i.sum}</td> </tr> ))} </tbody> diff --git a/src/Score.jsx b/src/Score.jsx @@ -20,6 +20,7 @@ function ScoringForm({session}) { const [athleteOpts, setAthleteOpts] = useState([]) const [athleteSelection, setAthleteSelection] = useState(0) const [score, setScore] = useState(0) + const [loading, setLoading] = useState(false) // add options to select or rank by heat const heatOpts = heats.map(h => { @@ -31,13 +32,16 @@ function ScoringForm({session}) { useEffect(() => { (async () => { + setLoading(true) const heatList = await supabase.from('heats').select() setHeats(heatList.data) const startlist = await getStartlistForHeats([heatSelection.value]) - if (startlist.error) + if (startlist.error) { + setLoading(false) return + } setAthleteOpts(startlist.data.map(s => { return { @@ -46,8 +50,10 @@ function ScoringForm({session}) { } })) - if (heatSelection.value === undefined || athleteSelection.value === undefined) + if (heatSelection.value === undefined || athleteSelection.value === undefined) { + setLoading(false) return + } // check if existing score for heat and athlete exists const currentScore = await supabase.from('scores').select() @@ -65,29 +71,46 @@ function ScoringForm({session}) { athleteSelection.value, session.user.id) } + setLoading(false) })(); }, [heatSelection, athleteSelection, session.user.id, score]); return ( <div> - <h1>Score Athletes</h1> - Heat: - <Select - options={heatOpts} - onChange={h => { setHeatSelection(h); setScore(0) }} - /> - Athlete: - <Select - options={athleteOpts} - onChange={a => { setAthleteSelection(a); setScore(0) }} - /> - Score: - <input - type="number" - size="5" - value={score} - onChange={(e) => setScore(e.target.value)} - /> + <button disabled={!loading}>{loading ? '🔄 loading' : ''}</button> + <table> + <thead> + <tr> + <th>Heat</th> + <th>Athlete</th> + <th>Score</th> + </tr> + </thead> + <tbody> + <tr> + <td data-title='Heat'> + <Select + options={heatOpts} + onChange={h => { setHeatSelection(h); setScore(0) }} + /> + </td> + <td data-title='Athlete'> + <Select + options={athleteOpts} + onChange={a => { setAthleteSelection(a); setScore(0) }} + /> + </td> + <td data-title='Score'> + <input + className='scoreInput' + type="number" + value={score} + onChange={(e) => setScore(e.target.value)} + /> + </td> + </tr> + </tbody> + </table> </div> ) } diff --git a/src/Startlist.jsx b/src/Startlist.jsx @@ -92,17 +92,18 @@ function StartlistForm({heatId}) { return ( <div> - <h1>Startlist #{heatId} {heatName} <button disabled={!loading}>{loading ? '🔄 loading' : ''}</button></h1> + <button disabled={!loading}>{loading ? '🔄 loading' : ''}</button> + <h1>Startlist #{heatId} {heatName}</h1> <div className='heatInfo'> <ul> - <li><b>Location:</b>&emsp;&emsp;&emsp;{heatLocation ? heatLocation : 'n/a'}</li> - <li><b>Planned start:</b>&emsp;{heatStart ? heatStart : 'n/a'}</li> + <li><b>Location:</b> {heatLocation ? heatLocation : 'n/a'}</li> + <li><b>Planned start:</b> {heatStart ? heatStart : 'n/a'}</li> </ul> </div> <table> <thead> <tr> - <th>Nr</th> + <th>Start Nr.</th> <th>Firstname</th> <th>Lastname</th> <th>Birthday</th> @@ -113,12 +114,12 @@ function StartlistForm({heatId}) { <tbody> {startlist.map(i => ( <tr key={i.id}> - <td className='right'>{i.athlete.nr}</td> - <td>{i.athlete.firstname}</td> - <td>{i.athlete.lastname}</td> - <td className='right'>{i.athlete.birthday}</td> - <td>{i.athlete.school}</td> - <td className='right'><button onClick={e => removeAthleteFromHeat( + <td data-title='Start Nr.' className='right'>{i.athlete.nr}</td> + <td data-title='Firstname'>{i.athlete.firstname}</td> + <td data-title='Lastname'>{i.athlete.lastname}</td> + <td data-title='Birthday'>{i.athlete.birthday}</td> + <td data-title='School'>{i.athlete.school}</td> + <td><button onClick={e => removeAthleteFromHeat( e, i.id, i.athlete.firstname, @@ -128,15 +129,13 @@ function StartlistForm({heatId}) { </tr> ))} <tr> - <td></td> - <td colSpan={2}> + <td data-title='Athlete' colSpan={5}> <Select options={athleteOpts} onChange={(e) => setselectedAthlete(e)} /> </td> - <td colSpan={2}></td> - <td className='right'> + <td> <button onClick={(e) => addAthleteToHeat(e, selectedAthlete, heatId)}>➕ add</button> </td> </tr> diff --git a/src/index.css b/src/index.css @@ -1,69 +1,31 @@ -:root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-text-size-adjust: 100%; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { +* { + padding: 0; margin: 0; - display: flex; - /* place-items: center; */ - min-width: 320px; - min-height: 100vh; + font-family: monospace; + font-size: 18px; } -h1 { - font-size: 3.2em; - line-height: 1.1; +a { + color: black; } button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; + background: none; + border: none; cursor: pointer; - transition: border-color 0.25s; + text-align: left; + box-shadow: 0 1px #c7c7c7; + background: white; + border: 1px solid #f7f7f7; + padding: 5px 10px; + width: 100%; + text-align: center; } -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; + +button:disabled { + visibility: hidden; } -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } +input { + width: 100%; }