initial
This commit is contained in:
39
src/App.css
39
src/App.css
@@ -1,38 +1,7 @@
|
|||||||
.App {
|
html {
|
||||||
text-align: center;
|
background-color: #313340;
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-logo {
|
* {
|
||||||
height: 40vmin;
|
font-family: 'Roboto Mono';
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
.App-logo {
|
|
||||||
animation: App-logo-spin infinite 20s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-header {
|
|
||||||
background-color: #282c34;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: calc(10px + 2vmin);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-link {
|
|
||||||
color: #61dafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes App-logo-spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
124
src/App.js
124
src/App.js
@@ -1,24 +1,118 @@
|
|||||||
import logo from './logo.svg';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
import '@fontsource/roboto-mono';
|
||||||
|
import Schedule from './components/schedule/Schedule';
|
||||||
|
import Radar from './components/Radar';
|
||||||
|
import { format, formatDistanceToNow } from 'date-fns';
|
||||||
|
import Metar from './components/metar/Metar';
|
||||||
|
|
||||||
|
const statuses = {
|
||||||
|
OPEN: 'ON TIME',
|
||||||
|
};
|
||||||
|
const days = [
|
||||||
|
'Sunday',
|
||||||
|
'Monday',
|
||||||
|
'Tuesday',
|
||||||
|
'Wednesday',
|
||||||
|
'Thursday',
|
||||||
|
'Friday',
|
||||||
|
'Saturday',
|
||||||
|
];
|
||||||
|
const pad = n => String(n).padStart(2, '0');
|
||||||
|
|
||||||
|
function formatUtcDate(date) {
|
||||||
|
return (
|
||||||
|
`${days[date.getUTCDay()]} ${pad(date.getUTCDate())}/${pad(date.getUTCMonth() + 1)}/${date.getUTCFullYear()} ` +
|
||||||
|
`${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())}Z`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const normaliseData = data => {
|
||||||
|
return data.map(d => {
|
||||||
|
const date = format(d.start, 'dd/MM');
|
||||||
|
const start = format(d.start, 'HH:mm');
|
||||||
|
const end = format(d.end, 'HH:mm');
|
||||||
|
const status = statuses[d.status] ?? d.status;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...d,
|
||||||
|
date,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
status,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const [time, setTime] = useState(new Date());
|
||||||
|
const [data, setData] = useState({});
|
||||||
|
const [dataUpdated, setDataUpdated] = useState(new Date());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timeInterval = setInterval(() => {
|
||||||
|
setTime(new Date());
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => clearInterval(timeInterval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch('https://mattfidd.rocks/veroDashboard.json');
|
||||||
|
const json = await res.json();
|
||||||
|
setData(normaliseData(json.slice(1)));
|
||||||
|
setDataUpdated(json[0].updatedAt);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching data:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
fetchData();
|
||||||
|
}, 5 * 1000); // every 5 seconds
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const formattedTime = useMemo(() => {
|
||||||
|
return formatUtcDate(time);
|
||||||
|
}, [time]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<>
|
||||||
<header className="App-header">
|
<h1
|
||||||
<img src={logo} className="App-logo" alt="logo" />
|
style={{
|
||||||
<p>
|
color: '#E0A951',
|
||||||
Edit <code>src/App.js</code> and save to reload.
|
textAlign: 'center',
|
||||||
</p>
|
fontSize: '48px',
|
||||||
<a
|
}}
|
||||||
className="App-link"
|
|
||||||
href="https://reactjs.org"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
>
|
||||||
Learn React
|
{formattedTime}
|
||||||
</a>
|
</h1>
|
||||||
</header>
|
|
||||||
|
<Schedule data={data} dataUpdated={dataUpdated} />
|
||||||
|
|
||||||
|
<Radar />
|
||||||
|
|
||||||
|
<Metar />
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: '1rem',
|
||||||
|
color: '#E0A951',
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: '12px',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Last updated {formatDistanceToNow(dataUpdated, { addSuffix: true })}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
23
src/components/Radar.jsx
Normal file
23
src/components/Radar.jsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Radar = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<iframe
|
||||||
|
title="radar"
|
||||||
|
width="1015"
|
||||||
|
height="662"
|
||||||
|
src="https://embed.windy.com/embed.html?type=map&location=coordinates&metricRain=default&metricTemp=°C&metricWind=kt&zoom=8&overlay=radar&product=radar&level=surface&lat=27.794&lon=-81.745&pressure=true&message=true&play=true"
|
||||||
|
frameborder="0"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Radar;
|
||||||
69
src/components/metar/Metar.jsx
Normal file
69
src/components/metar/Metar.jsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
const TafDisplay = ({ taf }) => {
|
||||||
|
const lines = taf
|
||||||
|
.split(/\s+(?=FM\d{6})/) // split on whitespace before "FM" lines
|
||||||
|
.map((line, index) => <div key={index}>{line}</div>);
|
||||||
|
|
||||||
|
return <div>{lines}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Metar = ({ data }) => {
|
||||||
|
const [metar, setMetar] = useState('');
|
||||||
|
const [taf, setTaf] = useState('');
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const [metarData, tafData] = await Promise.all([
|
||||||
|
fetch('https://mattfidd.rocks/proxy/metar').then(r => r.text()),
|
||||||
|
fetch('https://mattfidd.rocks/proxy/taf').then(r => r.text()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
setMetar(metarData);
|
||||||
|
setTaf(tafData);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching data:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
fetchData();
|
||||||
|
}, 60 * 1000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
margin: '1rem 0',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
minHeight: '395px',
|
||||||
|
maxHeight: '395px',
|
||||||
|
minWidth: '1015px',
|
||||||
|
maxWidth: '1015px',
|
||||||
|
backgroundColor: '#23252A',
|
||||||
|
borderRadius: '8px',
|
||||||
|
overflow: 'scroll',
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ padding: '.5rem 1rem' }}>
|
||||||
|
<p>{metar}</p>
|
||||||
|
<TafDisplay taf={taf} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Metar;
|
||||||
92
src/components/schedule/Schedule.jsx
Normal file
92
src/components/schedule/Schedule.jsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import ScheduleRow from './ScheduleRow';
|
||||||
|
import ScheduleCell from './ScheduleCell';
|
||||||
|
|
||||||
|
const widths = [85, 85, 85, 85, 400, 150];
|
||||||
|
|
||||||
|
const Schedule = ({ data }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
margin: '1rem 0',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
minHeight: '662px',
|
||||||
|
maxHeight: '662px',
|
||||||
|
minWidth: '1015px',
|
||||||
|
backgroundColor: '#23252A',
|
||||||
|
borderRadius: '8px',
|
||||||
|
overflow: 'scroll',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ScheduleRow bg="0" border="true">
|
||||||
|
<ScheduleCell width={widths[0]} color="white">
|
||||||
|
Pilot
|
||||||
|
</ScheduleCell>
|
||||||
|
<ScheduleCell width={widths[1]} color="white">
|
||||||
|
Date
|
||||||
|
</ScheduleCell>
|
||||||
|
<ScheduleCell width={widths[2]} color="white">
|
||||||
|
Depart
|
||||||
|
</ScheduleCell>
|
||||||
|
<ScheduleCell width={widths[3]} color="white">
|
||||||
|
Arrive
|
||||||
|
</ScheduleCell>
|
||||||
|
<ScheduleCell width={widths[4]} color="white">
|
||||||
|
Destination
|
||||||
|
</ScheduleCell>
|
||||||
|
<ScheduleCell width="15" color="white"></ScheduleCell>
|
||||||
|
<ScheduleCell width={widths[5]} color="white">
|
||||||
|
Gate
|
||||||
|
</ScheduleCell>
|
||||||
|
<ScheduleCell end="true" color="white">
|
||||||
|
Status
|
||||||
|
</ScheduleCell>
|
||||||
|
</ScheduleRow>
|
||||||
|
|
||||||
|
{data?.length > 0 &&
|
||||||
|
data.map((row, i) => {
|
||||||
|
return (
|
||||||
|
<ScheduleRow bg={i % 2 === 1} key={row.id}>
|
||||||
|
<ScheduleCell width={widths[0]}>{row.name}</ScheduleCell>
|
||||||
|
<ScheduleCell width={widths[1]}>{row.date}</ScheduleCell>
|
||||||
|
<ScheduleCell width={widths[2]}>{row.start}</ScheduleCell>
|
||||||
|
<ScheduleCell width={widths[3]}>{row.end}</ScheduleCell>
|
||||||
|
<ScheduleCell width={widths[4]}>
|
||||||
|
{row.description?.length
|
||||||
|
? row.description
|
||||||
|
: row.backseat
|
||||||
|
? 'backseat'
|
||||||
|
: undefined}
|
||||||
|
</ScheduleCell>
|
||||||
|
<ScheduleCell width="15" color="white"></ScheduleCell>
|
||||||
|
<ScheduleCell width={widths[5]}>{row.location}</ScheduleCell>
|
||||||
|
<ScheduleCell
|
||||||
|
end="true"
|
||||||
|
color={
|
||||||
|
row.status.toUpperCase() === 'COMPLETED'
|
||||||
|
? '#6FB165'
|
||||||
|
: row.status.toUpperCase() === 'IN PROGRESS'
|
||||||
|
? '#3197E5'
|
||||||
|
: row.status.toUpperCase() === 'CANCELLED'
|
||||||
|
? '#E54331'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{row.status}
|
||||||
|
</ScheduleCell>
|
||||||
|
</ScheduleRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Schedule;
|
||||||
28
src/components/schedule/ScheduleCell.jsx
Normal file
28
src/components/schedule/ScheduleCell.jsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const ScheduleCell = ({
|
||||||
|
children,
|
||||||
|
width = '100',
|
||||||
|
color = '#E0A951',
|
||||||
|
end = false,
|
||||||
|
} = {}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
color: color,
|
||||||
|
minWidth: `${width}px`,
|
||||||
|
maxWidth: end ? 'unset' : `${width}px`,
|
||||||
|
flex: end ? 1 : 'inherit',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
fontSize: '18px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScheduleCell;
|
||||||
24
src/components/schedule/ScheduleRow.jsx
Normal file
24
src/components/schedule/ScheduleRow.jsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const bgs = {
|
||||||
|
true: 'none',
|
||||||
|
false: '#2F3136',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ScheduleRow = ({ children, bg, border }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
color: 'white',
|
||||||
|
padding: '.5rem 1rem',
|
||||||
|
borderBottom: border ? '1px solid grey' : 'none',
|
||||||
|
backgroundColor: bgs[bg],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScheduleRow;
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
font-family:
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
|
||||||
sans-serif;
|
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
font-family:
|
||||||
monospace;
|
source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const root = ReactDOM.createRoot(document.getElementById('root'));
|
|||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</React.StrictMode>
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// If you want to start measuring performance in your app, pass a function
|
// If you want to start measuring performance in your app, pass a function
|
||||||
|
|||||||
Reference in New Issue
Block a user