This commit is contained in:
Matt Fiddaman
2025-04-01 15:01:48 -04:00
parent 4242d44045
commit 1412825b8d
9 changed files with 357 additions and 58 deletions

View File

@@ -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);
}
} }

View File

@@ -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
View 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;

View 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;

View 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;

View 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;

View 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;

View File

@@ -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;
} }

View File

@@ -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