diff --git a/CouncilDistrictsProvider.tsx b/CouncilDistrictsProvider.tsx new file mode 100644 index 0000000..9e834c2 --- /dev/null +++ b/CouncilDistrictsProvider.tsx @@ -0,0 +1,21 @@ +'use client'; + +import React from 'react'; + +import { createContext } from 'react'; + +export const CouncilDistrictsContext = createContext([]); + +export default function CouncilDistrictsProvider({ + councilDistricts, + children, +}: { + councilDistricts: number[]; + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/app/BoroughData.tsx b/app/BoroughData.tsx index 50dc58c..0127fee 100644 --- a/app/BoroughData.tsx +++ b/app/BoroughData.tsx @@ -5,21 +5,24 @@ import DataContainer from './DataContainer'; import { Granularity } from './constants'; import LoadingSpinner from './LoadingSpinner'; import Topline from './Topline'; +import { isBorough } from './utils'; +import { + Borough, + Timeframe, + getChartData, + getTimeframeData, + getToplineData, +} from './action'; import { Box, FormControl, InputLabel, MenuItem, Select, + SelectChangeEvent, Typography, } from '@mui/material'; import React, { memo } from 'react'; -import { - Timeframe, - getChartData, - getTimeframeData, - getToplineData, -} from './action'; export type BoroughDataFetcherFunction = ( borough: string, @@ -29,14 +32,21 @@ export type BoroughDataFetcherFunction = ( ) => Promise; export default memo(function BoroughData() { - const [borough, setBorough] = React.useState(undefined); + const [borough, setBorough] = React.useState(undefined); const [timeframe, setTimeframe] = React.useState( undefined, ); const [isLoading, setIsLoading] = React.useState(false); + function handleBoroughChange(event: SelectChangeEvent) { + const value = event.target.value; + if (isBorough(value)) { + setBorough(value); + } + } + React.useEffect(() => { - if (borough !== undefined) { + if (borough) { setIsLoading(true); setTimeframe(undefined); getTimeframeData({ dock: { borough } }).then((newData) => { @@ -53,7 +63,19 @@ export default memo(function BoroughData() { endDate: Date, ) => { const daily = granularity === Granularity.Daily; - return getChartData({ dock: { borough } }, daily, startDate, endDate); + + // Unfortunately due to how the function is used, we cannot require the + // borough parameter is Borough + if (isBorough(borough)) { + return getChartData({ dock: { borough } }, daily, startDate, endDate); + } else { + return getChartData( + { dock: { borough: 'Brooklyn' } }, + daily, + startDate, + endDate, + ); + } }; return ( @@ -66,7 +88,7 @@ export default memo(function BoroughData() { id='borough-options' value={borough} label='borough' - onChange={(e) => setBorough(e.target.value)} + onChange={handleBoroughChange} > The Bronx Brooklyn @@ -88,7 +110,7 @@ export default memo(function BoroughData() { )} - {!isLoading && borough === undefined && ( + {!isLoading && !borough && ( (undefined); - const [communityDistricts, setCommunityDistricts] = React.useState< - { communityDistrict: number; borough: string }[] - >([]); const [communityDistrict, setCommunityDistrict] = React.useState< number | undefined >(undefined); - const [communityDistrictsLoading, setCommunityDistrictsLoading] = - React.useState(false); const [timeframe, setTimeframe] = React.useState( undefined, ); const [isLoading, setIsLoading] = React.useState(false); - React.useEffect(() => { - setCommunityDistrictsLoading(true); - async function fn() { - const communityDistricts = await getCommunityDistricts(); - setCommunityDistricts([...communityDistricts]); - setCommunityDistrictsLoading(false); - } - fn(); - }, []); + const communityDistricts = useContext(CommunityDistrictsContext); React.useEffect(() => { if (communityDistrict !== undefined) { @@ -107,10 +94,9 @@ export default memo(function CommunityDistrictData() { - Community District{communityDistrictsLoading && 's Loading...'} + Community District )} - {!isLoading - && !councilDistrictsLoading - && councilDistrict === undefined && ( - - - <>Select a council district to see some data. - - - )} + {!isLoading && councilDistrict === undefined && ( + + + <>Select a council district to see some data. + + + )} {!isLoading && councilDistrict && timeframe !== undefined && ( ([]); + +export default function CouncilDistrictsProvider({ + councilDistricts, + children, +}: { + councilDistricts: CouncilDistrict[]; + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/app/DockData.tsx b/app/DockData.tsx index 5ae6c18..da4ba08 100644 --- a/app/DockData.tsx +++ b/app/DockData.tsx @@ -3,7 +3,9 @@ import DataContainer from './DataContainer'; import { Granularity } from './constants'; import LoadingSpinner from './LoadingSpinner'; +import { StationsContext } from './StationsProvider'; import Topline from './Topline'; +import { isBorough } from './utils'; import match from 'autosuggest-highlight/match'; import parse from 'autosuggest-highlight/parse'; import { @@ -11,20 +13,19 @@ import { Autocomplete, Box, Chip, - CircularProgress, FormControl, InputLabel, MenuItem, Select, + SelectChangeEvent, TextField, Typography, } from '@mui/material'; -import { ChartData, getMostRecentDateInDatabase } from './action'; -import React, { SyntheticEvent, memo } from 'react'; +import { Borough, ChartData, getMostRecentDateInDatabase } from './action'; +import React, { SyntheticEvent, memo, useContext } from 'react'; import { Timeframe, getChartData, - getDocks, getTimeframeData, getToplineData, } from './action'; @@ -50,10 +51,8 @@ function Bold({ children }: { children: string }) { } export default memo(function DockData() { - const [borough, setBorough] = React.useState('Brooklyn'); + const [borough, setBorough] = React.useState('Brooklyn'); const [dock, setDock] = React.useState({ name: '', id: 0 }); - const [docks, setDocks] = React.useState([]); - const [docksLoading, setDocksLoading] = React.useState(true); const [isLoading, setIsLoading] = React.useState(false); const [mostRecentMonth, setMostRecentMonth] = React.useState(0); const [mostRecentYear, setMostRecentYear] = React.useState(0); @@ -61,12 +60,26 @@ export default memo(function DockData() { undefined, ); + function clearDock() { + setDock({ name: '', id: 0 }); + } + + const docks = useContext(StationsContext)[borough]; + const dockNames = docks.map((d) => d.name).sort((a, b) => (a > b ? 1 : -1)); + function handleBoroughChange(event: SelectChangeEvent) { + const value = event.target.value; + + if (isBorough(value)) { + setBorough(borough); + clearDock(); + } + } + function handleDockChange(event: SyntheticEvent, value: string | null) { if (value === null || value === '') { - // use cleared the input - setDock({ name: '', id: 0 }); + clearDock(); } else { const id = docks.find((d) => d.name === value)?.id; if (id !== undefined) { @@ -88,17 +101,6 @@ export default memo(function DockData() { updateMostRecentMonthAndYear(); }, []); - React.useEffect(() => { - setDocksLoading(true); - setDock({ name: '', id: 0 }); - async function fn() { - const newDocks = await getDocks(borough); - setDocks([...newDocks]); - setDocksLoading(false); - } - fn(); - }, [borough]); - React.useEffect(() => { if (dock.name !== '') { setIsLoading(true); @@ -154,7 +156,7 @@ export default memo(function DockData() { id='borough-options' value={borough} label='borough' - onChange={(e) => setBorough(e.target.value)} + onChange={handleBoroughChange} > The Bronx Brooklyn @@ -167,7 +169,6 @@ export default memo(function DockData() { { @@ -196,17 +197,11 @@ export default memo(function DockData() { renderInput={(p) => ( - {docksLoading ? ( - - ) : ( - p.InputProps.endAdornment - )} - + {p.InputProps.endAdornment} ), }} /> @@ -261,24 +256,22 @@ export default memo(function DockData() { )} - {!isLoading - && !docksLoading - && (dock.name === '' || timeframe === undefined) && ( - - - <> - Select a {borough} station to see some data. - - - - )} + {!isLoading && (dock.name === '' || timeframe === undefined) && ( + + + <> + Select a {borough} station to see some data. + + + + )} {!isLoading && dock.name !== '' && timeframe !== undefined && ( ({ + Bronx: [], + Brooklyn: [], + Manhattan: [], + Queens: [], +}); + +export default function StationsProvider({ + stations, + children, +}: { + stations: Stations; + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/app/action.ts b/app/action.ts index 8b2cebf..2f03ad6 100644 --- a/app/action.ts +++ b/app/action.ts @@ -1,10 +1,37 @@ 'use server'; +import { isBorough } from './utils'; + import prisma from '@/prisma/db'; +export type Borough = 'Bronx' | 'Brooklyn' | 'Manhattan' | 'Queens'; + +export interface Station { + borough: Borough; + id: number; + name: string; +} + +export interface Stations { + Bronx: Station[]; + Brooklyn: Station[]; + Manhattan: Station[]; + Queens: Station[]; +} + +export interface CommunityDistrict { + communityDistrict: number; + borough: Borough; +} + +export interface CouncilDistrict { + councilDistrict: number; + borough: Borough; +} + interface BoroughSpecifier { dock: { - borough: string; + borough: Borough; }; } @@ -173,9 +200,36 @@ export async function getChartData( })); } -export async function getDocks(borough: string) { - const queryResults = await prisma.dock.findMany({ where: { borough } }); - return queryResults; +export async function getDocks(): Promise { + const stations: Stations = { + Bronx: [], + Brooklyn: [], + Manhattan: [], + Queens: [], + }; + + interface StationResult { + name: string; + borough: string | null; + id: number; + } + + function isValidStation(obj: StationResult): obj is Station { + return obj.borough !== null && isBorough(obj.borough); + } + + const queryResults = await prisma.dock.findMany({ + where: { NOT: { borough: null } }, + select: { id: true, name: true, borough: true }, + }); + + const validStations = queryResults.filter(isValidStation); + + validStations.forEach((station) => { + stations[station.borough].push(station); + }); + + return stations; } // This function is extremely messy because for some reason prisma does not @@ -187,14 +241,9 @@ export async function getCouncilDistricts() { borough: string | null; } - interface ValidCouncilDistrict { - councilDistrict: number; - borough: string; - } - function isValidCouncilDistrict( obj: CouncilDistrictResult, - ): obj is ValidCouncilDistrict { + ): obj is CouncilDistrict { return obj.councilDistrict !== null && obj.borough !== null; } @@ -223,14 +272,9 @@ export async function getCommunityDistricts() { borough: string | null; } - interface ValidCommunityDistrict { - communityDistrict: number; - borough: string; - } - function isValidCommunityDistrict( obj: CommunityDistrictResult, - ): obj is ValidCommunityDistrict { + ): obj is CommunityDistrict { return obj.communityDistrict !== null && obj.borough !== null; } diff --git a/app/layout.tsx b/app/layout.tsx index 35732c8..1afcb70 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,9 +1,14 @@ import './globals.css'; import { Analytics } from '@vercel/analytics/react'; +import CommunityDistrictsProvider from './CommunityDistrictsProvider'; +import CouncilDistrictsProvider from './CouncilDistrictsProvider'; import type { Metadata } from 'next'; import React from 'react'; +import StationsProvider from './StationsProvider'; import ThemeProvider from './ThemeProvider'; +import { getCommunityDistricts, getCouncilDistricts, getDocks } from './action'; + export const metadata: Metadata = { title: 'Citi Bike Dock Data', description: 'Explore trip data for your favorite Citi Bike docks', @@ -13,14 +18,24 @@ interface RootLayoutProps { children: React.ReactNode; } -export default function RootLayout({ children }: RootLayoutProps) { +export default async function RootLayout({ children }: RootLayoutProps) { + const stations = await getDocks(); + const councilDistricts = await getCouncilDistricts(); + const communityDistricts = await getCommunityDistricts(); + return ( - - {children} - - + + + + + {children} + + + + + ); diff --git a/app/utils.tsx b/app/utils.tsx new file mode 100644 index 0000000..9c96cab --- /dev/null +++ b/app/utils.tsx @@ -0,0 +1,10 @@ +function isBorough(value: string) { + return ( + value === 'Bronx' + || value === 'Brooklyn' + || value === 'Manhattan' + || value === 'Queens' + ); +} + +export { isBorough };