new sample Flight-tracker
This commit is contained in:
parent
6fe61af2ca
commit
10342b46b2
|
@ -5,10 +5,10 @@
|
||||||
This WebPart allows to track all flights from a selected airport, date and information type.
|
This WebPart allows to track all flights from a selected airport, date and information type.
|
||||||
The SPFx use external API (https://aerodatabox.p.rapidapi.com/flights/number/) to get data of flight, please see https://rapidapi.com/aedbx-aedbx/api/aerodatabox/ to get more information. There is some restritions on this API, the number of requests is limited (free version)
|
The SPFx use external API (https://aerodatabox.p.rapidapi.com/flights/number/) to get data of flight, please see https://rapidapi.com/aedbx-aedbx/api/aerodatabox/ to get more information. There is some restritions on this API, the number of requests is limited (free version)
|
||||||
|
|
||||||
![SharePoint View](assets/FQzNLB4XwAIFMRh.jpg "SharePoint View")
|
![SharePoint View](./src/assets/sharepoint.png "SharePoint View")
|
||||||
![Teams View](assets/FQzO9YjWUAgFlrU.jpg "Teams View")
|
![Teams View](./src/assets/teams.jpg "Teams View")
|
||||||
|
|
||||||
## Compatibility
|
React-
|
||||||
|
|
||||||
![SPFx 1.15](https://img.shields.io/badge/SPFx-1.15-green.svg)
|
![SPFx 1.15](https://img.shields.io/badge/SPFx-1.15-green.svg)
|
||||||
![Node.js v14 | v12](https://img.shields.io/badge/Node.js-v14%20%7C%20v12-green.svg)
|
![Node.js v14 | v12](https://img.shields.io/badge/Node.js-v14%20%7C%20v12-green.svg)
|
||||||
|
@ -39,25 +39,36 @@ The SPFx use external API (https://aerodatabox.p.rapidapi.com/flights/number/) t
|
||||||
|
|
||||||
Solution|Author(s)
|
Solution|Author(s)
|
||||||
--------|---------
|
--------|---------
|
||||||
react-fluentui-9 | [Nick Brown](https://github.com/techienickb) ([@techienickb](https://twitter.com/techienickb) / [Jisc](https://jisc.ac.uk))
|
react-flighttracker | [João Mendes](https://github.com/joaojmendes) ([@joaojmendes](https://twitter.com/joaojmendes)), StaffBase |
|
||||||
|
|
||||||
## Version history
|
## Version history
|
||||||
|
|
||||||
Version|Date|Comments
|
Version|Date|Comments
|
||||||
-------|----|--------
|
-------|----|--------
|
||||||
1.0|April 20, 2022|Initial release
|
1.0|November 11, 2022|Initial release
|
||||||
|
|
||||||
- Clone this repository (or [download this solution as a .ZIP file](https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-fluentui-9) then unzip it)
|
## Minimal Path to Awesome
|
||||||
- From your command-line, change your current directory to the directory containing this sample (`react-fluentui-9`, located under `samples`)
|
|
||||||
- in the command-line run:
|
- Clone this repository
|
||||||
|
- Ensure that you are at the solution folder
|
||||||
|
|
||||||
|
- in the command line run:
|
||||||
- `npm install`
|
- `npm install`
|
||||||
- `gulp serve`
|
- `gulp build --ship`
|
||||||
|
- `gulp bundle --ship`
|
||||||
|
- `gulp package-solution --ship`
|
||||||
|
- Browse to your SharePoint app catalog and load the SPFx package.
|
||||||
|
- Browse to your SharePoint Admin Center and under advanced you will need to open Api Access and allow the requests for Microsoft Graph.
|
||||||
|
- If you have the APIs permissions already allowed you can follow the below steps.
|
||||||
|
|
||||||
> This sample can also be opened with [VS Code Remote Development](https://code.visualstudio.com/docs/remote/remote-overview). Visit <https://aka.ms/spfx-devcontainer> for further instructions.
|
- in the command line run:
|
||||||
|
*`npm install`
|
||||||
|
* `gulp serve --nobrowser`
|
||||||
|
- browse to your hosted workbench [https://YOURTENANT.sharepoint.com/sites/_layouts/15/workbench.aspx](https://YOURTENANT.sharepoint.com/sites/_layouts/15/workbench.aspx) and add the adaptive card extension.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
Very simple demo, the handling of the theme provider is interesting implementing it and handling the custom themes SharePoint can use.
|
External API , Global State Management with Recoil Library, React Hooks , CCS-In-JS Styling
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
|
@ -88,4 +99,4 @@ Finally, if you have an idea for improvement, [make a suggestion](https://github
|
||||||
|
|
||||||
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
|
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
|
||||||
|
|
||||||
<img src="https://pnptelemetry.azurewebsites.net/sp-dev-fx-webparts/samples/react-fluentui-9" />
|
<img src="https://pnptelemetry.azurewebsites.net/sp-dev-fx-webparts/samples/react-flighttracler" />
|
||||||
|
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
Before Width: | Height: | Size: 453 KiB After Width: | Height: | Size: 453 KiB |
|
@ -25,6 +25,7 @@ export const FlightTrackerControl: React.FunctionComponent<IFlightTrackerProps>
|
||||||
context: context,
|
context: context,
|
||||||
numberItemsPerPage: numberItemsPerPage,
|
numberItemsPerPage: numberItemsPerPage,
|
||||||
webpartContainerWidth: webpartContainerWidth,
|
webpartContainerWidth: webpartContainerWidth,
|
||||||
|
|
||||||
});
|
});
|
||||||
}, [isDarkTheme, hasTeamsContext, currentTheme, context, setGlobalState, webpartContainerWidth]);
|
}, [isDarkTheme, hasTeamsContext, currentTheme, context, setGlobalState, webpartContainerWidth]);
|
||||||
|
|
||||||
|
|
|
@ -12,4 +12,5 @@ export interface IFlightTrackerProps {
|
||||||
displayMode: DisplayMode;
|
displayMode: DisplayMode;
|
||||||
updateProperty: (value: string) => void;
|
updateProperty: (value: string) => void;
|
||||||
webpartContainerWidth: number;
|
webpartContainerWidth: number;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,14 @@ export const FlightTrackerList: React.FunctionComponent<IFlightTrackerListProps>
|
||||||
const [appState, setGlobalState] = useRecoilState(globalState);
|
const [appState, setGlobalState] = useRecoilState(globalState);
|
||||||
const [airlineList, setAirlineList] = useRecoilState(airlineState);
|
const [airlineList, setAirlineList] = useRecoilState(airlineState);
|
||||||
const { mapFlightSchedules } = useMappingFlightSchedules();
|
const { mapFlightSchedules } = useMappingFlightSchedules();
|
||||||
const { selectedAirPort, selectedInformationType, selectedDate, numberItemsPerPage, selectedTime } = appState;
|
const {
|
||||||
|
selectedAirPort,
|
||||||
|
selectedInformationType,
|
||||||
|
selectedDate,
|
||||||
|
numberItemsPerPage,
|
||||||
|
selectedTime,
|
||||||
|
|
||||||
|
} = appState;
|
||||||
const [isLoadingItems, setIsLoadingItems] = React.useState<boolean>(true);
|
const [isLoadingItems, setIsLoadingItems] = React.useState<boolean>(true);
|
||||||
const [errorMessage, setErrorMessage] = React.useState<string>("");
|
const [errorMessage, setErrorMessage] = React.useState<string>("");
|
||||||
const [listItems, setListItems] = React.useState<IFlightTrackerListItem[]>([]);
|
const [listItems, setListItems] = React.useState<IFlightTrackerListItem[]>([]);
|
||||||
|
@ -51,6 +58,7 @@ export const FlightTrackerList: React.FunctionComponent<IFlightTrackerListProps>
|
||||||
const [hasMore, setHasMore] = React.useState<boolean>(true);
|
const [hasMore, setHasMore] = React.useState<boolean>(true);
|
||||||
const pageIndex = React.useRef<number>(0);
|
const pageIndex = React.useRef<number>(0);
|
||||||
const currentInformationType = React.useRef(selectedInformationType);
|
const currentInformationType = React.useRef(selectedInformationType);
|
||||||
|
const [timerId, setTimerId] = React.useState<number>(undefined);
|
||||||
|
|
||||||
const checkTypeInformationToScroll = React.useCallback(() => {
|
const checkTypeInformationToScroll = React.useCallback(() => {
|
||||||
if (selectedInformationType !== currentInformationType.current) {
|
if (selectedInformationType !== currentInformationType.current) {
|
||||||
|
@ -59,6 +67,20 @@ export const FlightTrackerList: React.FunctionComponent<IFlightTrackerListProps>
|
||||||
}
|
}
|
||||||
}, [selectedInformationType]);
|
}, [selectedInformationType]);
|
||||||
|
|
||||||
|
const onRefresh = React.useCallback(async () => {
|
||||||
|
pageIndex.current = 0;
|
||||||
|
const currentDateTime = new Date();
|
||||||
|
setGlobalState(
|
||||||
|
(prevState) =>
|
||||||
|
({
|
||||||
|
...prevState,
|
||||||
|
selectedDate: currentDateTime,
|
||||||
|
selectedTime: currentDateTime,
|
||||||
|
} as IGlobalState)
|
||||||
|
);
|
||||||
|
setIsRefreshing(true);
|
||||||
|
}, [appState]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!isEmpty(airlines)) {
|
if (!isEmpty(airlines)) {
|
||||||
setAirlineList(airlines);
|
setAirlineList(airlines);
|
||||||
|
@ -139,17 +161,6 @@ export const FlightTrackerList: React.FunctionComponent<IFlightTrackerListProps>
|
||||||
return (isLoadingFlightSchedules || loadingAirlines || isLoadingItems) && !showMessage;
|
return (isLoadingFlightSchedules || loadingAirlines || isLoadingItems) && !showMessage;
|
||||||
}, [isLoadingFlightSchedules, loadingAirlines, isLoadingItems, errorFlightSchedules]);
|
}, [isLoadingFlightSchedules, loadingAirlines, isLoadingItems, errorFlightSchedules]);
|
||||||
|
|
||||||
const onRefresh = React.useCallback(async () => {
|
|
||||||
pageIndex.current = 0;
|
|
||||||
const currentDateTime = new Date();
|
|
||||||
setGlobalState((prevState) => ({
|
|
||||||
...prevState,
|
|
||||||
selectedDate: currentDateTime,
|
|
||||||
selectedTime: currentDateTime,
|
|
||||||
} as IGlobalState));
|
|
||||||
setIsRefreshing(true);
|
|
||||||
}, [appState]);
|
|
||||||
|
|
||||||
if (!selectedAirPort || !selectedInformationType) {
|
if (!selectedAirPort || !selectedInformationType) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,125 +0,0 @@
|
||||||
/* eslint-disable react/self-closing-comp */
|
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { IColumn } from 'office-ui-fabric-react/lib/DetailsList';
|
|
||||||
|
|
||||||
import { IFlightStatus } from '../../models/IFlightStatus';
|
|
||||||
import { IFlightTrackerListItem } from '../../models/IFlightTrackerListItem';
|
|
||||||
import { FlightStatus } from '../FlightStatus/FlightStatus';
|
|
||||||
import { useFlightTrackerStyles } from './useFlightTrackerStyles';
|
|
||||||
|
|
||||||
export interface IFlightTrackerListColumns {
|
|
||||||
getListColumns: () => IColumn[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useFlightTrackerListColumns = ():IFlightTrackerListColumns => {
|
|
||||||
const { controlStyles } = useFlightTrackerStyles();
|
|
||||||
|
|
||||||
const getListColumns = React.useCallback((): IColumn[] => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
key: "column1",
|
|
||||||
name: " flightCompanyImage",
|
|
||||||
className: controlStyles.fileIconCell,
|
|
||||||
iconClassName: controlStyles.fileIconHeaderIcon,
|
|
||||||
ariaLabel: "company image",
|
|
||||||
isIconOnly: true,
|
|
||||||
fieldName: "image",
|
|
||||||
minWidth: 70,
|
|
||||||
maxWidth: 70,
|
|
||||||
onRender: (item: IFlightTrackerListItem) => {
|
|
||||||
return (
|
|
||||||
<img src={item.flightCompanyImage} style={{width: 28, height: 28}} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "column2",
|
|
||||||
name: "Air line",
|
|
||||||
fieldName: "flightCompany",
|
|
||||||
minWidth: 210,
|
|
||||||
maxWidth: 210,
|
|
||||||
isRowHeader: true,
|
|
||||||
isResizable: true,
|
|
||||||
data: "string",
|
|
||||||
isPadded: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "column6",
|
|
||||||
|
|
||||||
name: "Flight",
|
|
||||||
fieldName: "flightNumber",
|
|
||||||
minWidth: 70,
|
|
||||||
maxWidth: 90,
|
|
||||||
isResizable: true,
|
|
||||||
isCollapsible: true,
|
|
||||||
data: "number",
|
|
||||||
onRender: (item: IFlightTrackerListItem) => {
|
|
||||||
return <span>{item.flightNumber}</span>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "column3",
|
|
||||||
name: "time",
|
|
||||||
fieldName: "flightTime",
|
|
||||||
minWidth: 70,
|
|
||||||
maxWidth: 90,
|
|
||||||
isResizable: true,
|
|
||||||
|
|
||||||
data: "number",
|
|
||||||
onRender: (item: IFlightTrackerListItem) => {
|
|
||||||
return <span>{item.flightTime}</span>;
|
|
||||||
},
|
|
||||||
isPadded: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "column4",
|
|
||||||
name: "Terminal",
|
|
||||||
fieldName: "flightTerminal",
|
|
||||||
minWidth: 70,
|
|
||||||
maxWidth: 90,
|
|
||||||
isResizable: true,
|
|
||||||
isCollapsible: true,
|
|
||||||
data: "string",
|
|
||||||
|
|
||||||
onRender: (item: IFlightTrackerListItem) => {
|
|
||||||
return <span>{item.flightTerminal}</span>;
|
|
||||||
},
|
|
||||||
isPadded: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "column5",
|
|
||||||
name: "Origem",
|
|
||||||
fieldName: "flightOrigin",
|
|
||||||
minWidth: 150,
|
|
||||||
maxWidth: 150,
|
|
||||||
isResizable: true,
|
|
||||||
isCollapsible: true,
|
|
||||||
data: "number",
|
|
||||||
onRender: (item: IFlightTrackerListItem) => {
|
|
||||||
return <span>{item.flightOrigin}</span>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
key: "column6",
|
|
||||||
name: "Status",
|
|
||||||
fieldName: "flightTimeStatus",
|
|
||||||
minWidth: 70,
|
|
||||||
maxWidth: 90,
|
|
||||||
isResizable: true,
|
|
||||||
isCollapsible: true,
|
|
||||||
data: "number",
|
|
||||||
onRender: (item: IFlightTrackerListItem) => {
|
|
||||||
const flightInfo:IFlightStatus = {
|
|
||||||
date: item.flightRealTime, status: item.flightTimeStatusText,
|
|
||||||
flightId: item.flightNumber
|
|
||||||
};
|
|
||||||
return <FlightStatus flightInfo={flightInfo}/>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {getListColumns }
|
|
||||||
};
|
|
|
@ -15,7 +15,7 @@ import { globalState } from '../../recoil/atoms';
|
||||||
|
|
||||||
export const useFlightTrackerStyles = () => {
|
export const useFlightTrackerStyles = () => {
|
||||||
const [globalStateApp] = useRecoilState(globalState);
|
const [globalStateApp] = useRecoilState(globalState);
|
||||||
const { currentTheme } = globalStateApp;
|
const { currentTheme, numberItemsPerPage } = globalStateApp;
|
||||||
|
|
||||||
const listHeaderStyles: ITextStyles = React.useMemo(() => {
|
const listHeaderStyles: ITextStyles = React.useMemo(() => {
|
||||||
return { root: { fontWeight: FontWeights.semibold, color: currentTheme?.semanticColors?.bodyText } };
|
return { root: { fontWeight: FontWeights.semibold, color: currentTheme?.semanticColors?.bodyText } };
|
||||||
|
@ -56,11 +56,11 @@ export const useFlightTrackerStyles = () => {
|
||||||
|
|
||||||
const scollableContainerStyles: Partial<IScrollablePaneStyles> = React.useMemo(() => {
|
const scollableContainerStyles: Partial<IScrollablePaneStyles> = React.useMemo(() => {
|
||||||
return {
|
return {
|
||||||
root: { position: "relative", height: 87 * 6 ,
|
root: { position: "relative", height: (87 * numberItemsPerPage) - 50 ,
|
||||||
},
|
},
|
||||||
contentContainer: { "::-webkit-scrollbar-thumb": {
|
contentContainer: { "::-webkit-scrollbar-thumb": {
|
||||||
|
|
||||||
backgroundColor: currentTheme?.semanticColors.bodyFrameBackground , },
|
backgroundColor: currentTheme?.palette.themeLight, },
|
||||||
"::-webkit-scrollbar": {
|
"::-webkit-scrollbar": {
|
||||||
height: 10,
|
height: 10,
|
||||||
width: 7,
|
width: 7,
|
||||||
|
@ -69,7 +69,7 @@ export const useFlightTrackerStyles = () => {
|
||||||
"scrollbar-color": currentTheme?.semanticColors.bodyFrameBackground,
|
"scrollbar-color": currentTheme?.semanticColors.bodyFrameBackground,
|
||||||
"scrollbar-width": "thin", },
|
"scrollbar-width": "thin", },
|
||||||
};
|
};
|
||||||
}, [currentTheme]);
|
}, [currentTheme, numberItemsPerPage]);
|
||||||
|
|
||||||
const stackContainerStyles: IStackStyles= React.useMemo(() => {
|
const stackContainerStyles: IStackStyles= React.useMemo(() => {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { useAirports } from '../../hooks/useAirports';
|
import { useAirports } from '../../hooks/useAirports';
|
||||||
|
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
||||||
import { IAirport } from '../../models/IAirport';
|
import { IAirport } from '../../models/IAirport';
|
||||||
import { globalState } from '../../recoil/atoms';
|
import { globalState } from '../../recoil/atoms';
|
||||||
import { Airport } from './Airport';
|
import { Airport } from './Airport';
|
||||||
|
@ -32,7 +33,9 @@ export const SelectAirportPicker: React.FunctionComponent = () => {
|
||||||
const { searchAirportsByText } = useAirports();
|
const { searchAirportsByText } = useAirports();
|
||||||
const [selectedAirport, setSelectedAirports] = React.useState<ITag[]>([]);
|
const [selectedAirport, setSelectedAirports] = React.useState<ITag[]>([]);
|
||||||
const { controlStyles, selecteAirportPickerStyles } = useSelectAirportStyles();
|
const { controlStyles, selecteAirportPickerStyles } = useSelectAirportStyles();
|
||||||
|
const { context } = appState;
|
||||||
|
const SELECTED_AIRPORT_SESSION_STORAGE_KEY = "___selectedAirport___";
|
||||||
|
const [getSelectedAirportFromSessionStorage, SetSelectedAirporttoSessionStorage] = useLocalStorage();
|
||||||
const inputProps: IInputProps = React.useMemo(() => {
|
const inputProps: IInputProps = React.useMemo(() => {
|
||||||
return {
|
return {
|
||||||
placeholder: "Select an airport",
|
placeholder: "Select an airport",
|
||||||
|
@ -93,14 +96,11 @@ export const SelectAirportPicker: React.FunctionComponent = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const onRenderSuggestionsItem = React.useCallback(
|
const onRenderSuggestionsItem = React.useCallback(
|
||||||
(props:ITagExtended, itemProps: ISuggestionItemProps<ITagExtended>) => {
|
(props: ITagExtended, itemProps: ISuggestionItemProps<ITagExtended>) => {
|
||||||
const { airportData } = props;
|
const { airportData } = props;
|
||||||
return (
|
return (
|
||||||
<div className={controlStyles.pickerItemStyles}>
|
<div className={controlStyles.pickerItemStyles}>
|
||||||
<Airport
|
<Airport airport={airportData} />
|
||||||
airport={airportData}
|
|
||||||
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -124,24 +124,44 @@ export const SelectAirportPicker: React.FunctionComponent = () => {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[appState]
|
[appState,setSelectedAirports,setAppState]
|
||||||
);
|
);
|
||||||
|
|
||||||
const pickerCalloutPropsStyles = (props: ICalloutContentStyleProps) => {
|
const pickerCalloutPropsStyles = (props: ICalloutContentStyleProps) => {
|
||||||
return { root: { width: divRef?.current?.offsetWidth } };
|
return { root: { width: divRef?.current?.offsetWidth } };
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPickerChange = React.useCallback((items: ITag[]) => {
|
const onPickerChange = React.useCallback(
|
||||||
|
(items: ITag[]) => {
|
||||||
|
SetSelectedAirporttoSessionStorage(
|
||||||
|
`${SELECTED_AIRPORT_SESSION_STORAGE_KEY}${context.instanceId}`,
|
||||||
|
items[0] as ITagExtended
|
||||||
|
);
|
||||||
setAppState({ ...appState, selectedAirPort: (items[0] as ITagExtended)?.airportData });
|
setAppState({ ...appState, selectedAirPort: (items[0] as ITagExtended)?.airportData });
|
||||||
}, [appState]);
|
},
|
||||||
|
[appState]
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (context) {
|
||||||
|
const selectedAirportInSessionStorage = getSelectedAirportFromSessionStorage(
|
||||||
|
`${SELECTED_AIRPORT_SESSION_STORAGE_KEY}${context.instanceId}`
|
||||||
|
);
|
||||||
|
if (selectedAirportInSessionStorage) {
|
||||||
|
setSelectedAirports([selectedAirportInSessionStorage]);
|
||||||
|
setAppState({
|
||||||
|
...appState,
|
||||||
|
selectedAirPort: (selectedAirportInSessionStorage as ITagExtended)?.airportData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [context]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
<div ref={divRef} className={controlStyles.searchContainerStyles}>
|
<div ref={divRef} className={controlStyles.searchContainerStyles}>
|
||||||
<Label>Airport</Label>
|
<Label>Airport</Label>
|
||||||
<TagPicker
|
<TagPicker
|
||||||
|
|
||||||
selectedItems={selectedAirport}
|
selectedItems={selectedAirport}
|
||||||
styles={selecteAirportPickerStyles}
|
styles={selecteAirportPickerStyles}
|
||||||
resolveDelay={500}
|
resolveDelay={500}
|
||||||
|
@ -160,7 +180,6 @@ export const SelectAirportPicker: React.FunctionComponent = () => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,12 +19,16 @@ export interface ISelectDateProps {}
|
||||||
export const SelectDate: React.FunctionComponent<ISelectDateProps> = (
|
export const SelectDate: React.FunctionComponent<ISelectDateProps> = (
|
||||||
props: React.PropsWithChildren<ISelectDateProps>
|
props: React.PropsWithChildren<ISelectDateProps>
|
||||||
) => {
|
) => {
|
||||||
|
|
||||||
const { selectedDateStyle, textFieldStyles, labelDateStyles, labelTimeStyles } = useSelctedDateStyles();
|
const { selectedDateStyle, textFieldStyles, labelDateStyles, labelTimeStyles } = useSelctedDateStyles();
|
||||||
const [appState, setAppState] = useRecoilState(globalState);
|
const [appState, setAppState] = useRecoilState(globalState);
|
||||||
const { selectedDate, selectedTime } = appState;
|
const { selectedDate, selectedTime } = appState;
|
||||||
|
|
||||||
|
|
||||||
const onSelectDate = React.useCallback(
|
const onSelectDate = React.useCallback(
|
||||||
(date: Date | null | undefined) => {
|
(date: Date | null | undefined) => {
|
||||||
|
|
||||||
|
|
||||||
setAppState({
|
setAppState({
|
||||||
...appState,
|
...appState,
|
||||||
selectedDate: date,
|
selectedDate: date,
|
||||||
|
@ -33,6 +37,9 @@ export const SelectDate: React.FunctionComponent<ISelectDateProps> = (
|
||||||
[appState, setAppState, selectedTime]
|
[appState, setAppState, selectedTime]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const datePickerRef = React.useRef<IDatePicker>(null);
|
const datePickerRef = React.useRef<IDatePicker>(null);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { EInformationType } from '../../constants/EInformationType';
|
import { EInformationType } from '../../constants/EInformationType';
|
||||||
import { EInformationTypesIcons } from '../../constants/EInformationTypesIcons';
|
import { EInformationTypesIcons } from '../../constants/EInformationTypesIcons';
|
||||||
|
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
||||||
import { globalState } from '../../recoil/atoms';
|
import { globalState } from '../../recoil/atoms';
|
||||||
import { useSelectInformationStyles } from './useSelectInformationStyles';
|
import { useSelectInformationStyles } from './useSelectInformationStyles';
|
||||||
|
|
||||||
|
@ -22,7 +23,10 @@ export interface ISelectInformationTypeProps {}
|
||||||
export const SelectInformationType: React.FunctionComponent<ISelectInformationTypeProps> = (
|
export const SelectInformationType: React.FunctionComponent<ISelectInformationTypeProps> = (
|
||||||
props: React.PropsWithChildren<ISelectInformationTypeProps>
|
props: React.PropsWithChildren<ISelectInformationTypeProps>
|
||||||
) => {
|
) => {
|
||||||
|
const SELECTED_INFORMATION_TYPE_SESSION_STORAGE_KEY = "___selectedInformationType___";
|
||||||
|
const [getSelectedInfTypeFromSessionStorage, setSelectedInfTypeToSessionStorage] = useLocalStorage();
|
||||||
const [appState, setAppState] = useRecoilState(globalState);
|
const [appState, setAppState] = useRecoilState(globalState);
|
||||||
|
const { context } = appState;
|
||||||
const { dropdownStyles, controlStyles } = useSelectInformationStyles();
|
const { dropdownStyles, controlStyles } = useSelectInformationStyles();
|
||||||
const options: IDropdownOption[] = React.useMemo(() => {
|
const options: IDropdownOption[] = React.useMemo(() => {
|
||||||
return [
|
return [
|
||||||
|
@ -79,6 +83,20 @@ export const SelectInformationType: React.FunctionComponent<ISelectInformationTy
|
||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (context) {
|
||||||
|
const selectedInformationTypeInSessionStorage = getSelectedInfTypeFromSessionStorage(
|
||||||
|
`${SELECTED_INFORMATION_TYPE_SESSION_STORAGE_KEY}${context.instanceId}`
|
||||||
|
);
|
||||||
|
if (selectedInformationTypeInSessionStorage) {
|
||||||
|
|
||||||
|
setAppState( (prevState) => {
|
||||||
|
return {...prevState, selectedInformationType: selectedInformationTypeInSessionStorage};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [context ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
@ -89,9 +107,15 @@ export const SelectInformationType: React.FunctionComponent<ISelectInformationTy
|
||||||
onRenderOption={onRenderOption}
|
onRenderOption={onRenderOption}
|
||||||
styles={dropdownStyles}
|
styles={dropdownStyles}
|
||||||
options={options}
|
options={options}
|
||||||
onChange={(event, option) =>
|
onChange={(event, option) => {
|
||||||
setAppState({ ...appState, selectedInformationType: option.key as EInformationType })
|
if (option) {
|
||||||
|
setAppState( (prevState) => { return {...prevState, selectedInformationType: option.key as EInformationType};});
|
||||||
|
setSelectedInfTypeToSessionStorage(
|
||||||
|
`${SELECTED_INFORMATION_TYPE_SESSION_STORAGE_KEY}${context.instanceId}`,
|
||||||
|
option.key
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}}
|
||||||
selectedKey={appState.selectedInformationType}
|
selectedKey={appState.selectedInformationType}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -15,15 +15,13 @@ const airlinesData = require("../mockData/airlines.json");
|
||||||
export const useAirlines = () => {
|
export const useAirlines = () => {
|
||||||
const [error, setError] = useState<Error>(null);
|
const [error, setError] = useState<Error>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [airlines, setAirlines] = useState<IAirlines >({} as IAirlines);
|
const [airlines, setAirlines] = useState<IAirlines>({} as IAirlines);
|
||||||
|
|
||||||
const [airlinesLocalStorage, setAirLinesLocalStorage] = useLocalStorage("__airlines__", []);
|
const [getAirLinesFromSessionStorage, setAirLinesToSessionStorage] = useLocalStorage();
|
||||||
|
|
||||||
const fetchAirlines = useCallback(async () => {
|
const fetchAirlines = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
setAirLinesToSessionStorage("__airlines__",airlinesData);
|
||||||
setAirLinesLocalStorage(airlinesData);
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
console.log("[useAirLines] error", error);
|
console.log("[useAirLines] error", error);
|
||||||
|
@ -36,12 +34,13 @@ export const useAirlines = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
if (!airlinesLocalStorage?.rows?.length ) {
|
const airlinesFromSessionStorage = getAirLinesFromSessionStorage("__airlines__");
|
||||||
|
if (!airlinesFromSessionStorage?.rows?.length) {
|
||||||
await fetchAirlines();
|
await fetchAirlines();
|
||||||
setAirlines(airlinesData);
|
setAirlines(airlinesData);
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
} else {
|
} else {
|
||||||
setAirlines(airlinesLocalStorage);
|
setAirlines(airlinesFromSessionStorage);
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
|
@ -1,36 +1,30 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
|
import addSeconds from 'date-fns/addSeconds';
|
||||||
|
import isAfter from 'date-fns/isAfter';
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
interface IStorage {
|
interface IStorage {
|
||||||
value: unknown;
|
value: unknown;
|
||||||
expires?: Date;
|
expires?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_EXPIRED_IN_SECONDS = 60 * 60 * 1000; // 1 hour
|
const DEFAULT_EXPIRED_IN_SECONDS = 60 * 60; // 1 hour
|
||||||
|
|
||||||
const getStorageValue = (key:string, defaultValue:unknown):any => {
|
export const useLocalStorage = (): any => {
|
||||||
const storage:IStorage = JSON.parse(sessionStorage.getItem(key) || '{}');
|
const setStorageValue = (key: string, newValue: unknown, expiredInSeconds: number) => {
|
||||||
|
const expires = addSeconds(new Date(), expiredInSeconds ?? DEFAULT_EXPIRED_IN_SECONDS);
|
||||||
|
sessionStorage.setItem(key, JSON.stringify({ value: newValue, expires }));
|
||||||
|
};
|
||||||
|
const getStorageValue = (key: string): any => {
|
||||||
|
const storage: IStorage = JSON.parse(sessionStorage.getItem(key) || "{}");
|
||||||
// getting stored value
|
// getting stored value
|
||||||
const { value, expires } = storage || {} as IStorage;
|
const { value, expires } = storage || ({} as IStorage);
|
||||||
if (expires > new Date() ) {
|
if (isAfter(new Date(expires), new Date())) {
|
||||||
return value || defaultValue;
|
return value;
|
||||||
}
|
}
|
||||||
return undefined ;
|
return undefined;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useLocalStorage = (key:string, defaultValue:unknown, expiredIn?: number):any => {
|
return [getStorageValue, setStorageValue];
|
||||||
const [value, setStorageValue] = React.useState(() => {
|
|
||||||
return getStorageValue(key, defaultValue);
|
|
||||||
});
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
// save value
|
|
||||||
const expiredInValue = expiredIn ? new Date(new Date().getTime() + expiredIn * 1000) : DEFAULT_EXPIRED_IN_SECONDS;
|
|
||||||
|
|
||||||
sessionStorage.setItem(key, JSON.stringify({ value, expires:expiredInValue }));
|
|
||||||
|
|
||||||
}, [key, value, expiredIn]);
|
|
||||||
|
|
||||||
return [value, setStorageValue];
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -18,4 +18,5 @@ export interface IGlobalState {
|
||||||
isScrolling: boolean;
|
isScrolling: boolean;
|
||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
webpartContainerWidth: number;
|
webpartContainerWidth: number;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,5 +19,6 @@ export const globalState = atom<IGlobalState>({
|
||||||
isScrolling: false,
|
isScrolling: false,
|
||||||
hasMore: true,
|
hasMore: true,
|
||||||
webpartContainerWidth: 0
|
webpartContainerWidth: 0
|
||||||
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,7 +14,6 @@ import {
|
||||||
IPropertyPaneConfiguration,
|
IPropertyPaneConfiguration,
|
||||||
PropertyPaneSlider,
|
PropertyPaneSlider,
|
||||||
PropertyPaneTextField,
|
PropertyPaneTextField,
|
||||||
PropertyPaneToggle,
|
|
||||||
} from '@microsoft/sp-property-pane';
|
} from '@microsoft/sp-property-pane';
|
||||||
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
|
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
|
||||||
|
|
||||||
|
@ -34,18 +33,14 @@ export interface IFlightTrackerWebPartProps {
|
||||||
displayMode: DisplayMode;
|
displayMode: DisplayMode;
|
||||||
updateProperty: (value: string) => void;
|
updateProperty: (value: string) => void;
|
||||||
numberItemsPerPage: number;
|
numberItemsPerPage: number;
|
||||||
refreshInterval: number;
|
|
||||||
enableRefreshInterval: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class FlightTrackerWebPart extends BaseClientSideWebPart<IFlightTrackerWebPartProps> {
|
export default class FlightTrackerWebPart extends BaseClientSideWebPart<IFlightTrackerWebPartProps> {
|
||||||
|
|
||||||
private _isDarkTheme: boolean = false;
|
private _isDarkTheme: boolean = false;
|
||||||
private containerWidth: number = 0;
|
private containerWidth: number = 0;
|
||||||
private _currentTheme: IReadonlyTheme | undefined;
|
private _currentTheme: IReadonlyTheme | undefined;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Apply Teams Context
|
// Apply Teams Context
|
||||||
private _applyTheme = (theme: string): void => {
|
private _applyTheme = (theme: string): void => {
|
||||||
this.context.domElement.setAttribute("data-theme", theme);
|
this.context.domElement.setAttribute("data-theme", theme);
|
||||||
|
@ -53,23 +48,26 @@ export default class FlightTrackerWebPart extends BaseClientSideWebPart<IFlightT
|
||||||
|
|
||||||
if (theme === "dark") {
|
if (theme === "dark") {
|
||||||
loadTheme({
|
loadTheme({
|
||||||
palette: teamsDarkTheme
|
palette: teamsDarkTheme,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (theme === "default") {
|
if (theme === "default") {
|
||||||
loadTheme({
|
loadTheme({
|
||||||
palette: teamsDefaultTheme
|
palette: teamsDefaultTheme,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (theme === "contrast") {
|
if (theme === "contrast") {
|
||||||
loadTheme({
|
loadTheme({
|
||||||
palette: teamsContrastTheme
|
palette: teamsContrastTheme,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
protected get disableReactivePropertyChanges(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
protected onAfterResize(newWidth: number): void {
|
protected onAfterResize(newWidth: number): void {
|
||||||
console.log("onAfterResize", newWidth);
|
console.log("onAfterResize", newWidth);
|
||||||
|
@ -78,42 +76,35 @@ export default class FlightTrackerWebPart extends BaseClientSideWebPart<IFlightT
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): void {
|
public render(): void {
|
||||||
const element: React.ReactElement<IFlightTrackerProps> = React.createElement(
|
const element: React.ReactElement<IFlightTrackerProps> = React.createElement(FlightTracker, {
|
||||||
FlightTracker,
|
|
||||||
{
|
|
||||||
title: this.properties.title,
|
title: this.properties.title,
|
||||||
isDarkTheme: this._isDarkTheme,
|
isDarkTheme: this._isDarkTheme,
|
||||||
context: this.context,
|
context: this.context,
|
||||||
hasTeamsContext: !!this.context.sdks.microsoftTeams,
|
hasTeamsContext: !!this.context.sdks.microsoftTeams,
|
||||||
currentTheme: this._currentTheme ,
|
currentTheme: this._currentTheme,
|
||||||
displayMode: this.displayMode,
|
displayMode: this.displayMode,
|
||||||
numberItemsPerPage: this.properties.numberItemsPerPage,
|
numberItemsPerPage: this.properties.numberItemsPerPage,
|
||||||
updateProperty: (value: string) => {
|
updateProperty: (value: string) => {
|
||||||
this.properties.title = value;
|
this.properties.title = value;
|
||||||
},
|
},
|
||||||
webpartContainerWidth: this.containerWidth
|
webpartContainerWidth: this.containerWidth
|
||||||
}
|
|
||||||
);
|
});
|
||||||
|
|
||||||
ReactDom.render(element, this.domElement);
|
ReactDom.render(element, this.domElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onInit(): Promise<void> {
|
protected onInit(): Promise<void> {
|
||||||
|
if (this.context.sdks.microsoftTeams) {
|
||||||
|
|
||||||
if (this.context.sdks.microsoftTeams ) {
|
|
||||||
// in teams ?
|
// in teams ?
|
||||||
const teamsContext = this.context.sdks.microsoftTeams?.context;
|
const teamsContext = this.context.sdks.microsoftTeams?.context;
|
||||||
this._applyTheme(teamsContext.theme || "default");
|
this._applyTheme(teamsContext.theme || "default");
|
||||||
this.context.sdks.microsoftTeams.teamsJs.registerOnThemeChangeHandler(
|
this.context.sdks.microsoftTeams.teamsJs.registerOnThemeChangeHandler(this._applyTheme);
|
||||||
this._applyTheme
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
this.containerWidth = this.domElement.clientWidth;
|
this.containerWidth = this.domElement.clientWidth;
|
||||||
return super.onInit();
|
return super.onInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void {
|
protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void {
|
||||||
if (!currentTheme) {
|
if (!currentTheme) {
|
||||||
return;
|
return;
|
||||||
|
@ -127,7 +118,7 @@ export default class FlightTrackerWebPart extends BaseClientSideWebPart<IFlightT
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get dataVersion(): Version {
|
protected get dataVersion(): Version {
|
||||||
return Version.parse('1.0');
|
return Version.parse("1.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||||
|
@ -135,40 +126,28 @@ export default class FlightTrackerWebPart extends BaseClientSideWebPart<IFlightT
|
||||||
pages: [
|
pages: [
|
||||||
{
|
{
|
||||||
header: {
|
header: {
|
||||||
description: strings.PropertyPaneDescription
|
description: strings.PropertyPaneDescription,
|
||||||
},
|
},
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
groupName: strings.BasicGroupName,
|
groupName: strings.BasicGroupName,
|
||||||
groupFields: [
|
groupFields: [
|
||||||
PropertyPaneTextField('title', {
|
PropertyPaneTextField("title", {
|
||||||
label: strings.DescriptionFieldLabel
|
label: strings.DescriptionFieldLabel,
|
||||||
}),
|
}),
|
||||||
PropertyPaneSlider('numberItemsPerPage', {
|
PropertyPaneSlider("numberItemsPerPage", {
|
||||||
label: strings.NumberItemsPerPageLabel,
|
label: strings.NumberItemsPerPageLabel,
|
||||||
value: this.properties.numberItemsPerPage,
|
value: this.properties.numberItemsPerPage,
|
||||||
min: 1,
|
min: 5,
|
||||||
max: 20,
|
max: 20,
|
||||||
showValue: true,
|
showValue: true,
|
||||||
}),
|
|
||||||
PropertyPaneToggle('enableRefreshInterval', {
|
|
||||||
label: strings.RefreshIntervalLabel,
|
|
||||||
checked: this.properties.enableRefreshInterval,
|
|
||||||
offText: strings.RefreshIntervalOffText,
|
|
||||||
onText: strings.RefreshIntervalOnText,
|
|
||||||
}),
|
|
||||||
PropertyPaneSlider('refreshInterval', {
|
|
||||||
label: strings.RefreshIntervalLabel,
|
|
||||||
value: this.properties.refreshInterval,
|
|
||||||
min: 1,
|
|
||||||
max: 5,
|
|
||||||
showValue: true,
|
|
||||||
})
|
})
|
||||||
]
|
|
||||||
}
|
],
|
||||||
]
|
},
|
||||||
}
|
],
|
||||||
]
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue