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.
|
||||
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")
|
||||
![Teams View](assets/FQzO9YjWUAgFlrU.jpg "Teams View")
|
||||
![SharePoint View](./src/assets/sharepoint.png "SharePoint View")
|
||||
![Teams View](./src/assets/teams.jpg "Teams View")
|
||||
|
||||
## Compatibility
|
||||
React-
|
||||
|
||||
![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)
|
||||
|
@ -39,25 +39,36 @@ The SPFx use external API (https://aerodatabox.p.rapidapi.com/flights/number/) t
|
|||
|
||||
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|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)
|
||||
- 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`
|
||||
## Minimal Path to Awesome
|
||||
|
||||
> 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
|
||||
|
||||
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
|
||||
|
||||
|
@ -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.**
|
||||
|
||||
<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,
|
||||
numberItemsPerPage: numberItemsPerPage,
|
||||
webpartContainerWidth: webpartContainerWidth,
|
||||
|
||||
});
|
||||
}, [isDarkTheme, hasTeamsContext, currentTheme, context, setGlobalState, webpartContainerWidth]);
|
||||
|
||||
|
|
|
@ -11,5 +11,6 @@ export interface IFlightTrackerProps {
|
|||
numberItemsPerPage: number;
|
||||
displayMode: DisplayMode;
|
||||
updateProperty: (value: string) => void;
|
||||
webpartContainerWidth: number;
|
||||
webpartContainerWidth: number;
|
||||
|
||||
}
|
||||
|
|
|
@ -38,7 +38,14 @@ export const FlightTrackerList: React.FunctionComponent<IFlightTrackerListProps>
|
|||
const [appState, setGlobalState] = useRecoilState(globalState);
|
||||
const [airlineList, setAirlineList] = useRecoilState(airlineState);
|
||||
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 [errorMessage, setErrorMessage] = React.useState<string>("");
|
||||
const [listItems, setListItems] = React.useState<IFlightTrackerListItem[]>([]);
|
||||
|
@ -51,6 +58,7 @@ export const FlightTrackerList: React.FunctionComponent<IFlightTrackerListProps>
|
|||
const [hasMore, setHasMore] = React.useState<boolean>(true);
|
||||
const pageIndex = React.useRef<number>(0);
|
||||
const currentInformationType = React.useRef(selectedInformationType);
|
||||
const [timerId, setTimerId] = React.useState<number>(undefined);
|
||||
|
||||
const checkTypeInformationToScroll = React.useCallback(() => {
|
||||
if (selectedInformationType !== currentInformationType.current) {
|
||||
|
@ -59,6 +67,20 @@ export const FlightTrackerList: React.FunctionComponent<IFlightTrackerListProps>
|
|||
}
|
||||
}, [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(() => {
|
||||
if (!isEmpty(airlines)) {
|
||||
setAirlineList(airlines);
|
||||
|
@ -139,17 +161,6 @@ export const FlightTrackerList: React.FunctionComponent<IFlightTrackerListProps>
|
|||
return (isLoadingFlightSchedules || loadingAirlines || isLoadingItems) && !showMessage;
|
||||
}, [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) {
|
||||
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 = () => {
|
||||
const [globalStateApp] = useRecoilState(globalState);
|
||||
const { currentTheme } = globalStateApp;
|
||||
const { currentTheme, numberItemsPerPage } = globalStateApp;
|
||||
|
||||
const listHeaderStyles: ITextStyles = React.useMemo(() => {
|
||||
return { root: { fontWeight: FontWeights.semibold, color: currentTheme?.semanticColors?.bodyText } };
|
||||
|
@ -56,11 +56,11 @@ export const useFlightTrackerStyles = () => {
|
|||
|
||||
const scollableContainerStyles: Partial<IScrollablePaneStyles> = React.useMemo(() => {
|
||||
return {
|
||||
root: { position: "relative", height: 87 * 6 ,
|
||||
root: { position: "relative", height: (87 * numberItemsPerPage) - 50 ,
|
||||
},
|
||||
contentContainer: { "::-webkit-scrollbar-thumb": {
|
||||
|
||||
backgroundColor: currentTheme?.semanticColors.bodyFrameBackground , },
|
||||
backgroundColor: currentTheme?.palette.themeLight, },
|
||||
"::-webkit-scrollbar": {
|
||||
height: 10,
|
||||
width: 7,
|
||||
|
@ -69,7 +69,7 @@ export const useFlightTrackerStyles = () => {
|
|||
"scrollbar-color": currentTheme?.semanticColors.bodyFrameBackground,
|
||||
"scrollbar-width": "thin", },
|
||||
};
|
||||
}, [currentTheme]);
|
||||
}, [currentTheme, numberItemsPerPage]);
|
||||
|
||||
const stackContainerStyles: IStackStyles= React.useMemo(() => {
|
||||
return {
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { useAirports } from '../../hooks/useAirports';
|
||||
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
||||
import { IAirport } from '../../models/IAirport';
|
||||
import { globalState } from '../../recoil/atoms';
|
||||
import { Airport } from './Airport';
|
||||
|
@ -32,7 +33,9 @@ export const SelectAirportPicker: React.FunctionComponent = () => {
|
|||
const { searchAirportsByText } = useAirports();
|
||||
const [selectedAirport, setSelectedAirports] = React.useState<ITag[]>([]);
|
||||
const { controlStyles, selecteAirportPickerStyles } = useSelectAirportStyles();
|
||||
|
||||
const { context } = appState;
|
||||
const SELECTED_AIRPORT_SESSION_STORAGE_KEY = "___selectedAirport___";
|
||||
const [getSelectedAirportFromSessionStorage, SetSelectedAirporttoSessionStorage] = useLocalStorage();
|
||||
const inputProps: IInputProps = React.useMemo(() => {
|
||||
return {
|
||||
placeholder: "Select an airport",
|
||||
|
@ -93,14 +96,11 @@ export const SelectAirportPicker: React.FunctionComponent = () => {
|
|||
);
|
||||
|
||||
const onRenderSuggestionsItem = React.useCallback(
|
||||
(props:ITagExtended, itemProps: ISuggestionItemProps<ITagExtended>) => {
|
||||
(props: ITagExtended, itemProps: ISuggestionItemProps<ITagExtended>) => {
|
||||
const { airportData } = props;
|
||||
return (
|
||||
<div className={controlStyles.pickerItemStyles}>
|
||||
<Airport
|
||||
airport={airportData}
|
||||
|
||||
/>
|
||||
<Airport airport={airportData} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
@ -118,30 +118,50 @@ export const SelectAirportPicker: React.FunctionComponent = () => {
|
|||
airport={airport}
|
||||
onRemove={(airport) => {
|
||||
setSelectedAirports([]);
|
||||
setAppState({ ...appState, selectedAirPort: undefined });
|
||||
setAppState({ ...appState, selectedAirPort: undefined });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[appState]
|
||||
[appState,setSelectedAirports,setAppState]
|
||||
);
|
||||
|
||||
const pickerCalloutPropsStyles = (props: ICalloutContentStyleProps) => {
|
||||
return { root: { width: divRef?.current?.offsetWidth } };
|
||||
};
|
||||
|
||||
const onPickerChange = React.useCallback((items: ITag[]) => {
|
||||
setAppState({ ...appState, selectedAirPort: (items[0] as ITagExtended)?.airportData });
|
||||
}, [appState]);
|
||||
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 });
|
||||
},
|
||||
[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 (
|
||||
<div>
|
||||
|
||||
<div ref={divRef} className={controlStyles.searchContainerStyles}>
|
||||
<Label>Airport</Label>
|
||||
<TagPicker
|
||||
|
||||
selectedItems={selectedAirport}
|
||||
styles={selecteAirportPickerStyles}
|
||||
resolveDelay={500}
|
||||
|
@ -160,7 +180,6 @@ export const SelectAirportPicker: React.FunctionComponent = () => {
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -19,12 +19,16 @@ export interface ISelectDateProps {}
|
|||
export const SelectDate: React.FunctionComponent<ISelectDateProps> = (
|
||||
props: React.PropsWithChildren<ISelectDateProps>
|
||||
) => {
|
||||
|
||||
const { selectedDateStyle, textFieldStyles, labelDateStyles, labelTimeStyles } = useSelctedDateStyles();
|
||||
const [appState, setAppState] = useRecoilState(globalState);
|
||||
const { selectedDate, selectedTime } = appState;
|
||||
const { selectedDate, selectedTime } = appState;
|
||||
|
||||
|
||||
const onSelectDate = React.useCallback(
|
||||
(date: Date | null | undefined) => {
|
||||
|
||||
|
||||
setAppState({
|
||||
...appState,
|
||||
selectedDate: date,
|
||||
|
@ -33,6 +37,9 @@ export const SelectDate: React.FunctionComponent<ISelectDateProps> = (
|
|||
[appState, setAppState, selectedTime]
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
const datePickerRef = React.useRef<IDatePicker>(null);
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -14,6 +14,7 @@ import { useRecoilState } from 'recoil';
|
|||
|
||||
import { EInformationType } from '../../constants/EInformationType';
|
||||
import { EInformationTypesIcons } from '../../constants/EInformationTypesIcons';
|
||||
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
||||
import { globalState } from '../../recoil/atoms';
|
||||
import { useSelectInformationStyles } from './useSelectInformationStyles';
|
||||
|
||||
|
@ -22,8 +23,11 @@ export interface ISelectInformationTypeProps {}
|
|||
export const SelectInformationType: React.FunctionComponent<ISelectInformationTypeProps> = (
|
||||
props: React.PropsWithChildren<ISelectInformationTypeProps>
|
||||
) => {
|
||||
const SELECTED_INFORMATION_TYPE_SESSION_STORAGE_KEY = "___selectedInformationType___";
|
||||
const [getSelectedInfTypeFromSessionStorage, setSelectedInfTypeToSessionStorage] = useLocalStorage();
|
||||
const [appState, setAppState] = useRecoilState(globalState);
|
||||
const { dropdownStyles, controlStyles } = useSelectInformationStyles();
|
||||
const { context } = appState;
|
||||
const { dropdownStyles, controlStyles } = useSelectInformationStyles();
|
||||
const options: IDropdownOption[] = React.useMemo(() => {
|
||||
return [
|
||||
{ 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 (
|
||||
<>
|
||||
<Dropdown
|
||||
|
@ -89,9 +107,15 @@ export const SelectInformationType: React.FunctionComponent<ISelectInformationTy
|
|||
onRenderOption={onRenderOption}
|
||||
styles={dropdownStyles}
|
||||
options={options}
|
||||
onChange={(event, option) =>
|
||||
setAppState({ ...appState, selectedInformationType: option.key as EInformationType })
|
||||
}
|
||||
onChange={(event, option) => {
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -9,21 +9,19 @@ import {
|
|||
import { IAirlines } from '../models/IAirlines';
|
||||
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/no-floating-promises */
|
||||
export const useAirlines = () => {
|
||||
const [error, setError] = useState<Error>(null);
|
||||
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 () => {
|
||||
try {
|
||||
|
||||
setAirLinesLocalStorage(airlinesData);
|
||||
|
||||
setAirLinesToSessionStorage("__airlines__",airlinesData);
|
||||
} catch (error) {
|
||||
if (DEBUG) {
|
||||
console.log("[useAirLines] error", error);
|
||||
|
@ -36,12 +34,13 @@ export const useAirlines = () => {
|
|||
useEffect(() => {
|
||||
(async () => {
|
||||
setLoading(true);
|
||||
if (!airlinesLocalStorage?.rows?.length ) {
|
||||
const airlinesFromSessionStorage = getAirLinesFromSessionStorage("__airlines__");
|
||||
if (!airlinesFromSessionStorage?.rows?.length) {
|
||||
await fetchAirlines();
|
||||
setAirlines(airlinesData);
|
||||
setError(undefined);
|
||||
} else {
|
||||
setAirlines(airlinesLocalStorage);
|
||||
setAirlines(airlinesFromSessionStorage);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
|
|
|
@ -1,36 +1,30 @@
|
|||
/* 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 {
|
||||
value: unknown;
|
||||
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 => {
|
||||
const storage:IStorage = JSON.parse(sessionStorage.getItem(key) || '{}');
|
||||
// getting stored value
|
||||
const { value, expires } = storage || {} as IStorage;
|
||||
if (expires > new Date() ) {
|
||||
return value || defaultValue;
|
||||
}
|
||||
return undefined ;
|
||||
}
|
||||
export const useLocalStorage = (): any => {
|
||||
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
|
||||
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 => {
|
||||
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];
|
||||
return [getStorageValue, setStorageValue];
|
||||
};
|
||||
|
|
|
@ -18,4 +18,5 @@ export interface IGlobalState {
|
|||
isScrolling: boolean;
|
||||
hasMore: boolean;
|
||||
webpartContainerWidth: number;
|
||||
|
||||
}
|
||||
|
|
|
@ -19,5 +19,6 @@ export const globalState = atom<IGlobalState>({
|
|||
isScrolling: false,
|
||||
hasMore: true,
|
||||
webpartContainerWidth: 0
|
||||
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,7 +14,6 @@ import {
|
|||
IPropertyPaneConfiguration,
|
||||
PropertyPaneSlider,
|
||||
PropertyPaneTextField,
|
||||
PropertyPaneToggle,
|
||||
} from '@microsoft/sp-property-pane';
|
||||
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
|
||||
|
||||
|
@ -34,18 +33,14 @@ export interface IFlightTrackerWebPartProps {
|
|||
displayMode: DisplayMode;
|
||||
updateProperty: (value: string) => void;
|
||||
numberItemsPerPage: number;
|
||||
refreshInterval: number;
|
||||
enableRefreshInterval: boolean;
|
||||
|
||||
}
|
||||
|
||||
export default class FlightTrackerWebPart extends BaseClientSideWebPart<IFlightTrackerWebPartProps> {
|
||||
|
||||
private _isDarkTheme: boolean = false;
|
||||
private containerWidth: number = 0;
|
||||
private _currentTheme: IReadonlyTheme | undefined;
|
||||
|
||||
|
||||
|
||||
// Apply Teams Context
|
||||
private _applyTheme = (theme: string): void => {
|
||||
this.context.domElement.setAttribute("data-theme", theme);
|
||||
|
@ -53,67 +48,63 @@ export default class FlightTrackerWebPart extends BaseClientSideWebPart<IFlightT
|
|||
|
||||
if (theme === "dark") {
|
||||
loadTheme({
|
||||
palette: teamsDarkTheme
|
||||
palette: teamsDarkTheme,
|
||||
});
|
||||
}
|
||||
|
||||
if (theme === "default") {
|
||||
loadTheme({
|
||||
palette: teamsDefaultTheme
|
||||
palette: teamsDefaultTheme,
|
||||
});
|
||||
}
|
||||
|
||||
if (theme === "contrast") {
|
||||
loadTheme({
|
||||
palette: teamsContrastTheme
|
||||
palette: teamsContrastTheme,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected get disableReactivePropertyChanges(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected onAfterResize(newWidth: number): void {
|
||||
console.log("onAfterResize", newWidth);
|
||||
this.containerWidth = newWidth;
|
||||
this.render();
|
||||
this.containerWidth = newWidth;
|
||||
this.render();
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
const element: React.ReactElement<IFlightTrackerProps> = React.createElement(
|
||||
FlightTracker,
|
||||
{
|
||||
title: this.properties.title,
|
||||
isDarkTheme: this._isDarkTheme,
|
||||
context: this.context,
|
||||
hasTeamsContext: !!this.context.sdks.microsoftTeams,
|
||||
currentTheme: this._currentTheme ,
|
||||
displayMode: this.displayMode,
|
||||
numberItemsPerPage: this.properties.numberItemsPerPage,
|
||||
updateProperty: (value: string) => {
|
||||
this.properties.title = value;
|
||||
},
|
||||
webpartContainerWidth: this.containerWidth
|
||||
}
|
||||
);
|
||||
const element: React.ReactElement<IFlightTrackerProps> = React.createElement(FlightTracker, {
|
||||
title: this.properties.title,
|
||||
isDarkTheme: this._isDarkTheme,
|
||||
context: this.context,
|
||||
hasTeamsContext: !!this.context.sdks.microsoftTeams,
|
||||
currentTheme: this._currentTheme,
|
||||
displayMode: this.displayMode,
|
||||
numberItemsPerPage: this.properties.numberItemsPerPage,
|
||||
updateProperty: (value: string) => {
|
||||
this.properties.title = value;
|
||||
},
|
||||
webpartContainerWidth: this.containerWidth
|
||||
|
||||
});
|
||||
|
||||
ReactDom.render(element, this.domElement);
|
||||
}
|
||||
|
||||
protected onInit(): Promise<void> {
|
||||
|
||||
|
||||
if (this.context.sdks.microsoftTeams ) {
|
||||
if (this.context.sdks.microsoftTeams) {
|
||||
// in teams ?
|
||||
const teamsContext = this.context.sdks.microsoftTeams?.context;
|
||||
this._applyTheme(teamsContext.theme || "default");
|
||||
this.context.sdks.microsoftTeams.teamsJs.registerOnThemeChangeHandler(
|
||||
this._applyTheme
|
||||
);
|
||||
this.context.sdks.microsoftTeams.teamsJs.registerOnThemeChangeHandler(this._applyTheme);
|
||||
}
|
||||
this.containerWidth = this.domElement.clientWidth;
|
||||
return super.onInit();
|
||||
}
|
||||
|
||||
|
||||
protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void {
|
||||
if (!currentTheme) {
|
||||
return;
|
||||
|
@ -127,7 +118,7 @@ export default class FlightTrackerWebPart extends BaseClientSideWebPart<IFlightT
|
|||
}
|
||||
|
||||
protected get dataVersion(): Version {
|
||||
return Version.parse('1.0');
|
||||
return Version.parse("1.0");
|
||||
}
|
||||
|
||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||
|
@ -135,40 +126,28 @@ export default class FlightTrackerWebPart extends BaseClientSideWebPart<IFlightT
|
|||
pages: [
|
||||
{
|
||||
header: {
|
||||
description: strings.PropertyPaneDescription
|
||||
description: strings.PropertyPaneDescription,
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
groupName: strings.BasicGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneTextField('title', {
|
||||
label: strings.DescriptionFieldLabel
|
||||
PropertyPaneTextField("title", {
|
||||
label: strings.DescriptionFieldLabel,
|
||||
}),
|
||||
PropertyPaneSlider('numberItemsPerPage', {
|
||||
PropertyPaneSlider("numberItemsPerPage", {
|
||||
label: strings.NumberItemsPerPageLabel,
|
||||
value: this.properties.numberItemsPerPage,
|
||||
min: 1,
|
||||
min: 5,
|
||||
max: 20,
|
||||
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