new sample Flight-tracker

This commit is contained in:
João Mendes 2022-11-13 21:35:35 +00:00
parent 6fe61af2ca
commit 10342b46b2
16 changed files with 190 additions and 267 deletions

View File

@ -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:
- `npm install`
- `gulp serve`
> 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. - Clone this repository
- Ensure that you are at the solution folder
- in the command line run:
- `npm install`
- `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.
- 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" />

View File

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

Before

Width:  |  Height:  |  Size: 453 KiB

After

Width:  |  Height:  |  Size: 453 KiB

View File

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

View File

@ -11,5 +11,6 @@ export interface IFlightTrackerProps {
numberItemsPerPage: number; numberItemsPerPage: number;
displayMode: DisplayMode; displayMode: DisplayMode;
updateProperty: (value: string) => void; updateProperty: (value: string) => void;
webpartContainerWidth: number; webpartContainerWidth: number;
} }

View File

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

View File

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

View File

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

View File

@ -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>
); );
}, },
@ -118,30 +118,50 @@ export const SelectAirportPicker: React.FunctionComponent = () => {
airport={airport} airport={airport}
onRemove={(airport) => { onRemove={(airport) => {
setSelectedAirports([]); setSelectedAirports([]);
setAppState({ ...appState, selectedAirPort: undefined }); setAppState({ ...appState, selectedAirPort: undefined });
}} }}
/> />
</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(
setAppState({ ...appState, selectedAirPort: (items[0] as ITagExtended)?.airportData }); (items: ITag[]) => {
}, [appState]); SetSelectedAirporttoSessionStorage(
`${SELECTED_AIRPORT_SESSION_STORAGE_KEY}${context.instanceId}`,
items[0] as ITagExtended
);
setAppState({ ...appState, selectedAirPort: (items[0] as ITagExtended)?.airportData });
},
[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>
); );
}; };

View File

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

View File

@ -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,8 +23,11 @@ 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 { dropdownStyles, controlStyles } = useSelectInformationStyles(); const { context } = appState;
const { dropdownStyles, controlStyles } = useSelectInformationStyles();
const options: IDropdownOption[] = React.useMemo(() => { const options: IDropdownOption[] = React.useMemo(() => {
return [ return [
{ key: "Header", text: "Options", itemType: DropdownMenuItemType.Header }, { key: "Header", text: "Options", itemType: DropdownMenuItemType.Header },
@ -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}
/> />
</> </>

View File

@ -9,21 +9,19 @@ import {
import { IAirlines } from '../models/IAirlines'; import { IAirlines } from '../models/IAirlines';
import { useLocalStorage } from './useLocalStorage'; import { useLocalStorage } from './useLocalStorage';
const airlinesData = require("../mockData/airlines.json"); const airlinesData = require("../mockData/airlines.json");
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable @typescript-eslint/no-floating-promises */ /* eslint-disable @typescript-eslint/no-floating-promises */
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);

View File

@ -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) => {
// getting stored value const expires = addSeconds(new Date(), expiredInSeconds ?? DEFAULT_EXPIRED_IN_SECONDS);
const { value, expires } = storage || {} as IStorage; sessionStorage.setItem(key, JSON.stringify({ value: newValue, expires }));
if (expires > new Date() ) { };
return value || defaultValue; const getStorageValue = (key: string): any => {
} const storage: IStorage = JSON.parse(sessionStorage.getItem(key) || "{}");
return undefined ; // getting stored value
} const { value, expires } = storage || ({} as IStorage);
if (isAfter(new Date(expires), new Date())) {
return value;
}
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];
}; };

View File

@ -18,4 +18,5 @@ export interface IGlobalState {
isScrolling: boolean; isScrolling: boolean;
hasMore: boolean; hasMore: boolean;
webpartContainerWidth: number; webpartContainerWidth: number;
} }

View File

@ -19,5 +19,6 @@ export const globalState = atom<IGlobalState>({
isScrolling: false, isScrolling: false,
hasMore: true, hasMore: true,
webpartContainerWidth: 0 webpartContainerWidth: 0
}, },
}); });

View File

@ -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,67 +48,63 @@ 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);
this.containerWidth = newWidth; this.containerWidth = newWidth;
this.render(); this.render();
} }
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,
{ isDarkTheme: this._isDarkTheme,
title: this.properties.title, context: this.context,
isDarkTheme: this._isDarkTheme, hasTeamsContext: !!this.context.sdks.microsoftTeams,
context: this.context, currentTheme: this._currentTheme,
hasTeamsContext: !!this.context.sdks.microsoftTeams, displayMode: this.displayMode,
currentTheme: this._currentTheme , numberItemsPerPage: this.properties.numberItemsPerPage,
displayMode: this.displayMode, updateProperty: (value: string) => {
numberItemsPerPage: this.properties.numberItemsPerPage, this.properties.title = value;
updateProperty: (value: string) => { },
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,
}) })
]
} ],
] },
} ],
] },
],
}; };
} }
} }