Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save WildGenie/0fb4875bbacd7649ebe36ebbedefa530 to your computer and use it in GitHub Desktop.

Select an option

Save WildGenie/0fb4875bbacd7649ebe36ebbedefa530 to your computer and use it in GitHub Desktop.
F1 Leaderboard UI Framer Motion API Formula One

F1 Leaderboard UI Framer Motion API Formula One

I've been watching F1 for the last couple of seasons and wanted to recreate the live leaderboard that is shown during the race.

Utilizing React hooks, Framer Motion API (for the transitions), and some math, a user can simulate a race with this pen! Some things that can be added would be simulating pit stops, representing who has the fastest lap, and a way to display a drivers information when click on the list.

A Pen by Bilgehan Zeki ÖZAYTAÇ on CodePen.

License.

<!----------------------------
------ React Template ------
------------------------------>
<div id="root"><div>
const { motion, useMotionValue } = Motion;
// add pitstop implementation
// add fastest lap feature
const API = [
{
'name' : 'Daniel Ricciardo',
'abb' : 'RIC',
'team': 'Red Bull Racing-TAG Heuer',
'color' : 'redBull',
'grid_position': 1,
'current_distance': 2000,
'weights': '1.001',
'tires': 'soft',
'number': '3',
'image': 'https://a.espncdn.com/combiner/i?img=/i/headshots/rpm/players/full/4510.png&h=112&w=112&scale=crop',
'flag': 'https://a.espncdn.com/i/teamlogos/countries/500/aus.png'
},
{
'name' : 'Max Verstappen',
'abb' : 'VER',
'team': 'Red Bull Racing-TAG Heuer',
'color' : 'redBull',
'grid_position': 2,
'current_distance': 1950,
'weights': '1.0015',
'tires': 'soft',
'number': '33',
'image': 'https://a.espncdn.com/combiner/i?img=/i/headshots/rpm/players/full/4665.png&h=128&w=128&scale=crop',
'flag': 'https://a.espncdn.com/i/teamlogos/countries/500/ned.png'
},
{ 'name': 'Lewis Hamiliton',
'abb' : 'HAM',
'team': 'Mercedes',
'color' : 'mercedes',
'grid_position': 3,
'current_distance': 1900,
'weights': '1.002',
'tires': 'med',
'number': '44',
'image': 'https://a.espncdn.com/combiner/i?img=/i/headshots/rpm/players/full/868.png&h=128&w=128&scale=crop',
'flag': 'https://a.espncdn.com/i/teamlogos/countries/500/gbr.png'
},
{
'name' : 'Sebastian Vettel',
'abb' : 'VET',
'team': 'Ferrari',
'color' : 'ferrari',
'grid_position': 4,
'current_distance': 1800,
'weights': '1.0016',
'tires': 'med',
'number': '5',
'image': 'https://a.espncdn.com/combiner/i?img=/i/headshots/rpm/players/full/864.png&h=128&w=128&scale=crop',
'flag': 'https://a.espncdn.com/i/teamlogos/countries/500/ger.png'
},
{
'name' : 'Valtteri Bottas',
'abb' : 'BOT',
'team': 'Mercedes',
'color' : 'mercedes',
'grid_position': 5,
'current_distance': 1750,
'weights': '1.001',
'tires': 'soft',
'number': '77',
'image': 'https://a.espncdn.com/combiner/i?img=/i/headshots/rpm/players/full/4520.png&h=128&w=128&scale=crop',
'flag': 'https://a.espncdn.com/i/teamlogos/countries/500/fin.png'
},
{
'name' : 'Kimi Räikkönen',
'abb' : 'RAI',
'team': 'Ferrari',
'color' : 'ferrari',
'grid_position': 6,
'current_distance': 1700,
'weights': '1.0009',
'tires': 'med',
'number': '7',
'image': 'https://a.espncdn.com/combiner/i?img=/i/headshots/rpm/players/full/337.png&h=128&w=128&scale=crop',
'flag': 'https://a.espncdn.com/i/teamlogos/countries/500/fin.png'
},
{
'name' : 'Nico Hülkenberg',
'abb' : 'HUL',
'team': 'Renault',
'color' : 'renault',
'grid_position': 7,
'current_distance': 1650,
'weights': '1.0006',
'tires': 'soft',
'number': '27',
'image': 'https://a.espncdn.com/combiner/i?img=/i/headshots/rpm/players/full/4396.png&h=128&w=128&scale=crop',
'flag': 'https://a.espncdn.com/i/teamlogos/countries/500/ger.png'
},
{
'name' : 'Carlos Sainz Jr.',
'abb' : 'SAI',
'team': 'Renault',
'color' : 'renault',
'grid_position': 8,
'current_distance': 1560,
'weights': '1.0005',
'tires': 'med',
'number': '55',
'image': 'https://a.espncdn.com/combiner/i?img=/i/headshots/rpm/players/full/4686.png&h=128&w=128&scale=crop',
'flag': 'https://a.espncdn.com/i/teamlogos/countries/500/esp.png'
},
{
'name' : 'Charles Leclerc',
'abb' : 'LEC',
'team': 'Sauber-Ferrari',
'color' : 'sauber',
'grid_position': 9,
'current_distance': 1450,
'weights': '1.0009',
'tires': 'hard',
'number': '16',
'image': 'https://a.espncdn.com/combiner/i?img=/i/headshots/rpm/players/full/5498.png&h=128&w=128&scale=crop',
'flag': 'https://a.espncdn.com/i/teamlogos/countries/500/mon.png'
},
{
'name' : 'Marcus Ericsson',
'abb' : 'ERI',
'team': 'Sauber-Ferrari',
'color' : 'sauber',
'grid_position': 10,
'current_distance': 1420,
'weights': '1.0004',
'tires': 'soft',
'number': '9',
'image': 'https://a.espncdn.com/combiner/i?img=/i/headshots/rpm/players/full/4624.png&h=128&w=128&scale=crop',
'flag': 'https://a.espncdn.com/i/teamlogos/countries/500/rus.png'
},
{
'name' : 'Esteban Ocon',
'abb' : 'OCO',
'team': 'Force India-Mercedes',
'color' : 'forceIndia',
'grid_position': 11,
'current_distance': 1300,
'weights': '1.0003',
'tires': 'hard',
'number': '31',
'image': 'https://a.espncdn.com/combiner/i?img=/i/headshots/rpm/players/full/4624.png&h=128&w=128&scale=crop',
'flag': 'https://a.espncdn.com/i/teamlogos/countries/500/rus.png'
},
{
'name' : 'Fernando Alonso',
'abb' : 'ALO',
'team': 'McLaren',
'color' : 'mclaren',
'grid_position': 12,
'current_distance': 1210,
'weights': '1.0003',
'tires': 'med',
'number': '14',
'image': 'https://a.espncdn.com/combiner/i?img=/i/headshots/rpm/players/full/4624.png&h=128&w=128&scale=crop',
'flag': 'https://a.espncdn.com/i/teamlogos/countries/500/rus.png'
},
{
'name' : 'Sergio Pérez',
'abb' : 'PER',
'team': 'Force India-Mercedes ',
'color' : 'forceIndia',
'grid_position': 13,
'current_distance': 1100,
'weights': '1.0002',
'tires': 'hard',
'number': '11',
'image': 'https://a.espncdn.com/combiner/i?img=/i/headshots/rpm/players/full/4472.png&h=128&w=128&scale=crop',
'flag': 'https://a.espncdn.com/i/teamlogos/countries/500/mex.png'
},
{
'name' : 'Brendon Hartley',
'abb' : 'HAR',
'team': 'Scuderia Toro Rosso-Honda',
'color' : 'toroRosso',
'grid_position': 14,
'current_distance': 1000,
'weights': '1.0001',
'tires': 'soft',
'number': '28',
'image': 'https://a.espncdn.com/combiner/i?img=/i/headshots/rpm/players/full/4624.png&h=128&w=128&scale=crop',
'flag': 'https://a.espncdn.com/i/teamlogos/countries/500/rus.png'
},
{
'name' : 'Pierre Gasly',
'abb' : 'GAS',
'team': 'Scuderia Toro Rosso-Honda',
'color' : 'toroRosso',
'grid_position': 15,
'current_distance': 900,
'weights': '1.0002',
'tires': 'hard',
'number': '10',
'image': 'https://a.espncdn.com/combiner/i?img=/i/headshots/rpm/players/full/5501.png&h=128&w=128&scale=crop',
'flag': 'https://a.espncdn.com/i/teamlogos/countries/500/fra.png'
},
{
'name' : 'Romain Grosjean',
'abb' : 'GRO',
'team': 'Hass-Ferrari',
'color' : 'haas',
'grid_position': 16,
'current_distance': 800,
'weights': '1.0002',
'tires': 'med',
'number': '8',
'image': 'https://a.espncdn.com/combiner/i?img=/i/headshots/rpm/players/full/4374.png&h=128&w=128&scale=crop',
'flag': 'https://a.espncdn.com/i/teamlogos/countries/500/fra.png'
},
{
'name' : 'Stoffel Vandoorne',
'abb' : 'VAN',
'team': 'McLaren-Renault',
'color' : 'mclaren',
'grid_position': 17,
'current_distance': 770,
'weights': '1.0001',
'tires': 'soft',
'number': '2',
'image': 'https://a.espncdn.com/combiner/i?img=/i/headshots/rpm/players/full/4624.png&h=128&w=128&scale=crop',
'flag': 'https://a.espncdn.com/i/teamlogos/countries/500/rus.png'
},
{
'name' : 'Kevin Magnussen',
'abb' : 'MAG',
'team': 'Haas-Ferrari',
'color' : 'haas',
'grid_position': 18,
'current_distance': 750,
'weights': '1.0001',
'tires': 'hard',
'number': '20',
'image': 'https://a.espncdn.com/combiner/i?img=/i/headshots/rpm/players/full/4623.png&h=128&w=128&scale=crop',
'flag': 'https://a.espncdn.com/i/teamlogos/countries/500/den.png'
},
{
'name' : 'Lance Stroll',
'abb' : 'STR',
'team': 'Williams-Mercedes',
'color' : 'williams',
'grid_position': 19,
'current_distance': 650,
'weights': '1.0001',
'tires': 'soft',
'number': '18',
'image': 'https://a.espncdn.com/combiner/i?img=/i/headshots/rpm/players/full/4775.png&h=128&w=128&scale=crop',
'flag': 'https://a.espncdn.com/i/teamlogos/countries/500/can.png'
},
{
'name' : 'Sergey Sirotkin',
'abb' : 'SIR',
'team': 'Williams-Mercedes',
'color' : 'williams',
'grid_position': 20,
'current_distance': 600,
'weights': '1.0001',
'tires': 'hard',
'number': '35',
'image': 'https://a.espncdn.com/combiner/i?img=/i/headshots/rpm/players/full/4624.png&h=128&w=128&scale=crop',
'flag': 'https://a.espncdn.com/i/teamlogos/countries/500/rus.png'
}
];
const Tyre = (size) => {
return (
<div class='tyreContainer'>
<span class={`leftArc ${size}`}/><span class="size">{size[0]}</span><span class={`rightArc ${size}`}/>
</div>
)
}
const getProperOrdinalNumber = (number) => {
if (number === 1) {
return 'st';
} else if (number === 2) {
return 'nd';
} else if (number === 3) {
return 'rd';
} else {
return 'th';
}
}
const DetailCard = (driver) => {
const [side, toggleSide] = React.useState('front');
const dude = driver.driver;
return (
<div class='detailCardContainer' onClick={()=>side==='front' ? toggleSide('back') : toggleSide('front') }>
<div class='detailCardTop flex'>
<div class="driverPosition">{dude.current_position || dude.grid_position}</div> <span class={`line ${dude.color}`}></span> <span class='driverName'>{dude.name.split(' ')[0]}</span>
<span class='driverName bold uppercase'>{dude.name.split(' ')[1]}</span><span class='driverNumber'>#{dude.number}</span>
</div>
{side === 'front' ? (
<div class='detailCardBottom flex'>
<div class='center quarter'>
<div class='largeText'>{dude.grid_position}<span class='ordinal'>{getProperOrdinalNumber(dude.grid_position)}</span></div>
<div class='smallText'>STARTED</div>
</div>
<div class='center quarter'>
<div class='largeText'>
<div class="driverInterval">
{dude.difference > 0 ? <div><span class='uparrow'>&#x25B2;</span> {String(dude.difference)}</div> :
dude.difference < 0 ?
<div><span class='downarrow'>&#x25BC;</span> {String(Math.abs(dude.difference))}</div>
: <div><span class='flatline'/> 0</div>
}
</div>
</div>
<div class='smallText'>PLACES</div>
</div>
<div class='center quarter'>
<div class='largeTire'>{Tyre(dude.tires)}</div>
<div class='smallText uppercase'>{dude.tires}</div>
</div>
<div class='center quarter'>
<div class='largeText'>0</div>
<div class='smallText'>PIT STOPS</div>
</div>
</div>
) : (
<div class='flex'>
<img class='profileimage' src={dude.image} alt='profile'/>
<img class='flag' src={dude.flag} alt='flag'/>
<p class='teamdetail'>{dude.team}</p>
</div>
)}
</div>
)
}
const Lap = (props) => {
return (
<section id='lap'>
<div>LAP</div>
<div id='seperator'></div>
<div>{Math.ceil(((props.lap.current_distance / 305354)) * 71) || 1} / 71</div>
</section>
)
}
const convertDistanceToTime = (lead, follow) => {
let difference = follow - lead;
return `+${Math.abs(difference * .055).toFixed(3)}`;
}
const Leaderboard = (props) => {
const spring = {
type: "tween",
damping: 100,
stiffness: 500,
mass: 2.5,
restDelta: 6,
duration: 1,
ease: 'easeOut'
};
return (
<section id='leaderboard'>
{props.view === 'gain' ? <div id='detailtitle'>GAINED/LOST</div> : props.view === 'tyres' ? <div id='detailtitle'>CURRENT TYRES</div> : null}
{props.activeDrivers.map((driver, driverIndex, allDrivers )=> {
const lastDriver = driverIndex !== 0 ?allDrivers[driverIndex - 1] : null;
const difference = driver.grid_position - (driverIndex + 1);
const DriverInterval = (<div class="driverInterval">
{driverIndex === 0 ? 'Interval' : convertDistanceToTime(lastDriver.current_distance, driver.current_distance)}
</div>)
const GainedLost = (
<div class="driverInterval">
{difference > 0 ? <div><span class='uparrow'>&#x25B2;</span> {String(difference)}</div> :
difference < 0 ?
<div><span class='downarrow'>&#x25BC;</span> {String(Math.abs(difference))}</div>
: <div><span class='flatline'/> 0</div>
}
</div>
)
const Tyrez = (<div class='tireDetail flex'>{Tyre(driver.tires)} <span class='tireHardness'>{driver.tires}</span></div>)
return (
<motion.div
key={driver.name}
layoutTransition={spring}
>
<div class="driverContainer" key={driver.abb} onClick={()=>props.selectDriver({...driver, current_position: driverIndex + 1, difference: difference})}>
<div class='driverLeft'>
<div class="driverPosition">{driverIndex + 1}</div>
<span class={`line ${driver.color}`}></span>
<div class="driverName">{driver.abb}</div>
</div>
<div class='driverRight'>
{props.view === 'interval' ? DriverInterval : props.view === 'gain' ? GainedLost: Tyrez}
</div>
</div>
</motion.div>
)
})}
</section>
)
}
const Inactive = (props) => {
return (
<section id='inactive'>
{props.inActiveDrivers && props.inActiveDrivers.length? (
<div>
{props.inActiveDrivers.map(driver => {
return (
<div class="driverContainer driverInactive" key={driver.abb}>
<div class='driverLeft'>
<span class={`line ${driver.color}`}></span>
<div class="driverName">{driver.abb}</div>
</div>
<div class='driverRight'>
OUT
</div>
</div>
)
})}
</div>
) : null}
</section>
)
}
const App = () => {
const [view, toggleView] = React.useState('interval');
const [activeDrivers, updateDrivers] = React.useState(API);
const [driver, selectDriver] = React.useState(activeDrivers[0]);
const [inActiveDrivers, updateInActiveDrivers] = React.useState([]);
const outDriverSet = new Set();
// rough estimation on who will be inactive from race
const dnf = () => Math.floor(Math.random() * 25000) === 415;
const simulateRace = () => {
let updated = [];
let temp = [];
activeDrivers.forEach(driver => {
driver.current_distance += Math.floor(driver.weights * (Math.random() * 10) + 10);
let out = dnf();
if (out) {
if (!outDriverSet.has(driver.name) && activeDrivers.length > 6) {
temp.push(driver);
outDriverSet.add(driver.name);
}
} else if (!outDriverSet.has(driver.name)) {
updated.push(driver);
}
})
updated.sort((a,b) => {
return b.current_distance - a.current_distance;
})
updateState(updated, temp);
}
const updateState = (active, inactive) => {
updateDrivers(result => [...active]);
updateInActiveDrivers(result => [...result, ...inactive]);
}
const startRace = () => {
setInterval(()=> simulateRace(), 150);
}
return (
<section id='container'>
<Lap lap={activeDrivers[0]}/>
<Leaderboard activeDrivers={activeDrivers} view={view} selectDriver={selectDriver}/>
<Inactive inActiveDrivers={inActiveDrivers}/>
<div id='controls'>
<button id='startRace' onClick={startRace}>Start Race!</button>
<div class='ferrari'>
<p style={{textAlign: 'center'}}>Change Views</p>
<button class='control' onClick={()=>toggleView('gain')}>Gain/Lost</button>
<button class='control' onClick={()=>toggleView('interval')}>Interval</button>
<button class='control' onClick={()=>toggleView('tyres')}>Tyres</button>
</div>
</div>
<DetailCard driver={driver}/>
</section>
)
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/[email protected]/dist/framer-motion.js"></script>
<script src="https://pro.fontawesome.com/releases/v5.6.1/js/all.js"></script>
@import url("https://fonts.googleapis.com/css?family=Orbitron:300,400,500");
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.redBull {
background: #1e41ff;
}
.mercedes {
background: #00d2be;
}
.ferrari {
background: #dc0000;
}
.sauber {
background: #6f0122;
}
.renault {
background: #fff500;
}
.forceIndia {
background: #f596c8;
}
.toroRosso {
background: #469bff;
}
.mclaren {
background: #ff8700;
}
.haas {
background: #bd9e57;
}
.williams {
background: #ffffff;
}
.flex {
display: flex;
}
html,
body {
height: 100%;
wdith: 100%;
margin: 0;
background: url(https://www.telegraph.co.uk/content/dam/formula-1/2019/05/22/TELEMMGLPICT000135384744_trans_NvBQzQNjv4Bqek9vKm18v_rkIPH9w2GMNtm3NAjPW-2_OvjCiS6COCU.jpeg)
left bottom;
background-size: cover;
color: #fff;
font-family: "Orbitron", sans-serif;
}
#container {
position: relative;
top: 10px;
left: 60px;
background: transparent;
width: 200px;
height: 100%;
display: flex;
flex-direction: column;
}
#lap {
background: black;
width: 110px;
font-size: 20px;
text-align: center;
border-radius: 2px 8px 0 0;
#seperator {
margin: 5px auto;
width: 60%;
height: 2px;
background: white;
border-radius: 50%;
}
}
.driverContainer {
display: flex;
width: 200px;
height: 30px;
background: transparent;
transition: all 0.5;
.driverLeft {
display: flex;
width: 110px;
background: black;
}
.line {
width: 4px;
height: 18px;
position: relative;
top: 5px;
left: 10px;
margin-right: 15px;
}
.driverPosition {
background: white;
color: black;
width: 29px;
height: 26px;
text-align: center;
font-weight: bold;
font-size: 14px;
line-height: 24px;
border-radius: 2px 2px 6px 2px;
margin: 2px;
}
.driverName {
width: 70px;
background: black;
margin: 2px;
font-size: 18px;
font-weight: bold;
text-align: left;
}
.driverRight {
width: 90px;
background: rgba(0, 0, 0, 0.5);
font-size: 15px;
padding: 3px;
text-align: right;
}
&:hover {
cursor: pointer;
}
}
#inactive {
padding-top: 3px;
height: 100%;
width: 200px;
background: transparent;
}
.driverInactive {
opacity: 0.7;
}
#controls {
position: absolute;
top: 50px;
left: 250px;
padding: 5px;
#startRace {
width: 120px;
height: 40px;
margin-bottom: 20px;
border-radius: 5px;
font-size: 20px;
box-shadow: 5px 10px 10px rgba(0, 0, 0, 0.4);
transition: all 1s;
&:hover {
cursor: pointer;
transform: translate(0px, -10px);
box-shadow: 5px 20px 10px rgba(0, 0, 0, 0.2);
}
}
.control {
width: 100%;
height: 30px;
padding: 5px;
&:hover {
cursor: pointer;
color: red;
}
}
}
.uparrow {
color: green;
}
.downarrow {
color: red;
}
.flatline {
background: grey;
width: 8px;
height: 3px;
position: relative;
margin: 5px;
display: inline-block;
}
#detailtitle {
width: 200px;
height: 25px;
border-top: 3px solid red;
margin-top: 3px;
text-align: center;
background: black;
color: white;
}
.tyreContainer {
position: relative;
background: black;
color: white;
border-radius: 50%;
width: 26px;
height: 24px;
.leftArc {
position: absolute;
width: 23px;
height: 12px;
background-color: transparent;
border-top-left-radius: 15px;
border-top-right-radius: 15px;
border: 3px solid red;
border-bottom: 0;
transform: rotate(90deg);
top: 6px;
left: 11px;
}
.rightArc {
position: absolute;
width: 23px;
height: 12px;
background-color: transparent;
border-top-left-radius: 15px;
border-top-right-radius: 15px;
border: 3px solid red;
border-bottom: 0;
transform: rotate(-90deg);
top: 6px;
left: -6px;
}
.size {
position: relative;
font-size: 16px;
font-weight: bold;
padding: 7px;
top: 2px;
text-transform: uppercase;
}
.soft {
border: 3px solid red;
border-bottom: 0;
}
.med {
border: 3px solid yellow;
border-bottom: 0;
}
.hard {
border: 3px solid white;
border-bottom: 0;
}
}
.tireDetail {
margin: auto;
.tireHardness {
width: 100%;
font-size: 14px;
font-weight: bold;
padding-left: 10px;
padding-top: 3px;
text-align: right;
text-transform: uppercase;
}
}
.detailCardContainer {
position: fixed;
// visibility: hidden;
bottom: 0;
left: 300px;
width: 400px;
height: 100px;
background: rgba(0, 0, 0, 0.5);
margin-bottom: 20px;
border-radius: 0 0 10px 0;
opacity: 1;
transition: all 0.25s ease-in-out;
&:hover {
opacity: 0.9;
cursor: pointer;
transform: translatey(-5px);
}
}
.detailCardTop {
width: 100%;
height: 35px;
line-height: 35px;
background: black;
}
.detailCardBottom {
width: 100%;
height: 65px;
justify-content: space-between;
background: transparent;
}
.quarter {
width: 100px;
}
.center {
margin: auto auto;
text-align: center;
}
.line {
width: 4px;
height: 25px;
position: relative;
top: 5px;
left: 5px;
margin-right: 15px;
}
.driverName {
font-size: 22px;
padding-right: 5px;
}
.driverPosition {
background: white;
color: black;
width: 34px;
height: 28px;
text-align: center;
font-weight: bold;
font-size: 22px;
line-height: 24px;
border-radius: 2px 2px 6px 2px;
margin: 3px;
}
.bold {
font-weight: bolder;
}
.uppercase {
text-transform: uppercase;
}
.driverNumber {
font-size: 24px;
padding-left: 10px;
background: transparent;
}
.ordinal {
font-size: 15px;
position: relative;
top: -6px;
}
.smallText {
font-size: 15px;
}
.largeText {
font-size: 25px;
}
.largeTire {
transform: scale(1.25);
margin: 5px 35px;
}
.profileimage {
height: 60px;
width: 60px;
border-radius: 50%;
border: 1px solid black;
background: white;
margin-left: 15px;
margin-top: 2px;
}
.flag {
position: relative;
height: auto;
width: 80px;
margin: auto 15px;
bottom: 8px;
}
.teamdetail {
font-size: 18px;
padding: 4px;
height: 65px;
text-transform: uppercase;
margin: auto;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment