This commit is contained in:
phntxx 2021-07-14 01:18:28 +02:00
parent ca2f7a763d
commit eaad2d56f0
36 changed files with 366 additions and 960 deletions

View file

@ -3,9 +3,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"repository": "git@github.com:phntxx/dashboard", "repository": "git@github.com:phntxx/dashboard",
"contributors": [ "contributors": ["phntxx <hello@phntxx.com>"],
"phntxx <hello@phntxx.com>"
],
"private": false, "private": false,
"dependencies": { "dependencies": {
"@types/node": "^14.14.37", "@types/node": "^14.14.37",
@ -47,11 +45,7 @@
"extends": "react-app" "extends": "react-app"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [">0.2%", "not dead", "not op_mini all"],
">0.2%",
"not dead",
"not op_mini all"
],
"development": [ "development": [
"last 1 chrome version", "last 1 chrome version",
"last 1 firefox version", "last 1 firefox version",

View file

@ -2,13 +2,13 @@ import { createGlobalStyle, ThemeProvider } from "styled-components";
import SearchBar from "./components/searchBar"; import SearchBar from "./components/searchBar";
import Greeter from "./components/greeter"; import Greeter from "./components/greeter";
import AppList from "./components/appList"; import { AppList } from "./components/apps";
import BookmarkList from "./components/bookmarks"; import BookmarkList from "./components/bookmarks";
import Settings from "./components/settings"; import Settings from "./components/settings";
import Imprint from "./components/imprint"; import Imprint from "./components/imprint";
import { IThemeProps, getTheme, setScheme } from "./lib/useTheme"; import { IThemeProps, getTheme, setScheme } from "./lib/useTheme";
import useFetcher from "./lib/fetcher"; import useFetch from "./lib/useFetch";
import useMediaQuery from "./lib/useMediaQuery"; import useMediaQuery from "./lib/useMediaQuery";
export const GlobalStyle = createGlobalStyle<{ theme: IThemeProps }>` export const GlobalStyle = createGlobalStyle<{ theme: IThemeProps }>`
@ -33,37 +33,26 @@ const App = () => {
const { const {
appData, appData,
bookmarkData, bookmarkData,
searchProviderData, searchData,
themeData, themeData,
imprintData, imprintData,
greeterData, greeterData,
} = useFetcher(); } = useFetch();
const theme = getTheme(); const theme = getTheme();
let isDark = useMediaQuery("(prefers-color-scheme: dark)"); let isDark = useMediaQuery("(prefers-color-scheme: dark)");
if (isDark) { setScheme(isDark ? "dark-theme" : "light-theme");
setScheme("dark-theme");
} else {
setScheme("light-theme");
}
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<GlobalStyle /> <GlobalStyle />
<div> <div>
<SearchBar search={searchProviderData?.search} /> <SearchBar search={searchData} />
{(!themeData.error || !searchProviderData.error) && ( <Settings themes={themeData} search={searchData} />
<Settings <Greeter greeter={greeterData} />
themes={themeData?.themes} <AppList apps={appData?.apps} categories={appData?.categories} />
search={searchProviderData?.search} <BookmarkList groups={bookmarkData} />
/> <Imprint imprint={imprintData} />
)}
<Greeter data={greeterData.greeter} />
{!appData.error && (
<AppList apps={appData.apps} categories={appData.categories} />
)}
{!bookmarkData.error && <BookmarkList groups={bookmarkData.groups} />}
{!imprintData.error && <Imprint imprint={imprintData.imprint} />}
</div> </div>
</ThemeProvider> </ThemeProvider>
); );

View file

@ -1,77 +0,0 @@
import Icon from "./icon";
import styled from "styled-components";
const AppContainer = styled.a`
display: flex;
flex: 1 0 auto;
padding: 1rem;
color: ${(props) => props.theme.mainColor};
font-weight: 500;
text-transform: uppercase;
margin: 0;
text-decoration: none;
font-size: 1rem;
`;
const IconContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
margin-right: 0.5rem;
`;
const DetailsContainer = styled.div`
display: flex;
flex-direction: column;
`;
const AppName = styled.div`
a:hover {
text-decoration: underline;
}
`;
const AppDescription = styled.p`
text-transform: uppercase;
margin: 0;
font-size: 0.65rem;
font-weight: 400;
color: ${(props) => props.theme.accentColor};
`;
export interface IAppProps {
name: string;
icon: string;
url: string;
displayURL: string;
newTab?: boolean;
}
/**
* Renders a single app shortcut
* @param {IAppProps} props the props of the given app
* @returns {React.ReactNode} the child node for the given app
*/
const App = ({ name, icon, url, displayURL, newTab }: IAppProps) => {
const linkAttrs =
newTab !== undefined && newTab
? {
target: "_blank",
rel: "noopener noreferrer",
}
: {};
return (
<AppContainer href={url} {...linkAttrs}>
<IconContainer>
<Icon name={icon} />
</IconContainer>
<DetailsContainer>
<AppName>{name}</AppName>
<AppDescription>{displayURL}</AppDescription>
</DetailsContainer>
</AppContainer>
);
};
export default App;

View file

@ -1,42 +0,0 @@
import styled from "styled-components";
import App, { IAppProps } from "./app";
import { ItemList, Item, SubHeadline } from "./elements";
const CategoryHeadline = styled(SubHeadline)`
padding-top: 1rem;
`;
const CategoryContainer = styled.div`
width: 100%;
`;
export interface IAppCategoryProps {
name: string;
items: Array<IAppProps>;
}
/**
* Renders one app category
* @param {IAppCategoryProps} props props of the given category
* @returns {React.ReactNode} the app category node
*/
const AppCategory = ({ name, items }: IAppCategoryProps) => (
<CategoryContainer>
{name && <CategoryHeadline>{name}</CategoryHeadline>}
<ItemList>
{items.map(({ name, icon, displayURL, newTab, url }, index) => (
<Item key={[name, index].join("")}>
<App
name={name}
icon={icon}
url={url}
displayURL={displayURL}
newTab={newTab}
/>
</Item>
))}
</ItemList>
</CategoryContainer>
);
export default AppCategory;

View file

@ -1,42 +0,0 @@
import AppCategory, { IAppCategoryProps } from "./appCategory";
import { IAppProps } from "./app";
import { Headline, ListContainer } from "./elements";
export interface IAppListProps {
categories?: Array<IAppCategoryProps>;
apps?: Array<IAppProps>;
}
/**
* Renders one list containing all app categories and uncategorized apps
* @param {IAppListProps} props props of the given list of apps
* @returns {React.ReactNode} the app list component
*/
const AppList = ({ categories, apps }: IAppListProps) => {
if (apps || categories) {
return (
<ListContainer>
<Headline>Applications</Headline>
{categories &&
categories.map(({ name, items }, index) => (
<AppCategory
key={[name, index].join("")}
name={name}
items={items}
/>
))}
{apps && (
<AppCategory
name={categories ? "Uncategorized apps" : ""}
items={apps}
/>
)}
</ListContainer>
);
} else {
return <></>;
}
};
export default AppList;

View file

@ -69,15 +69,10 @@ export interface IAppCategoryProps {
} }
export interface IAppListProps { export interface IAppListProps {
categories?: Array<IAppCategoryProps>;
apps?: Array<IAppProps>; apps?: Array<IAppProps>;
categories?: Array<IAppCategoryProps>;
} }
export const defaults: IAppListProps = {
categories: [],
apps: [],
};
/** /**
* Renders a single app shortcut * Renders a single app shortcut
* @param {IAppProps} props the props of the given app * @param {IAppProps} props the props of the given app
@ -135,7 +130,7 @@ export const AppCategory = ({ name, items }: IAppCategoryProps) => (
* @returns {React.ReactNode} the app list component * @returns {React.ReactNode} the app list component
*/ */
export const AppList = ({ categories, apps }: IAppListProps) => { export const AppList = ({ categories, apps }: IAppListProps) => {
if (apps || categories) { if (apps || categories)
return ( return (
<ListContainer> <ListContainer>
<Headline>Applications</Headline> <Headline>Applications</Headline>
@ -155,9 +150,8 @@ export const AppList = ({ categories, apps }: IAppListProps) => {
)} )}
</ListContainer> </ListContainer>
); );
} else {
return <></>; return <></>;
}
}; };
export default AppList; export default AppList;

View file

@ -37,7 +37,7 @@ export interface IBookmarkGroupProps {
} }
export interface IBookmarkListProps { export interface IBookmarkListProps {
groups: Array<IBookmarkGroupProps>; groups?: Array<IBookmarkGroupProps>;
} }
/** /**
@ -63,15 +63,24 @@ export const BookmarkGroup = ({ name, items }: IBookmarkGroupProps) => (
* @param {IBookmarkListProps} props props of the given bookmark list * @param {IBookmarkListProps} props props of the given bookmark list
* @returns {React.ReactNode} the bookmark list component * @returns {React.ReactNode} the bookmark list component
*/ */
const BookmarkList = ({ groups }: IBookmarkListProps) => ( const BookmarkList = ({ groups }: IBookmarkListProps) => {
<ListContainer> if (groups)
<Headline>Bookmarks</Headline> return (
<ItemList> <ListContainer>
{groups.map(({ name, items }, index) => ( <Headline>Bookmarks</Headline>
<BookmarkGroup key={[name, index].join("")} name={name} items={items} /> <ItemList>
))} {groups.map(({ name, items }, index) => (
</ItemList> <BookmarkGroup
</ListContainer> key={[name, index].join("")}
); name={name}
items={items}
/>
))}
</ItemList>
</ListContainer>
);
return <></>;
};
export default BookmarkList; export default BookmarkList;

View file

@ -20,7 +20,7 @@ const DateText = styled.h3`
`; `;
export interface IGreeterComponentProps { export interface IGreeterComponentProps {
data: IGreeterProps; greeter?: IGreeterProps;
} }
export interface IGreeterProps { export interface IGreeterProps {
@ -112,13 +112,18 @@ export const getDateString = (
* @param {IGreeterComponentProps} data required greeter data * @param {IGreeterComponentProps} data required greeter data
* @returns {React.ReactNode} the greeter * @returns {React.ReactNode} the greeter
*/ */
const Greeter = ({ data }: IGreeterComponentProps) => ( const Greeter = ({ greeter }: IGreeterComponentProps) => {
<GreeterContainer> if (greeter)
<DateText> return (
{getDateString(data.days, data.months, data.dateformat)} <GreeterContainer>
</DateText> <DateText>
<GreetText>{getGreeting(data.greetings)}</GreetText> {getDateString(greeter.days, greeter.months, greeter.dateformat)}
</GreeterContainer> </DateText>
); <GreetText>{getGreeting(greeter.greetings)}</GreetText>
</GreeterContainer>
);
return <></>;
};
export default Greeter; export default Greeter;

View file

@ -31,16 +31,12 @@ const ItemContainer = styled.div`
`; `;
export interface IImprintProps { export interface IImprintProps {
name: IImprintFieldProps; fields: Array<IImprintFieldProps>;
address: IImprintFieldProps;
phone: IImprintFieldProps;
email: IImprintFieldProps;
url: IImprintFieldProps;
text: string; text: string;
} }
export interface IImprintComponentProps { export interface IImprintComponentProps {
imprint: IImprintProps; imprint?: IImprintProps;
} }
interface IImprintFieldComponentProps { interface IImprintFieldComponentProps {
@ -72,41 +68,49 @@ export const onClose = () => {
* @param {IImprintProps} props contents of the imprint * @param {IImprintProps} props contents of the imprint
* @returns {React.ReactNode} the imprint node * @returns {React.ReactNode} the imprint node
*/ */
const Imprint = ({ imprint }: IImprintComponentProps) => ( const Imprint = ({ imprint }: IImprintComponentProps) => {
<> if (imprint)
<ListContainer> return (
<Headline>About</Headline> <>
<ItemList> <ListContainer>
<ItemContainer> <Headline>About</Headline>
<SubHeadline>Imprint</SubHeadline> <ItemList>
<Modal <ItemContainer>
element="text" <SubHeadline>Imprint</SubHeadline>
text="View Imprint" <Modal
title="Legal Disclosure" element="text"
condition={!window.location.href.endsWith("#imprint")} text="View Imprint"
onClose={onClose} title="Legal Disclosure"
> condition={!window.location.href.endsWith("#imprint")}
<div> onClose={onClose}
<ModalSubHeadline> >
Information in accordance with section 5 TMG {imprint.fields && (
</ModalSubHeadline> <div>
<> <ModalSubHeadline>
{imprint.name && <ImprintField field={imprint.name} />} Information in accordance with section 5 TMG
{imprint.address && <ImprintField field={imprint.address} />} </ModalSubHeadline>
{imprint.email && <ImprintField field={imprint.email} />} <>
{imprint.phone && <ImprintField field={imprint.phone} />} {imprint.fields.map((field, index) => (
{imprint.url && <ImprintField field={imprint.url} />} <ImprintField
</> key={[field.text, index].join("")}
</div> field={field}
<div> />
<ModalSubHeadline>Imprint</ModalSubHeadline> ))}
{imprint.text && <Text>{imprint.text}</Text>} </>
</div> </div>
</Modal> )}
</ItemContainer> <div>
</ItemList> <ModalSubHeadline>Imprint</ModalSubHeadline>
</ListContainer> {imprint.text && <Text>{imprint.text}</Text>}
</> </div>
); </Modal>
</ItemContainer>
</ItemList>
</ListContainer>
</>
);
return <></>;
};
export default Imprint; export default Imprint;

View file

@ -48,7 +48,7 @@ export interface ISearchProps {
} }
interface ISearchBarProps { interface ISearchBarProps {
search: ISearchProps; search?: ISearchProps;
} }
export const handleQueryWithProvider = ( export const handleQueryWithProvider = (
@ -79,7 +79,13 @@ export const handleQueryWithProvider = (
* Renders a search bar * Renders a search bar
* @param {ISearchBarProps} search - The search providers for the search bar to use * @param {ISearchBarProps} search - The search providers for the search bar to use
*/ */
const SearchBar = ({ search }: ISearchBarProps) => { const SearchBar = ({
search = {
placeholder: "",
defaultProvider: "",
providers: [],
},
}: ISearchBarProps) => {
let [input, setInput] = useState<string>(""); let [input, setInput] = useState<string>("");
let [buttonsHidden, setButtonsHidden] = useState<boolean>(true); let [buttonsHidden, setButtonsHidden] = useState<boolean>(true);

View file

@ -54,11 +54,6 @@ const Code = styled.p`
color: ${(props) => props.theme.accentColor}; color: ${(props) => props.theme.accentColor};
`; `;
const ThemeHeader = styled.p`
grid-column: 1 / 4;
color: ${(props) => props.theme.accentColor};
`;
const ThemeSelect = styled(Select)` const ThemeSelect = styled(Select)`
-webkit-appearance: button; -webkit-appearance: button;
-moz-appearance: button; -moz-appearance: button;
@ -66,6 +61,7 @@ const ThemeSelect = styled(Select)`
text-transform: uppercase; text-transform: uppercase;
font-family: Roboto, sans-serif; font-family: Roboto, sans-serif;
font-weight: 400; font-weight: 400;
border-radius: 0;
border: 1px solid ${(props) => props.theme.mainColor}; border: 1px solid ${(props) => props.theme.mainColor};
color: ${(props) => props.theme.mainColor}; color: ${(props) => props.theme.mainColor};
background: none; background: none;
@ -76,8 +72,8 @@ const ThemeSelect = styled(Select)`
`; `;
interface ISettingsProps { interface ISettingsProps {
themes: Array<IThemeProps> | undefined; themes?: Array<IThemeProps>;
search: ISearchProps | undefined; search?: ISearchProps;
} }
/** /**
@ -99,20 +95,36 @@ const Settings = ({ themes, search }: ISettingsProps) => {
<Section> <Section>
<SectionHeadline>Theme:</SectionHeadline> <SectionHeadline>Theme:</SectionHeadline>
<FormContainer> <FormContainer>
<ThemeHeader>Light</ThemeHeader> <Table>
<ThemeSelect <tbody>
items={themes} <TableRow>
onChange={(theme: IThemeProps) => setNewLightTheme(theme)} <HeadCell>Light</HeadCell>
current={currentLightTheme} <HeadCell>Dark</HeadCell>
testId="light" </TableRow>
></ThemeSelect> <TableRow>
<ThemeHeader>Dark</ThemeHeader> <TableCell>
<ThemeSelect <ThemeSelect
items={themes} items={themes}
onChange={(theme: IThemeProps) => setNewDarkTheme(theme)} onChange={(theme: IThemeProps) =>
current={currentDarkTheme} setNewLightTheme(theme)
testId="dark" }
></ThemeSelect> current={currentLightTheme}
testId="light"
></ThemeSelect>
</TableCell>
<TableCell>
<ThemeSelect
items={themes}
onChange={(theme: IThemeProps) =>
setNewDarkTheme(theme)
}
current={currentDarkTheme}
testId="dark"
></ThemeSelect>
</TableCell>
</TableRow>
</tbody>
</Table>
</FormContainer> </FormContainer>
<Button <Button
data-testid="button-submit" data-testid="button-submit"

View file

@ -1,91 +0,0 @@
import React, { useCallback, useEffect, useState } from "react";
import { IAppListProps } from "../components/apps";
import { IThemeProps } from "./useTheme";
import { IBookmarkListProps } from "../components/bookmarks";
import { ISearchProps } from "../components/searchBar";
import { IImprintProps } from "../components/imprint";
import { IGreeterProps } from "../components/greeter";
const inProduction = process.env.NODE_ENV === "production";
interface IFetchItemProps {
url: string;
setHook?: React.Dispatch<React.SetStateAction<any>>;
}
interface IFetchListProps {
app: IFetchItemProps;
bookmarks: IFetchItemProps;
greeter: IFetchItemProps;
imprint: IFetchItemProps;
search: IFetchItemProps;
themes: IFetchItemProps;
}
let fetchList: IFetchListProps = {
app: { url: "/data/app.json" },
bookmarks: { url: "/data/bookmarks.json" },
greeter: { url: "/data/greeter.json" },
imprint: { url: "/data/imprint.json" },
search: { url: "/data/search.json" },
themes: { url: "/data/themes.json" },
};
export const handleResponse = (response: Response, type: string) => {
if (response.ok) return response.json();
throw new Error("Error fetching " + type + " data");
};
const handleError = (error: Error) => {
console.error(error.message);
};
const fetchURL = (url: string, type: string) => {
const response = inProduction ? fetch(url) : import(".." + url);
return response
.then((response: Response) => handleResponse(response, type))
.catch(handleError);
};
const useFetch = () => {
const [appData, setAppData] = useState<IAppListProps>();
fetchList.app.setHook = setAppData;
const [bookmarkData, setBookmarkData] = useState<IBookmarkListProps>();
fetchList.bookmarks.setHook = setBookmarkData;
const [greeterData, setGreeterData] = useState<IGreeterProps>();
fetchList.greeter.setHook = setGreeterData;
const [imprintData, setImprintData] = useState<IImprintProps>();
fetchList.imprint.setHook = setImprintData;
const [searchData, setSearchData] = useState<ISearchProps>();
fetchList.search.setHook = setSearchData;
const [themeData, setThemeData] = useState<Array<IThemeProps>>();
fetchList.themes.setHook = setThemeData;
const callback = useCallback(() => {
Object.entries(fetchList).forEach(([key, val]) => {
fetchURL(val.url, key).then((data) => {
val.setHook(data);
});
});
}, []);
useEffect(() => callback(), [callback]);
return {
appData,
bookmarkData,
greeterData,
imprintData,
searchData,
themeData,
callback,
};
};
export default useFetch;

32
src/lib/fetcher.d.ts vendored
View file

@ -1,32 +0,0 @@
import { ISearchProps } from "../components/searchBar";
import { IBookmarkGroupProps } from "../components/bookmarks";
import { IAppCategoryProps } from "../components/appCategory";
import { IAppProps } from "../components/app";
import { IThemeProps } from "./theme";
import { IImprintProps } from "../components/imprint";
import { IGreeterProps } from "../components/greeter";
declare module "../data/apps.json" {
export const categories: IAppCategoryProps[];
export const apps: IAppProps[];
}
declare module "../data/search.json" {
export const search: ISearchProps;
}
declare module "../data/bookmarks.json" {
export const groups: IBookmarkGroupProps[];
}
declare module "../data/themes.json" {
export const themes: IThemeProps[];
}
declare module "../data/imprint.json" {
export const imprint: IImprintProps;
}
declare module "../data/greeter.json" {
export const greeter: IGreeterProps;
}

View file

@ -1,278 +0,0 @@
import { useCallback, useEffect, useState } from "react";
import { ISearchProps } from "../components/searchBar";
import { IBookmarkGroupProps } from "../components/bookmarks";
import { IAppCategoryProps } from "../components/appCategory";
import { IAppProps } from "../components/app";
import { IThemeProps } from "./useTheme";
import { IImprintProps } from "../components/imprint";
import { IGreeterProps } from "../components/greeter";
const errorMessage = "Failed to load data.";
const inProduction = process.env.NODE_ENV === "production";
/**
* Handles the response from the fetch requests
* @param {Response} response - The response given by the fetch request
* @returns - The response in JSON
* @throws - Error with given error message if request failed
*/
export const handleResponse = (response: Response) => {
if (response.ok) return response.json();
throw new Error(errorMessage);
};
export interface ISearchDataProps {
search: ISearchProps;
error: string | boolean;
}
export interface IBookmarkDataProps {
groups: Array<IBookmarkGroupProps>;
error: string | boolean;
}
export interface IAppDataProps {
categories: Array<IAppCategoryProps>;
apps: Array<IAppProps>;
error: string | boolean;
}
export interface IThemeDataProps {
themes: Array<IThemeProps>;
error: string | boolean;
}
export interface IImprintDataProps {
imprint: IImprintProps;
error: string | boolean;
}
export interface IGreeterDataProps {
greeter: IGreeterProps;
error: string | boolean;
}
/**
* Default values for the respective state variables
*/
export const defaults = {
app: {
categories: [],
apps: [],
error: false,
},
bookmark: {
groups: [],
error: false,
},
search: {
search: {
placeholder: "",
defaultProvider: "https://google.com/search?q=",
providers: [],
},
error: false,
},
theme: {
themes: [],
error: false,
},
imprint: {
imprint: {
name: { text: "", link: "" },
address: { text: "", link: "" },
phone: { text: "", link: "" },
email: { text: "", link: "" },
url: { text: "", link: "" },
text: "",
},
error: false,
},
greeter: {
greeter: {
months: [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
],
days: [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
],
greetings: [
{
greeting: "Good night!",
start: 0,
end: 6,
},
{
greeting: "Good morning!",
start: 6,
end: 12,
},
{
greeting: "Good afternoon!",
start: 12,
end: 18,
},
{
greeting: "Good evening!",
start: 18,
end: 0,
},
],
dateformat: "%wd, %m %d%e %y",
},
error: false,
},
};
/**
* Handles fetch errors by returning the error message.
* @param {string} type - The type of fetch request that threw an error
* @param {Error} error - The error itself
*/
export const handleError = (status: string, error: Error) => {
switch (status) {
case "apps":
return { ...defaults.app, error: error.message };
case "bookmark":
return { ...defaults.bookmark, error: error.message };
case "searchProvider":
return { ...defaults.search, error: error.message };
case "theme":
return { ...defaults.theme, error: error.message };
case "imprint":
return { ...defaults.imprint, error: error.message };
case "greeter":
return { ...defaults.greeter, error: error.message };
default:
break;
}
};
/**
* Fetches all of the data by doing fetch requests (only available in production)
*/
export const fetchProduction = Promise.all([
fetch("/data/apps.json")
.then(handleResponse)
.catch((error: Error) => handleError("apps", error)),
fetch("/data/bookmarks.json")
.then(handleResponse)
.catch((error: Error) => handleError("bookmark", error)),
fetch("/data/search.json")
.then(handleResponse)
.catch((error: Error) => handleError("searchProvider", error)),
fetch("/data/themes.json")
.then(handleResponse)
.catch((error: Error) => handleError("theme", error)),
fetch("/data/imprint.json")
.then(handleResponse)
.catch((error: Error) => handleError("imprint", error)),
fetch("/data/greeter.json")
.then(handleResponse)
.catch((error: Error) => handleError("greeter", error)),
]);
/**
* Fetches all of the data by importing it (only available in development)
*/
export const fetchDevelopment = Promise.all([
import("../data/apps.json"),
import("../data/bookmarks.json"),
import("../data/search.json"),
import("../data/themes.json"),
import("../data/imprint.json"),
import("../data/greeter.json"),
]);
/**
* Fetches app, bookmark, search, theme and imprint data and returns it.
*/
export const useFetcher = () => {
const [appData, setAppData] = useState<IAppDataProps>(defaults.app);
const [bookmarkData, setBookmarkData] = useState<IBookmarkDataProps>(
defaults.bookmark,
);
const [searchProviderData, setSearchProviderData] =
useState<ISearchDataProps>(defaults.search);
const [themeData, setThemeData] = useState<IThemeDataProps>(defaults.theme);
const [imprintData, setImprintData] = useState<IImprintDataProps>(
defaults.imprint,
);
const [greeterData, setGreeterData] = useState<IGreeterDataProps>(
defaults.greeter,
);
const callback = useCallback(() => {
(inProduction ? fetchProduction : fetchDevelopment).then(
([
appData,
bookmarkData,
searchData,
themeData,
imprintData,
greeterData,
]: [
IAppDataProps,
IBookmarkDataProps,
ISearchDataProps,
IThemeDataProps,
IImprintDataProps,
IGreeterDataProps,
]) => {
setAppData(appData.error ? appData : { ...appData, error: false });
setBookmarkData(
bookmarkData.error ? bookmarkData : { ...bookmarkData, error: false },
);
setSearchProviderData(
searchData.error ? searchData : { ...searchData, error: false },
);
setThemeData(
themeData.error ? themeData : { ...themeData, error: false },
);
setImprintData(
imprintData.error ? imprintData : { ...imprintData, error: false },
);
setGreeterData(
greeterData.error ? greeterData : { ...greeterData, error: false },
);
},
);
}, []);
useEffect(() => callback(), [callback]);
return {
appData,
bookmarkData,
searchProviderData,
themeData,
imprintData,
greeterData,
callback,
};
};
export default useFetcher;

View file

@ -1,7 +1,7 @@
import { ISearchProps } from "../components/searchBar"; import { ISearchProps } from "../components/searchBar";
import { IBookmarkGroupProps } from "../components/bookmarks"; import { IBookmarkGroupProps } from "../components/bookmarks";
import { IAppProps, IAppCategoryProps } from "../components/apps"; import { IAppProps, IAppCategoryProps } from "../components/apps";
import { IThemeProps } from "./theme"; import { IThemeProps } from "./useTheme";
import { IImprintProps } from "../components/imprint"; import { IImprintProps } from "../components/imprint";
import { IGreeterProps } from "../components/greeter"; import { IGreeterProps } from "../components/greeter";

119
src/lib/useFetch.tsx Normal file
View file

@ -0,0 +1,119 @@
import { useCallback, useEffect, useState } from "react";
import { ISearchProps } from "../components/searchBar";
import { IBookmarkGroupProps } from "../components/bookmarks";
import { IAppProps, IAppCategoryProps } from "../components/apps";
import { IImprintProps } from "../components/imprint";
import { IGreeterProps } from "../components/greeter";
import { IThemeProps } from "./useTheme";
const inProduction = process.env.NODE_ENV === "production";
export interface IAppDataProps {
categories: Array<IAppCategoryProps>;
apps: Array<IAppProps>;
}
export interface IBookmarkDataProps {
groups: Array<IBookmarkGroupProps>;
}
export interface IGreeterDataProps {
greeter: IGreeterProps;
}
export interface IImprintDataProps {
imprint: IImprintProps;
}
export interface ISearchDataProps {
search: ISearchProps;
}
export interface IThemeDataProps {
themes: Array<IThemeProps>;
}
/**
* Fetches all of the data by doing fetch requests (only available in production)
*/
export const fetchProduction = Promise.all([
fetch("/data/apps.json")
.then((res) => res.json())
.catch(console.error),
fetch("/data/bookmarks.json")
.then((res) => res.json())
.catch(console.error),
fetch("/data/search.json")
.then((res) => res.json())
.catch(console.error),
fetch("/data/themes.json")
.then((res) => res.json())
.catch(console.error),
fetch("/data/imprint.json")
.then((res) => res.json())
.catch(console.error),
fetch("/data/greeter.json")
.then((res) => res.json())
.catch(console.error),
]);
/**
* Fetches all of the data by importing it (only available in development)
*/
export const fetchDevelopment = Promise.all([
import("../data/apps.json"),
import("../data/bookmarks.json"),
import("../data/search.json"),
import("../data/themes.json"),
import("../data/imprint.json"),
import("../data/greeter.json"),
]);
/**
* Fetches app, bookmark, search, theme and imprint data and returns it.
*/
export const useFetch = () => {
const [appData, setAppData] = useState<IAppDataProps>();
const [bookmarkData, setBookmarkData] =
useState<Array<IBookmarkGroupProps>>();
const [greeterData, setGreeterData] = useState<IGreeterProps>();
const [imprintData, setImprintData] = useState<IImprintProps>();
const [searchData, setSearchData] = useState<ISearchProps>();
const [themeData, setThemeData] = useState<Array<IThemeProps>>();
const callback = useCallback(() => {
(inProduction ? fetchProduction : fetchDevelopment).then(
([apps, { groups }, { search }, { themes }, { imprint }, { greeter }]: [
IAppDataProps,
IBookmarkDataProps,
ISearchDataProps,
IThemeDataProps,
IImprintDataProps,
IGreeterDataProps,
]) => {
setAppData(apps);
setBookmarkData(groups);
setSearchData(search);
setThemeData(themes);
setImprintData(imprint);
setGreeterData(greeter);
},
);
}, []);
useEffect(() => callback(), [callback]);
return {
appData,
bookmarkData,
searchData,
themeData,
imprintData,
greeterData,
callback,
};
};
export default useFetch;

View file

@ -1,7 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`app.tsx Tests app rendering with newTab=false 1`] = `[Function]`;
exports[`app.tsx Tests app rendering with newTab=true 1`] = `[Function]`;
exports[`app.tsx Tests app rendering without newTab 1`] = `[Function]`;

View file

@ -1,3 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AppCategory snapshot test 1`] = `[Function]`;

View file

@ -1,9 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`appList.tsx Tests AppList rendering with apps 1`] = `[Function]`;
exports[`appList.tsx Tests AppList rendering with categories 1`] = `[Function]`;
exports[`appList.tsx Tests AppList rendering with categories and apps 1`] = `[Function]`;
exports[`appList.tsx Tests AppList rendering with neither 1`] = `[Function]`;

View file

@ -17,3 +17,5 @@ exports[`app.tsx Tests AppList rendering 2`] = `[Function]`;
exports[`app.tsx Tests AppList rendering 3`] = `[Function]`; exports[`app.tsx Tests AppList rendering 3`] = `[Function]`;
exports[`app.tsx Tests AppList rendering 4`] = `[Function]`; exports[`app.tsx Tests AppList rendering 4`] = `[Function]`;
exports[`app.tsx Tests AppList rendering without any props 1`] = `[Function]`;

View file

@ -1,5 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BookmarkGroup snapshot test 1`] = `[Function]`; exports[`bookmarks.tsx Tests BookmarkGroup rendering 1`] = `[Function]`;
exports[`BookmarkList snapshot test 1`] = `[Function]`; exports[`bookmarks.tsx Tests BookmarkList rendering with props 1`] = `[Function]`;
exports[`bookmarks.tsx Tests BookmarkList rendering without props 1`] = `[Function]`;

View file

@ -1,3 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Greeter snapshot test 1`] = `[Function]`; exports[`greeter.tsx tests greeter rendering with props 1`] = `[Function]`;
exports[`greeter.tsx tests greeter rendering without props 1`] = `[Function]`;

View file

@ -1,5 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`imprint.tsx Tests Imprint 1`] = `[Function]`; exports[`imprint.tsx Tests Imprint rendering with props 1`] = `[Function]`;
exports[`imprint.tsx Tests ImprintField 1`] = `[Function]`; exports[`imprint.tsx Tests ImprintField 1`] = `[Function]`;
exports[`imprint.tsx Tests imprint rendering without props 1`] = `[Function]`;

View file

@ -1,3 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`searchBar.tsx Tests SearchBar rendering 1`] = `[Function]`; exports[`searchBar.tsx Tests SearchBar rendering with props 1`] = `[Function]`;
exports[`searchBar.tsx Tests SearchBar rendering without props 1`] = `[Function]`;

View file

@ -1,52 +0,0 @@
import { render } from "@testing-library/react";
import App, { IAppProps } from "../../components/app";
const props: IAppProps = {
name: "App Test",
icon: "bug_report",
url: "#",
displayURL: "test",
};
describe("app.tsx", () => {
it("Tests app rendering with newTab=true", () => {
const { asFragment } = render(
<App
name={props.name}
icon={props.icon}
url={props.url}
displayURL={props.displayURL}
newTab={true}
/>,
);
expect(asFragment).toMatchSnapshot();
});
it("Tests app rendering with newTab=false", () => {
const { asFragment } = render(
<App
name={props.name}
icon={props.icon}
url={props.url}
displayURL={props.displayURL}
newTab={false}
/>,
);
expect(asFragment).toMatchSnapshot();
});
it("Tests app rendering without newTab", () => {
const { asFragment } = render(
<App
name={props.name}
icon={props.icon}
url={props.url}
displayURL={props.displayURL}
/>,
);
expect(asFragment).toMatchSnapshot();
});
});

View file

@ -1,30 +0,0 @@
import { render } from "@testing-library/react";
import AppCategory, { IAppCategoryProps } from "../../components/appCategory";
const props: IAppCategoryProps = {
name: "Category Test",
items: [
{
name: "App Test",
icon: "bug_report",
url: "#",
displayURL: "test",
newTab: false,
},
{
name: "App Test",
icon: "bug_report",
url: "#",
displayURL: "test",
newTab: false,
},
],
};
it("AppCategory snapshot test", () => {
const { asFragment } = render(
<AppCategory name={props.name} items={props.items} />,
);
expect(asFragment).toMatchSnapshot();
});

View file

@ -1,60 +0,0 @@
import { render } from "@testing-library/react";
import AppList, { IAppListProps } from "../../components/appList";
const props: IAppListProps = {
categories: [
{
name: "Category Test",
items: [
{
name: "App Test",
icon: "bug_report",
url: "#",
displayURL: "test",
newTab: false,
},
{
name: "App Test",
icon: "bug_report",
url: "#",
displayURL: "test",
newTab: false,
},
],
},
],
apps: [
{
name: "App Test",
icon: "bug_report",
url: "#",
displayURL: "test",
newTab: false,
},
],
};
describe("appList.tsx", () => {
it("Tests AppList rendering with categories and apps", () => {
const { asFragment } = render(
<AppList categories={props.categories} apps={props.apps} />,
);
expect(asFragment).toMatchSnapshot();
});
it("Tests AppList rendering with categories", () => {
const { asFragment } = render(<AppList categories={props.categories} />);
expect(asFragment).toMatchSnapshot();
});
it("Tests AppList rendering with apps", () => {
const { asFragment } = render(<AppList apps={props.apps} />);
expect(asFragment).toMatchSnapshot();
});
it("Tests AppList rendering with neither", () => {
const { asFragment } = render(<AppList />);
expect(asFragment).toMatchSnapshot();
});
});

View file

@ -96,4 +96,9 @@ describe("app.tsx", () => {
expect(asFragment).toMatchSnapshot(); expect(asFragment).toMatchSnapshot();
}); });
}); });
it("Tests AppList rendering without any props", () => {
const { asFragment } = render(<AppList />);
expect(asFragment).toMatchSnapshot();
});
}); });

View file

@ -19,21 +19,28 @@ const bookmarkListProps: IBookmarkListProps = {
groups: [bookmarkGroupProps, bookmarkGroupProps], groups: [bookmarkGroupProps, bookmarkGroupProps],
}; };
it("BookmarkGroup snapshot test", () => { describe("bookmarks.tsx", () => {
const { asFragment } = render( it("Tests BookmarkGroup rendering", () => {
<BookmarkGroup const { asFragment } = render(
name={bookmarkGroupProps.name} <BookmarkGroup
items={bookmarkGroupProps.items} name={bookmarkGroupProps.name}
/>, items={bookmarkGroupProps.items}
); />,
);
expect(asFragment).toMatchSnapshot(); expect(asFragment).toMatchSnapshot();
}); });
it("BookmarkList snapshot test", () => { it("Tests BookmarkList rendering with props", () => {
const { asFragment } = render( const { asFragment } = render(
<BookmarkList groups={bookmarkListProps.groups} />, <BookmarkList groups={bookmarkListProps.groups} />,
); );
expect(asFragment).toMatchSnapshot(); expect(asFragment).toMatchSnapshot();
});
it("Tests BookmarkList rendering without props", () => {
const { asFragment } = render(<BookmarkList />);
expect(asFragment).toMatchSnapshot();
});
}); });

View file

@ -54,24 +54,31 @@ const props: IGreeterProps = {
dateformat: "%wd, %m %d%e %y", dateformat: "%wd, %m %d%e %y",
}; };
it("isBetween test", () => { describe("greeter.tsx", () => {
expect(isBetween(5, 1, 3)).toBeFalsy; it("tests isBetween", () => {
expect(isBetween(64, 1, 8)).toBeFalsy; expect(isBetween(5, 1, 3)).toBeFalsy;
expect(isBetween(-1, -5, -3)).toBeFalsy; expect(isBetween(64, 1, 8)).toBeFalsy;
expect(isBetween(4, 4, 4)).toBeTruthy; expect(isBetween(-1, -5, -3)).toBeFalsy;
expect(isBetween(3, 1, 8)).toBeTruthy; expect(isBetween(4, 4, 4)).toBeTruthy;
expect(isBetween(-3, -5, -1)).toBeTruthy; expect(isBetween(3, 1, 8)).toBeTruthy;
}); expect(isBetween(-3, -5, -1)).toBeTruthy;
});
it("getExtension test", () => { it("tests getExtension", () => {
expect(getExtension(0)).toEqual("th"); expect(getExtension(0)).toEqual("th");
expect(getExtension(1)).toEqual("st"); expect(getExtension(1)).toEqual("st");
expect(getExtension(2)).toEqual("nd"); expect(getExtension(2)).toEqual("nd");
expect(getExtension(3)).toEqual("rd"); expect(getExtension(3)).toEqual("rd");
expect(getExtension(15)).toEqual("th"); expect(getExtension(15)).toEqual("th");
}); });
it("Greeter snapshot test", () => { it("tests greeter rendering with props", () => {
const { asFragment } = render(<Greeter data={props} />); const { asFragment } = render(<Greeter greeter={props} />);
expect(asFragment).toMatchSnapshot(); expect(asFragment).toMatchSnapshot();
});
it("tests greeter rendering without props", () => {
const { asFragment } = render(<Greeter />);
expect(asFragment).toMatchSnapshot();
});
}); });

View file

@ -6,26 +6,16 @@ import Imprint, {
} from "../../components/imprint"; } from "../../components/imprint";
const props: IImprintProps = { const props: IImprintProps = {
name: { fields: [
text: "Test Name", {
link: "#", text: "Test Field",
}, link: "#",
address: { },
text: "Test Address", {
link: "#", text: "Test Field",
}, link: "#",
phone: { },
text: "Test Phone", ],
link: "#",
},
email: {
text: "Test Email",
link: "#",
},
url: {
text: "Test URL",
link: "#",
},
text: "This is a test!", text: "This is a test!",
}; };
@ -42,11 +32,16 @@ describe("imprint.tsx", () => {
}; };
}); });
it("Tests Imprint", () => { it("Tests Imprint rendering with props", () => {
const { asFragment } = render(<Imprint imprint={props} />); const { asFragment } = render(<Imprint imprint={props} />);
expect(asFragment).toMatchSnapshot(); expect(asFragment).toMatchSnapshot();
}); });
it("Tests imprint rendering without props", () => {
const { asFragment } = render(<Imprint />);
expect(asFragment).toMatchSnapshot();
});
it("Tests onClose with #imprint", () => { it("Tests onClose with #imprint", () => {
const location = window.location.href; const location = window.location.href;
window.location.href = location + "#imprint"; window.location.href = location + "#imprint";
@ -61,7 +56,7 @@ describe("imprint.tsx", () => {
}); });
it("Tests ImprintField", () => { it("Tests ImprintField", () => {
const { asFragment } = render(<ImprintField field={props.name} />); const { asFragment } = render(<ImprintField field={props.fields[0]} />);
expect(asFragment).toMatchSnapshot(); expect(asFragment).toMatchSnapshot();
}); });
}); });

View file

@ -54,11 +54,16 @@ describe("searchBar.tsx", () => {
}; };
}); });
it("Tests SearchBar rendering", () => { it("Tests SearchBar rendering with props", () => {
const { asFragment } = render(<SearchBar search={props} />); const { asFragment } = render(<SearchBar search={props} />);
expect(asFragment).toMatchSnapshot(); expect(asFragment).toMatchSnapshot();
}); });
it("Tests SearchBar rendering without props", () => {
const { asFragment } = render(<SearchBar />);
expect(asFragment).toMatchSnapshot();
});
it("Tests handleQueryWithProvider", () => { it("Tests handleQueryWithProvider", () => {
props.providers?.forEach((provider: ISearchProviderProps) => { props.providers?.forEach((provider: ISearchProviderProps) => {
handleQueryWithProvider(props, provider.prefix + " test"); handleQueryWithProvider(props, provider.prefix + " test");

View file

@ -1,46 +0,0 @@
import { ok } from "assert";
import useFetcher, {
defaults,
handleResponse,
handleError,
fetchProduction,
fetchDevelopment,
} from "../../lib/fetcher";
describe("fetcher.tsx", () => {
it("Tests handleResponse", () => {});
it("Tests handleError", () => {
expect(handleError("apps", Error("Test!"))).toEqual({
...defaults.app,
error: "Test!",
});
expect(handleError("bookmark", Error("Test!"))).toEqual({
...defaults.bookmark,
error: "Test!",
});
expect(handleError("searchProvider", Error("Test!"))).toEqual({
...defaults.search,
error: "Test!",
});
expect(handleError("theme", Error("Test!"))).toEqual({
...defaults.theme,
error: "Test!",
});
expect(handleError("imprint", Error("Test!"))).toEqual({
...defaults.imprint,
error: "Test!",
});
expect(handleError("greeter", Error("Test!"))).toEqual({
...defaults.greeter,
error: "Test!",
});
expect(handleError("", Error("Test!"))).toEqual(undefined);
});
});

View file

@ -1,4 +1,4 @@
import { getTheme, IThemeProps, setTheme } from "../../lib/useTheme"; import { getTheme, IThemeProps, setTheme, setScheme } from "../../lib/useTheme";
const props: IThemeProps = { const props: IThemeProps = {
label: "Classic", label: "Classic",
@ -27,7 +27,15 @@ const setup = () => {
}; };
}; };
describe("theme.tsx", () => { describe("useTheme.tsx", () => {
it("tests setScheme", () => {
setup();
setScheme("Test");
expect(window.localStorage.setItem).toHaveBeenCalledTimes(1);
expect(window.localStorage.setItem).toHaveBeenCalledWith("theme", "Test");
});
it("setTheme light test", () => { it("setTheme light test", () => {
setup(); setup();

View file

@ -14,7 +14,8 @@
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true,
"typeRoots": ["node_modules/@types", "./src/lib/useFetch.d.ts"]
}, },
"include": ["src", "src/test"] "include": ["src", "src/test"]
} }

View file

@ -6827,6 +6827,11 @@ jsesc@~0.5.0:
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=
json-loader@^0.5.7:
version "0.5.7"
resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d"
integrity sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==
json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"