Merge branch 'master' into feature/defaultsearchprovider

This commit is contained in:
phntxx 2021-06-21 20:16:40 +02:00
commit 0ce5d3aea6
53 changed files with 2934 additions and 1856 deletions

58
.circleci/config.yml Normal file
View file

@ -0,0 +1,58 @@
version: 2.1
commands:
install_dependencies:
description: "Installs dependencies and uses CircleCI's cache"
steps:
- checkout
- restore_cache:
keys:
- dependencies-{{ checksum "yarn.lock" }}
- dependencies-
- run:
command: |
yarn install
- save_cache:
paths:
- node_modules
key: dependencies-{{ checksum "yarn.lock" }}
jobs:
style:
docker:
- image: node:latest
steps:
- install_dependencies
- run:
name: prettier
command: |
yarn prettier --check
- run:
name: lint
command: |
yarn lint
dashboard:
docker:
- image: node:latest
steps:
- install_dependencies
- run:
name: typecheck
command: |
yarn typecheck
- run:
name: test
command: |
yarn test
- run:
name: coverage
command: |
yarn coverage
workflows:
version: 2
dashboard:
jobs:
- style
- dashboard

9
.eslintrc.js Normal file
View file

@ -0,0 +1,9 @@
module.exports = {
extends: ["eslint:recommended", "plugin:react-hooks/recommended"],
rules: {
maxClassesPerFile: 0,
maxLineLength: 0,
memberOrdering: 0,
variableName: 0,
},
};

2
.gitignore vendored
View file

@ -21,3 +21,5 @@
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*

21
.prettierrc.js Normal file
View file

@ -0,0 +1,21 @@
module.exports = {
bracketSpacing: true,
printWidth: 80,
parser: "typescript",
trailingComma: "all",
arrowParens: "always",
overrides: [
{
files: "README.md",
options: {
parser: "markdown",
},
},
{
files: "*.json",
options: {
parser: "json",
}
}
],
};

View file

@ -1,5 +1,12 @@
# Dashboard # Dashboard
[![CircleCI][shield-circleci]][circleci]
[![Docker Cloud Build Status][shield-docker]][docker]
[![Docker Image Size (latest)][shield-docker-image]][docker]
[![codecov][shield-codecov]][codecov]
[![Dependencies][shield-deps]][repo]
[![GitHub license][shield-license]][license]
![Alt text](/screenshot.png?raw=true "screenshot") ![Alt text](/screenshot.png?raw=true "screenshot")
Dashboard is just that - a dashboard. It's inspired by [SUI](https://github.com/jeroenpardon/sui) and has all the same features as SUI, such as simple customization through JSON-files and a handy search bar to search the internet more efficiently. Dashboard is just that - a dashboard. It's inspired by [SUI](https://github.com/jeroenpardon/sui) and has all the same features as SUI, such as simple customization through JSON-files and a handy search bar to search the internet more efficiently.
@ -199,3 +206,15 @@ In order for the imprint-modal to show up, make sure your `imprint.json` resembl
``` ```
> :exclamation: I haven't quite tested this. I'm not a lawyer and I'm not responsible if you're sued for using this incorrectly. > :exclamation: I haven't quite tested this. I'm not a lawyer and I'm not responsible if you're sued for using this incorrectly.
[docker]: https://hub.docker.com/r/phntxx/dashboard
[codecov]: https://codecov.io/gh/phntxx/dashboard
[repo]: https://github.com/phntxx/dashboard
[license]: https://github.com/phntxx/dashboard/LICENSE
[circleci]: https://circleci.com/gh/phntxx/dashboard
[shield-docker]: https://img.shields.io/docker/cloud/build/phntxx/dashboard
[shield-docker-image]: https://img.shields.io/docker/image-size/phntxx/dashboard/latest
[shield-circleci]: https://circleci.com/gh/phntxx/dashboard.svg?style=shield
[shield-codecov]: https://codecov.io/gh/phntxx/dashboard/branch/master/graph/badge.svg
[shield-license]: https://img.shields.io/github/license/phntxx/dashboard.svg
[shield-deps]: https://img.shields.io/david/phntxx/dashboard

13
codecov.yml Normal file
View file

@ -0,0 +1,13 @@
coverage:
status:
project:
default: off
dashboard:
target: 80%
flags: dashboard
flags:
dashboard:
paths:
- src/components/
- src/lib/

View file

@ -42,7 +42,7 @@
{ {
"greeting": "Good evening!", "greeting": "Good evening!",
"start": 18, "start": 18,
"end": 0 "end": 24
} }
], ],
"dateformat": "%wd, %m %d%e %y" "dateformat": "%wd, %m %d%e %y"

View file

@ -8,14 +8,13 @@
], ],
"private": false, "private": false,
"dependencies": { "dependencies": {
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^13.0.6",
"@types/jest": "^26.0.22",
"@types/node": "^14.14.37", "@types/node": "^14.14.37",
"@types/react-dom": "^17.0.3", "@types/react-dom": "^17.0.3",
"@types/react-select": "^4.0.13", "@types/react-select": "^4.0.13",
"@types/styled-components": "^5.1.9", "@types/styled-components": "^5.1.9",
"browserslist": "^4.16.6",
"http-server": "^0.12.3",
"npm-run-all": "^4.1.5",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-scripts": "^4.0.3", "react-scripts": "^4.0.3",
@ -23,11 +22,28 @@
"styled-components": "^5.2.1", "styled-components": "^5.2.1",
"typescript": "^4.2.3" "typescript": "^4.2.3"
}, },
"devDependencies": {
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^13.0.6",
"@types/jest": "^26.0.22",
"codecov": "^3.8.2",
"eslint": "^7.28.0",
"eslint-plugin-react-hooks": "^4.2.0",
"prettier": "^2.3.1"
},
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",
"coverage": "codecov -f coverage/*.json -F dashboard",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject" "typecheck": "tsc --noEmit",
"eject": "react-scripts eject",
"lint": "eslint --config .eslintrc.js",
"prettier": "prettier --config .prettierrc.js '{data,src}/**/*.{json,ts,tsx}'",
"http-server:data": "http-server ./ -c-1",
"http-server:app": "http-server ./build --proxy http://localhost:8080 --port 3000",
"serve:production": "npm-run-all --parallel http-server:data http-server:app"
}, },
"eslintConfig": { "eslintConfig": {
"extends": "react-app" "extends": "react-app"

View file

@ -3,7 +3,7 @@ import { createGlobalStyle } 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/appList";
import BookmarkList from "./components/bookmarkList"; 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";
@ -29,20 +29,27 @@ const GlobalStyle = createGlobalStyle`
* Renders the entire app by calling individual components * Renders the entire app by calling individual components
*/ */
const App = () => { const App = () => {
const {
const { appData, bookmarkData, searchProviderData, themeData, imprintData, greeterData } = useFetcher(); appData,
bookmarkData,
searchProviderData,
themeData,
imprintData,
greeterData,
} = useFetcher();
return ( return (
<> <>
<GlobalStyle /> <GlobalStyle />
<div> <div>
<SearchBar search={searchProviderData?.search} /> <SearchBar search={searchProviderData?.search} />
{!themeData.error && !searchProviderData.error && ( {!themeData.error ||
<Settings (!searchProviderData.error && (
themes={themeData?.themes} <Settings
search={searchProviderData?.search} themes={themeData?.themes}
/> search={searchProviderData?.search}
)} />
))}
<Greeter data={greeterData.greeter} /> <Greeter data={greeterData.greeter} />
{!appData.error && ( {!appData.error && (
<AppList apps={appData.apps} categories={appData.categories} /> <AppList apps={appData.apps} categories={appData.categories} />

View file

@ -1,12 +1,17 @@
import React, { useEffect } from "react";
import Icon from "./icon"; import Icon from "./icon";
import styled from "styled-components"; import styled from "styled-components";
import selectedTheme from "../lib/theme"; import selectedTheme from "../lib/theme";
const AppContainer = styled.div` const AppContainer = styled.a`
display: flex; display: flex;
flex: auto 25%; flex: 1 0 auto;
padding: 1rem; padding: 1rem;
color: ${selectedTheme.mainColor};
font-weight: 500;
text-transform: uppercase;
margin: 0;
text-decoration: none;
font-size: 1rem;
`; `;
const IconContainer = styled.div` const IconContainer = styled.div`
@ -21,16 +26,8 @@ const DetailsContainer = styled.div`
flex-direction: column; flex-direction: column;
`; `;
const AppLink = styled.a` const AppName = styled.div`
flex: 1 0 auto; a:hover & {
color: ${selectedTheme.mainColor};
font-weight: 500;
text-transform: uppercase;
margin: 0;
text-decoration: none;
font-size: 1rem;
&:hover {
text-decoration: underline; text-decoration: underline;
} }
`; `;
@ -53,26 +50,29 @@ export interface IAppProps {
/** /**
* 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
* @returns {React.ReactNode} the child node for the given app
*/ */
export const App = ({ name, icon, url, displayURL, newTab }: IAppProps) => { const App = ({ name, icon, url, displayURL, newTab }: IAppProps) => {
const linkAttrs =
useEffect(() => { console.log(newTab) }, [newTab]) newTab !== undefined && newTab
? {
target: "_blank",
rel: "noopener noreferrer",
}
: {};
return ( return (
<AppContainer> <AppContainer href={url} {...linkAttrs}>
<IconContainer> <IconContainer>
<Icon name={icon} /> <Icon name={icon} />
</IconContainer> </IconContainer>
<DetailsContainer> <DetailsContainer>
<AppName>{name}</AppName>
{
(newTab !== undefined && newTab) ?
<AppLink href={url} target="_blank" rel="noopener noreferrer">{name}</AppLink> : <AppLink href={url}>{name}</AppLink>
}
<AppDescription>{displayURL}</AppDescription> <AppDescription>{displayURL}</AppDescription>
</DetailsContainer> </DetailsContainer>
</AppContainer> </AppContainer>
); );
} };
export default App;

View file

@ -1,6 +1,5 @@
import React from "react";
import styled from "styled-components"; import styled from "styled-components";
import { App, IAppProps } from "./app"; import App, { IAppProps } from "./app";
import { ItemList, Item, SubHeadline } from "./elements"; import { ItemList, Item, SubHeadline } from "./elements";
const CategoryHeadline = styled(SubHeadline)` const CategoryHeadline = styled(SubHeadline)`
@ -18,9 +17,10 @@ export interface IAppCategoryProps {
/** /**
* Renders one app category * Renders one app category
* @param {IAppCategoryProps} props - The props of the given category * @param {IAppCategoryProps} props props of the given category
* @returns {React.ReactNode} the app category node
*/ */
export const AppCategory = ({ name, items }: IAppCategoryProps) => ( const AppCategory = ({ name, items }: IAppCategoryProps) => (
<CategoryContainer> <CategoryContainer>
{name && <CategoryHeadline>{name}</CategoryHeadline>} {name && <CategoryHeadline>{name}</CategoryHeadline>}
<ItemList> <ItemList>
@ -38,3 +38,5 @@ export const AppCategory = ({ name, items }: IAppCategoryProps) => (
</ItemList> </ItemList>
</CategoryContainer> </CategoryContainer>
); );
export default AppCategory;

View file

@ -1,4 +1,4 @@
import { AppCategory, IAppCategoryProps } from "./appCategory"; import AppCategory, { IAppCategoryProps } from "./appCategory";
import { IAppProps } from "./app"; import { IAppProps } from "./app";
import { Headline, ListContainer } from "./elements"; import { Headline, ListContainer } from "./elements";
@ -10,7 +10,8 @@ export interface IAppListProps {
/** /**
* Renders one list containing all app categories and uncategorized apps * Renders one list containing all app categories and uncategorized apps
* @param {IAppListProps} props - The props of the given list of apps * @param {IAppListProps} props props of the given list of apps
* @returns {React.ReactNode} the app list component
*/ */
const AppList = ({ categories, apps }: IAppListProps) => ( const AppList = ({ categories, apps }: IAppListProps) => (
<ListContainer> <ListContainer>
@ -20,10 +21,7 @@ const AppList = ({ categories, apps }: IAppListProps) => (
<AppCategory key={[name, idx].join("")} name={name} items={items} /> <AppCategory key={[name, idx].join("")} name={name} items={items} />
))} ))}
{apps && ( {apps && (
<AppCategory <AppCategory name={categories ? "Uncategorized apps" : ""} items={apps} />
name={categories ? "Uncategorized apps" : ""}
items={apps}
/>
)} )}
</ListContainer> </ListContainer>
); );

View file

@ -1,24 +0,0 @@
import React from "react";
import { Headline, ListContainer, ItemList } from "./elements";
import { BookmarkGroup, IBookmarkGroupProps } from "./bookmarkGroup";
interface IBookmarkListProps {
groups: Array<IBookmarkGroupProps>;
}
/**
* Renders a given list of categorized bookmarks
* @param {IBookmarkListProps} props - The props of the given bookmark list
*/
const BookmarkList = ({ groups }: IBookmarkListProps) => (
<ListContainer>
<Headline>Bookmarks</Headline>
<ItemList>
{groups.map(({ name, items }, idx) => (
<BookmarkGroup key={[name, idx].join("")} name={name} items={items} />
))}
</ItemList>
</ListContainer>
);
export default BookmarkList;

View file

@ -1,6 +1,11 @@
import React from "react";
import styled from "styled-components"; import styled from "styled-components";
import { Item, SubHeadline } from "./elements"; import {
Headline,
Item,
ItemList,
ListContainer,
SubHeadline,
} from "./elements";
import selectedTheme from "../lib/theme"; import selectedTheme from "../lib/theme";
const GroupContainer = styled.div` const GroupContainer = styled.div`
@ -32,9 +37,14 @@ export interface IBookmarkGroupProps {
items: Array<IBookmarkProps>; items: Array<IBookmarkProps>;
} }
export interface IBookmarkListProps {
groups: Array<IBookmarkGroupProps>;
}
/** /**
* Renders a given bookmark group * Renders a given bookmark group
* @param {IBookmarkGroupProps} props - The given props of the bookmark group * @param {IBookmarkGroupProps} props given props of the bookmark group
* @returns {React.ReactNode} the bookmark group component
*/ */
export const BookmarkGroup = ({ name, items }: IBookmarkGroupProps) => ( export const BookmarkGroup = ({ name, items }: IBookmarkGroupProps) => (
<Item> <Item>
@ -48,3 +58,21 @@ export const BookmarkGroup = ({ name, items }: IBookmarkGroupProps) => (
</GroupContainer> </GroupContainer>
</Item> </Item>
); );
/**
* Renders a given list of categorized bookmarks
* @param {IBookmarkListProps} props props of the given bookmark list
* @returns {React.ReactNode} the bookmark list component
*/
const BookmarkList = ({ groups }: IBookmarkListProps) => (
<ListContainer>
<Headline>Bookmarks</Headline>
<ItemList>
{groups.map(({ name, items }, idx) => (
<BookmarkGroup key={[name, idx].join("")} name={name} items={items} />
))}
</ItemList>
</ListContainer>
);
export default BookmarkList;

View file

@ -1,7 +1,5 @@
import React from "react";
import styled from "styled-components"; import styled from "styled-components";
import selectedTheme from "../lib/theme"; import selectedTheme from "../lib/theme";
import Icon from "./icon";
export const ListContainer = styled.div` export const ListContainer = styled.div`
padding: 2rem 0; padding: 2rem 0;
@ -56,29 +54,3 @@ export const Button = styled.button`
cursor: pointer; cursor: pointer;
} }
`; `;
const StyledButton = styled.button`
float: right;
border: none;
padding: 0;
background: none;
&:hover {
cursor: pointer;
}
`;
interface IIconButtonProps {
icon: string;
onClick: (e: React.FormEvent) => void;
}
/**
* Renders a button with an icon
* @param {IIconProps} props - The props of the given IconButton
*/
export const IconButton = ({ icon, onClick }: IIconButtonProps) => (
<StyledButton onClick={onClick}>
<Icon name={icon} />
</StyledButton>
);

View file

@ -20,6 +20,10 @@ const DateText = styled.h3`
color: ${selectedTheme.accentColor}; color: ${selectedTheme.accentColor};
`; `;
export interface IGreeterComponentProps {
data: IGreeterProps;
}
export interface IGreeterProps { export interface IGreeterProps {
months: Array<string>; months: Array<string>;
days: Array<string>; days: Array<string>;
@ -33,50 +37,48 @@ interface IGreetingProps {
end: number; end: number;
} }
interface IGreeterComponentProps {
data: IGreeterProps;
}
/** /**
* * Checks if a number is between two numbers
* @param a the number that's supposed to be checked * @param {number} a number that's supposed to be checked
* @param b the minimum * @param {number} b minimum
* @param c the maximum * @param {number} c maximum
*/ */
const isBetween = (a: number, b: number, c: number): boolean => (a > b && a < c) export const isBetween = (a: number, b: number, c: number): boolean =>
a >= b && a <= c;
/** /**
* Returns a greeting based on the current time * Returns a greeting based on the current time
* @returns {string} - A greeting * @param {Array<IGreetingProps>} greetings a list of greetings with start and end date
* @returns {string} a greeting
*/ */
const getGreeting = (greetings: Array<IGreetingProps>): string => { export const getGreeting = (greetings: Array<IGreetingProps>): string => {
let hours = Math.floor(new Date().getHours());
let hours = Math.floor(new Date().getHours())
let result = ""; let result = "";
greetings.forEach(greeting => { greetings.forEach((greeting) => {
if (isBetween(hours, greeting.start, greeting.end)) result = greeting.greeting; if (isBetween(hours, greeting.start, greeting.end))
}) result = greeting.greeting;
});
return result; return result;
}; };
/** /**
* Returns the appropriate extension for a number (eg. 'rd' for '3' to make '3rd') * Returns the appropriate extension for a number (eg. 'rd' for '3' to make '3rd')
* @param {number} day - The number of a day within a month * @param {number} day number of a day within a month
* @returns {string} - The extension for that number * @returns {string} extension for that number
*/ */
const getExtension = (day: number) => { export const getExtension = (day: number) => {
let extension = ""; let extension = "";
if ((day > 4 && day <= 20) || (day > 20 && day % 10 >= 4)) { if (day % 10 === 1) {
extension = "th";
} else if (day % 10 === 1) {
extension = "st"; extension = "st";
} else if (day % 10 === 2) { } else if (day % 10 === 2) {
extension = "nd"; extension = "nd";
} else if (day % 10 === 3) { } else if (day % 10 === 3) {
extension = "rd"; extension = "rd";
} else if (isBetween(day, 4, 20) || (day > 20 && day % 10 >= 4)) {
extension = "th";
} }
return extension; return extension;
@ -84,10 +86,14 @@ const getExtension = (day: number) => {
/** /**
* Generates the current date * Generates the current date
* @param {string} format - The format of the date string * @param {string} format format of the date string
* @returns {string} - The current date as a string * @returns {string} current date as a string
*/ */
const getDateString = (weekdays: Array<string>, months: Array<string>, format: string) => { export const getDateString = (
weekdays: Array<string>,
months: Array<string>,
format: string,
) => {
let currentDate = new Date(); let currentDate = new Date();
let weekday = weekdays[currentDate.getUTCDay()]; let weekday = weekdays[currentDate.getUTCDay()];
@ -96,15 +102,24 @@ const getDateString = (weekdays: Array<string>, months: Array<string>, format: s
let extension = getExtension(day); let extension = getExtension(day);
let year = currentDate.getFullYear(); let year = currentDate.getFullYear();
return format.replace("%wd", weekday).replace("%d", day.toString()).replace("%e", extension).replace("%m", month).replace("%y", year.toString()); return format
.replace("%wd", weekday)
.replace("%d", day.toString())
.replace("%e", extension)
.replace("%m", month)
.replace("%y", year.toString());
}; };
/** /**
* Renders the Greeter * Renders the Greeter
* @param {IGreeterComponentProps} data required greeter data
* @returns {React.ReactNode} the greeter
*/ */
const Greeter = ({ data }: IGreeterComponentProps) => ( const Greeter = ({ data }: IGreeterComponentProps) => (
<GreeterContainer> <GreeterContainer>
<DateText>{getDateString(data.days, data.months, data.dateformat)}</DateText> <DateText>
{getDateString(data.days, data.months, data.dateformat)}
</DateText>
<GreetText>{getGreeting(data.greetings)}</GreetText> <GreetText>{getGreeting(data.greetings)}</GreetText>
</GreeterContainer> </GreeterContainer>
); );

View file

@ -7,32 +7,62 @@ interface IIconProps {
size?: string; size?: string;
} }
interface IIconButtonProps {
testid?: string;
icon: string;
onClick: (e: React.FormEvent) => void;
}
const StyledButton = styled.button`
float: right;
border: none;
padding: 0;
background: none;
&:hover {
cursor: pointer;
}
`;
const IconContainer = styled.i`
font-family: "Material Icons";
font-weight: normal;
font-style: normal;
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
-moz-osx-font-smoothing: grayscale;
font-feature-settings: "liga";
font-size: ${(props) => props.about};
color: ${(props) => props.color};
`;
/** /**
* Renders an Icon * Renders an Icon
* @param {IIconProps} props - The props needed for the given icon * @param {IIconProps} props props needed for the given icon
* @returns {React.ReactNode} the icon node
*/ */
export const Icon = ({ name, size }: IIconProps) => { export const Icon = ({ name, size }: IIconProps) => (
<IconContainer color={selectedTheme.mainColor} about={size}>
{name}
</IconContainer>
);
let IconContainer = styled.i` /**
font-family: "Material Icons"; * Renders a button with an icon
font-weight: normal; * @param {IIconProps} props - The props of the given IconButton
font-style: normal; * @returns {React.ReactNode} the icon button node
font-size: ${size ? size : "24px"}; */
color: ${selectedTheme.mainColor}; export const IconButton = ({ testid, icon, onClick }: IIconButtonProps) => (
display: inline-block; <StyledButton data-testid={testid} onClick={onClick}>
line-height: 1; <Icon name={icon} />
text-transform: none; </StyledButton>
letter-spacing: normal; );
word-wrap: normal;
white-space: nowrap;
direction: ltr;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
-moz-osx-font-smoothing: grayscale;
font-feature-settings: "liga";
`;
return <IconContainer>{name}</IconContainer>;
};
export default Icon; export default Icon;

View file

@ -2,12 +2,7 @@ import React from "react";
import Modal from "./modal"; import Modal from "./modal";
import styled from "styled-components"; import styled from "styled-components";
import selectedTheme from "../lib/theme"; import selectedTheme from "../lib/theme";
import { import { ListContainer, ItemList, Headline, SubHeadline } from "./elements";
ListContainer,
ItemList,
Headline,
SubHeadline,
} from "./elements";
const ModalSubHeadline = styled(SubHeadline)` const ModalSubHeadline = styled(SubHeadline)`
display: block; display: block;
@ -37,11 +32,6 @@ const ItemContainer = styled.div`
padding: 1rem 0; padding: 1rem 0;
`; `;
interface IImprintFieldProps {
text: string;
link: string;
}
export interface IImprintProps { export interface IImprintProps {
name: IImprintFieldProps; name: IImprintFieldProps;
address: IImprintFieldProps; address: IImprintFieldProps;
@ -51,25 +41,32 @@ export interface IImprintProps {
text: string; text: string;
} }
export interface IImprintComponentProps {
imprint: IImprintProps;
}
interface IImprintFieldComponentProps { interface IImprintFieldComponentProps {
field: IImprintFieldProps; field: IImprintFieldProps;
} }
interface IImprintComponentProps { interface IImprintFieldProps {
imprint: IImprintProps; text: string;
link: string;
} }
/** /**
* Renders an imprint field * Renders an imprint field
* @param {IImprintFieldComponentProps} props - The data for the field * @param {IImprintFieldComponentProps} props data for the field
* @returns {React.ReactNode} the imprint field component
*/ */
const ImprintField = ({ field }: IImprintFieldComponentProps) => ( export const ImprintField = ({ field }: IImprintFieldComponentProps) => (
<Link href={field.link}>{field.text}</Link> <Link href={field.link}>{field.text}</Link>
); );
/** /**
* Renders the imprint component * Renders the imprint component
* @param {IImprintProps} props - The contents of the imprint * @param {IImprintProps} props contents of the imprint
* @returns {React.ReactNode} the imprint node
*/ */
const Imprint = ({ imprint }: IImprintComponentProps) => ( const Imprint = ({ imprint }: IImprintComponentProps) => (
<> <>
@ -85,15 +82,17 @@ const Imprint = ({ imprint }: IImprintComponentProps) => (
condition={!window.location.href.endsWith("#imprint")} condition={!window.location.href.endsWith("#imprint")}
onClose={() => { onClose={() => {
if (window.location.href.endsWith("#imprint")) { if (window.location.href.endsWith("#imprint")) {
let location = window.location.href.replace("#imprint", ""); window.location.href = window.location.href.replace(
window.location.href = location; "#imprint",
"",
);
} }
}} }}
> >
<div> <div>
<ModalSubHeadline> <ModalSubHeadline>
Information in accordance with section 5 TMG Information in accordance with section 5 TMG
</ModalSubHeadline> </ModalSubHeadline>
<> <>
{imprint.name && <ImprintField field={imprint.name} />} {imprint.name && <ImprintField field={imprint.name} />}
{imprint.address && <ImprintField field={imprint.address} />} {imprint.address && <ImprintField field={imprint.address} />}
@ -103,9 +102,7 @@ const Imprint = ({ imprint }: IImprintComponentProps) => (
</> </>
</div> </div>
<div> <div>
<ModalSubHeadline> <ModalSubHeadline>Imprint</ModalSubHeadline>
Imprint
</ModalSubHeadline>
{imprint.text && <Text>{imprint.text}</Text>} {imprint.text && <Text>{imprint.text}</Text>}
</div> </div>
</Modal> </Modal>

View file

@ -2,7 +2,8 @@ import React, { useState } from "react";
import styled from "styled-components"; import styled from "styled-components";
import selectedTheme from "../lib/theme"; import selectedTheme from "../lib/theme";
import { Headline, IconButton } from "./elements"; import { Headline } from "./elements";
import { IconButton } from "./icon";
const ModalContainer = styled.div` const ModalContainer = styled.div`
position: absolute; position: absolute;
@ -37,7 +38,7 @@ const TitleContainer = styled.div`
justify-content: space-between; justify-content: space-between;
`; `;
interface IModalProps { export interface IModalProps {
element: string; element: string;
icon?: string; icon?: string;
text?: string; text?: string;
@ -49,9 +50,18 @@ interface IModalProps {
/** /**
* Renders a modal with button to hide and un-hide * Renders a modal with button to hide and un-hide
* @param {IModalProps} props - The needed props for the modal * @param {IModalProps} props needed props for the modal
* @returns {React.ReactNode} the modal component
*/ */
const Modal = ({ element, icon, text, condition, title, onClose, children }: IModalProps) => { const Modal = ({
element,
icon,
text,
condition,
title,
onClose,
children,
}: IModalProps) => {
const [modalHidden, setModalHidden] = useState<boolean>(condition ?? true); const [modalHidden, setModalHidden] = useState<boolean>(condition ?? true);
const closeModal = () => { const closeModal = () => {
@ -62,17 +72,27 @@ const Modal = ({ element, icon, text, condition, title, onClose, children }: IMo
return ( return (
<> <>
{element === "icon" && ( {element === "icon" && (
<IconButton icon={icon ?? ""} onClick={() => closeModal()} /> <IconButton
icon={icon ?? ""}
testid="toggle-button"
onClick={() => closeModal()}
/>
)} )}
{element === "text" && ( {element === "text" && (
<Text onClick={() => closeModal()}>{text}</Text> <Text data-testid="toggle-button" onClick={() => closeModal()}>
{text}
</Text>
)} )}
<ModalContainer hidden={modalHidden}> <ModalContainer hidden={modalHidden}>
<TitleContainer> <TitleContainer>
<Headline>{title}</Headline> <Headline>{title}</Headline>
<IconButton icon="close" onClick={() => closeModal()} /> <IconButton
icon="close"
testid="close-button"
onClick={() => closeModal()}
/>
</TitleContainer> </TitleContainer>
{children} {children}
</ModalContainer> </ModalContainer>

View file

@ -16,18 +16,17 @@ const Search = styled.form`
const SearchInput = styled.input` const SearchInput = styled.input`
width: 100%; width: 100%;
margin: 0px;
font-size: 1rem; font-size: 1rem;
border: none; border: none;
border-bottom: 1px solid ${selectedTheme.accentColor}; border-bottom: 1px solid ${selectedTheme.accentColor};
border-radius: 0;
background: none; background: none;
border-radius: 0;
color: ${selectedTheme.mainColor}; color: ${selectedTheme.mainColor};
margin: 0px;
:focus { :focus {
outline: none; outline: none;
} }
@ -53,6 +52,30 @@ interface ISearchBarProps {
search: ISearchProps; search: ISearchProps;
} }
export const handleQueryWithProvider = (
search: ISearchProps,
query: string,
) => {
let queryArray: Array<string> = query.split(" ");
let prefix: string = queryArray[0];
queryArray.shift();
let searchQuery: string = queryArray.join(" ");
let providerFound: boolean = false;
if (search.providers) {
search.providers.forEach((provider: ISearchProviderProps) => {
if (provider.prefix === prefix) {
providerFound = true;
window.location.href = provider.url + searchQuery;
}
});
}
if (!providerFound) window.location.href = search.defaultProvider + query;
};
/** /**
* 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
@ -67,7 +90,7 @@ const SearchBar = ({ search }: ISearchBarProps) => {
var query: string = input || ""; var query: string = input || "";
if (query.split(" ")[0].includes("/")) { if (query.split(" ")[0].includes("/")) {
handleQueryWithProvider(query); handleQueryWithProvider(search, query);
} else { } else {
window.location.href = search.defaultProvider + query; window.location.href = search.defaultProvider + query;
} }
@ -75,32 +98,11 @@ const SearchBar = ({ search }: ISearchBarProps) => {
e.preventDefault(); e.preventDefault();
}; };
const handleQueryWithProvider = (query: string) => {
let queryArray: Array<string> = query.split(" ");
let prefix: string = queryArray[0];
queryArray.shift();
let searchQuery: string = queryArray.join(" ");
let providerFound: boolean = false;
if (search.providers) {
search.providers.forEach((provider: ISearchProviderProps) => {
if (provider.prefix === prefix) {
providerFound = true;
window.location.href = provider.url + searchQuery;
}
});
}
if (!providerFound)
window.location.href = search.defaultProvider + query;
};
return ( return (
<Search onSubmit={(e) => handleSearchQuery(e)}> <Search onSubmit={(e) => handleSearchQuery(e)}>
<SearchInput <SearchInput
type="text" type="text"
data-testid="search-input"
value={input} value={input}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setInput(e.target.value) setInput(e.target.value)
@ -108,12 +110,17 @@ const SearchBar = ({ search }: ISearchBarProps) => {
></SearchInput> ></SearchInput>
<SearchButton <SearchButton
type="button" type="button"
data-testid="search-clear"
onClick={() => setInput("")} onClick={() => setInput("")}
hidden={buttonsHidden} hidden={buttonsHidden}
> >
Clear Clear
</SearchButton> </SearchButton>
<SearchButton type="submit" hidden={buttonsHidden}> <SearchButton
type="submit"
data-testid="search-submit"
hidden={buttonsHidden}
>
Search Search
</SearchButton> </SearchButton>
</Search> </Search>

View file

@ -1,4 +1,4 @@
import React, { useState } from "react"; import { useState } from "react";
import styled from "styled-components"; import styled from "styled-components";
import Select, { ValueType } from "react-select"; import Select, { ValueType } from "react-select";
@ -9,36 +9,37 @@ import { Button, SubHeadline } from "./elements";
import Modal from "./modal"; import Modal from "./modal";
const FormContainer = styled.div` export const FormContainer = styled.div`
display: grid; display: grid;
grid-template-columns: auto auto auto; grid-template-columns: auto auto auto;
`; `;
const Table = styled.table` export const Table = styled.table`
font-weight: 400; font-weight: 400;
background: none; background: none;
width: 100%; width: 100%;
color: ${selectedTheme.mainColor}; color: ${selectedTheme.mainColor};
`; `;
const TableRow = styled.tr` export const TableRow = styled.tr`
border-bottom: 1px solid ${selectedTheme.mainColor}; border-bottom: 1px solid ${selectedTheme.mainColor};
`; `;
const TableCell = styled.td` export const TableCell = styled.td`
background: none;
padding-top: 0.5rem; padding-top: 0.5rem;
`; `;
const HeadCell = styled.th` export const HeadCell = styled.th`
font-weight: 700; font-weight: 700;
text-align: left; text-align: left;
`; `;
const Section = styled.div` export const Section = styled.div`
padding: 1rem 0; padding: 1rem 0;
`; `;
const SectionHeadline = styled(SubHeadline)` export const SectionHeadline = styled(SubHeadline)`
width: 100%; width: 100%;
border-bottom: 1px solid ${selectedTheme.accentColor}; border-bottom: 1px solid ${selectedTheme.accentColor};
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
@ -54,7 +55,7 @@ const Code = styled.p`
color: ${selectedTheme.accentColor}; color: ${selectedTheme.accentColor};
`; `;
const SelectorStyle: any = { export const SelectorStyle: any = {
container: (base: any): any => ({ container: (base: any): any => ({
...base, ...base,
margin: "0 2px", margin: "0 2px",
@ -72,15 +73,15 @@ const SelectorStyle: any = {
boxShadow: "none", boxShadow: "none",
"&:hover": { "&:hover": {
border: "1px solid", border: "1px solid",
borderColor: selectedTheme.mainColor borderColor: selectedTheme.mainColor,
}, },
}), }),
dropdownIndicator: (base: any): any => ({ dropdownIndicator: (base: any): any => ({
...base, ...base,
color: selectedTheme.mainColor, color: selectedTheme.mainColor,
"&:hover": { "&:hover": {
color: selectedTheme.mainColor color: selectedTheme.mainColor,
} },
}), }),
indicatorSeparator: () => ({ indicatorSeparator: () => ({
display: "none", display: "none",
@ -91,7 +92,7 @@ const SelectorStyle: any = {
border: "1px solid " + selectedTheme.mainColor, border: "1px solid " + selectedTheme.mainColor,
borderRadius: 0, borderRadius: 0,
boxShadow: "none", boxShadow: "none",
margin: "4px 0" margin: "4px 0",
}), }),
option: (base: any): any => ({ option: (base: any): any => ({
...base, ...base,
@ -125,7 +126,7 @@ interface ISettingsProps {
const Settings = ({ themes, search }: ISettingsProps) => { const Settings = ({ themes, search }: ISettingsProps) => {
const [newTheme, setNewTheme] = useState<IThemeProps>(); const [newTheme, setNewTheme] = useState<IThemeProps>();
if (themes && search) { if (themes || search) {
return ( return (
<Modal element="icon" icon="settings" title="Settings"> <Modal element="icon" icon="settings" title="Settings">
{themes && ( {themes && (
@ -133,6 +134,7 @@ const Settings = ({ themes, search }: ISettingsProps) => {
<SectionHeadline>Theme:</SectionHeadline> <SectionHeadline>Theme:</SectionHeadline>
<FormContainer> <FormContainer>
<Select <Select
classNamePrefix="list"
options={themes} options={themes}
defaultValue={selectedTheme} defaultValue={selectedTheme}
onChange={(e: ValueType<IThemeProps, false>) => { onChange={(e: ValueType<IThemeProps, false>) => {
@ -140,10 +142,20 @@ const Settings = ({ themes, search }: ISettingsProps) => {
}} }}
styles={SelectorStyle} styles={SelectorStyle}
/> />
<Button onClick={() => setTheme(JSON.stringify(newTheme))}> <Button
data-testid="button-submit"
onClick={() => {
if (newTheme) setTheme(newTheme);
}}
>
Apply Apply
</Button> </Button>
<Button onClick={() => window.location.reload()}>Refresh</Button> <Button
data-testid="button-refresh"
onClick={() => window.location.reload()}
>
Refresh
</Button>
</FormContainer> </FormContainer>
</Section> </Section>
)} )}
@ -155,24 +167,22 @@ const Settings = ({ themes, search }: ISettingsProps) => {
<Code>{search.defaultProvider}</Code> <Code>{search.defaultProvider}</Code>
</> </>
<> <>
{ {search.providers && (
search.providers && ( <Table>
<Table> <tbody>
<tbody> <TableRow>
<TableRow> <HeadCell>Search Provider</HeadCell>
<HeadCell>Search Provider</HeadCell> <HeadCell>Prefix</HeadCell>
<HeadCell>Prefix</HeadCell> </TableRow>
{search.providers.map((provider, index) => (
<TableRow key={provider.name + index}>
<TableCell>{provider.name}</TableCell>
<TableCell>{provider.prefix}</TableCell>
</TableRow> </TableRow>
{search.providers.map((provider, index) => ( ))}
<TableRow key={provider.name + index}> </tbody>
<TableCell>{provider.name}</TableCell> </Table>
<TableCell>{provider.prefix}</TableCell> )}
</TableRow>
))}
</tbody>
</Table>
)
}
</> </>
</Section> </Section>
)} )}

View file

@ -7,7 +7,7 @@ ReactDOM.render(
<React.StrictMode> <React.StrictMode>
<App /> <App />
</React.StrictMode>, </React.StrictMode>,
document.getElementById("root") document.getElementById("root"),
); );
// If you want your app to work offline and load faster, you can change // If you want your app to work offline and load faster, you can change

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

@ -0,0 +1,32 @@
import { ISearchProps } from "../components/searchBar";
import { IBookmarkGroupProps } from "../components/bookmarkGroup";
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,7 +1,7 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { ISearchProps } from "../components/searchBar"; import { ISearchProps } from "../components/searchBar";
import { IBookmarkGroupProps } from "../components/bookmarkGroup"; import { IBookmarkGroupProps } from "../components/bookmarks";
import { IAppCategoryProps } from "../components/appCategory"; import { IAppCategoryProps } from "../components/appCategory";
import { IAppProps } from "../components/app"; import { IAppProps } from "../components/app";
import { IThemeProps } from "./theme"; import { IThemeProps } from "./theme";
@ -17,7 +17,7 @@ const inProduction = process.env.NODE_ENV === "production";
* @returns - The response in JSON * @returns - The response in JSON
* @throws - Error with given error message if request failed * @throws - Error with given error message if request failed
*/ */
const handleResponse = (response: Response) => { export const handleResponse = (response: Response) => {
if (response.ok) return response.json(); if (response.ok) return response.json();
throw new Error(errorMessage); throw new Error(errorMessage);
}; };
@ -56,7 +56,7 @@ export interface IGreeterDataProps {
/** /**
* Default values for the respective state variables * Default values for the respective state variables
*/ */
const defaults = { export const defaults = {
app: { app: {
categories: [], categories: [],
apps: [], apps: [],
@ -102,7 +102,7 @@ const defaults = {
"September", "September",
"October", "October",
"November", "November",
"December" "December",
], ],
days: [ days: [
"Sunday", "Sunday",
@ -111,34 +111,34 @@ const defaults = {
"Wednesday", "Wednesday",
"Thursday", "Thursday",
"Friday", "Friday",
"Saturday" "Saturday",
], ],
greetings: [ greetings: [
{ {
greeting: "Good night!", greeting: "Good night!",
start: 0, start: 0,
end: 6 end: 6,
}, },
{ {
greeting: "Good morning!", greeting: "Good morning!",
start: 6, start: 6,
end: 12 end: 12,
}, },
{ {
greeting: "Good afternoon!", greeting: "Good afternoon!",
start: 12, start: 12,
end: 18 end: 18,
}, },
{ {
greeting: "Good evening!", greeting: "Good evening!",
start: 18, start: 18,
end: 0 end: 0,
} },
], ],
dateformat: "%wd, %m %d%e %y" dateformat: "%wd, %m %d%e %y",
}, },
error: false, error: false,
} },
}; };
/** /**
@ -146,47 +146,59 @@ const defaults = {
* @param {string} type - The type of fetch request that threw an error * @param {string} type - The type of fetch request that threw an error
* @param {Error} error - The error itself * @param {Error} error - The error itself
*/ */
const handleError = (status: string, error: Error) => { export const handleError = (status: string, error: Error) => {
switch (status) { switch (status) {
case "apps": case "apps":
return { ...defaults.app, error: error.message } return { ...defaults.app, error: error.message };
case "bookmark": case "bookmark":
return { ...defaults.bookmark, error: error.message } return { ...defaults.bookmark, error: error.message };
case "searchProvider": case "searchProvider":
return { ...defaults.search, error: error.message } return { ...defaults.search, error: error.message };
case "theme": case "theme":
return { ...defaults.theme, error: error.message } return { ...defaults.theme, error: error.message };
case "imprint": case "imprint":
return { ...defaults.imprint, error: error.message } return { ...defaults.imprint, error: error.message };
case "greeter": case "greeter":
return { ...defaults.greeter, error: error.message } return { ...defaults.greeter, error: error.message };
default: default:
break; break;
} }
} };
/** /**
* Fetches all of the data by doing fetch requests (only available in production) * Fetches all of the data by doing fetch requests (only available in production)
*/ */
const fetchProduction = Promise.all([ export const fetchProduction = Promise.all([
fetch("/data/apps.json").then(handleResponse).catch((error: Error) => handleError("apps", error)), fetch("/data/apps.json")
fetch("/data/bookmarks.json").then(handleResponse).catch((error: Error) => handleError("bookmark", error)), .then(handleResponse)
fetch("/data/search.json").then(handleResponse).catch((error: Error) => handleError("searchProvider", error)), .catch((error: Error) => handleError("apps", error)),
fetch("/data/themes.json").then(handleResponse).catch((error: Error) => handleError("theme", error)), fetch("/data/bookmarks.json")
fetch("/data/imprint.json").then(handleResponse).catch((error: Error) => handleError("imprint", error)), .then(handleResponse)
fetch("/data/greeter.json").then(handleResponse).catch((error: Error) => handleError("greeter", error)) .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) * Fetches all of the data by importing it (only available in development)
*/ */
const fetchDevelopment = Promise.all([ export const fetchDevelopment = Promise.all([
import("../data/apps.json"), import("../data/apps.json"),
import("../data/bookmarks.json"), import("../data/bookmarks.json"),
import("../data/search.json"), import("../data/search.json"),
import("../data/themes.json"), import("../data/themes.json"),
import("../data/imprint.json"), import("../data/imprint.json"),
import("../data/greeter.json") import("../data/greeter.json"),
]); ]);
/** /**
@ -196,32 +208,56 @@ export const useFetcher = () => {
const [appData, setAppData] = useState<IAppDataProps>(defaults.app); const [appData, setAppData] = useState<IAppDataProps>(defaults.app);
const [bookmarkData, setBookmarkData] = useState<IBookmarkDataProps>( const [bookmarkData, setBookmarkData] = useState<IBookmarkDataProps>(
defaults.bookmark defaults.bookmark,
); );
const [ const [searchProviderData, setSearchProviderData] =
searchProviderData, useState<ISearchDataProps>(defaults.search);
setSearchProviderData,
] = useState<ISearchDataProps>(defaults.search);
const [themeData, setThemeData] = useState<IThemeDataProps>(defaults.theme); const [themeData, setThemeData] = useState<IThemeDataProps>(defaults.theme);
const [imprintData, setImprintData] = useState<IImprintDataProps>( const [imprintData, setImprintData] = useState<IImprintDataProps>(
defaults.imprint defaults.imprint,
); );
const [greeterData, setGreeterData] = useState<IGreeterDataProps>(defaults.greeter); const [greeterData, setGreeterData] = useState<IGreeterDataProps>(
defaults.greeter,
);
const callback = useCallback(() => { const callback = useCallback(() => {
(inProduction ? fetchProduction : fetchDevelopment).then( (inProduction ? fetchProduction : fetchDevelopment).then(
([appData, bookmarkData, searchData, themeData, imprintData, greeterData]: [IAppDataProps, IBookmarkDataProps, ISearchDataProps, IThemeDataProps, IImprintDataProps, IGreeterDataProps]) => { ([
setAppData((appData.error) ? appData : { ...appData, error: false }); appData,
setBookmarkData((bookmarkData.error) ? bookmarkData : { ...bookmarkData, error: false }); bookmarkData,
setSearchProviderData((searchData.error) ? searchData : { ...searchData, error: false }); searchData,
setThemeData((themeData.error) ? themeData : { ...themeData, error: false }); themeData,
setImprintData((imprintData.error) ? imprintData : { ...imprintData, error: false }); imprintData,
setGreeterData((greeterData.error) ? greeterData : { ...greeterData, error: false }); 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 },
);
},
); );
}, []); }, []);
@ -234,7 +270,7 @@ export const useFetcher = () => {
themeData, themeData,
imprintData, imprintData,
greeterData, greeterData,
callback callback,
}; };
}; };

View file

@ -6,7 +6,7 @@ export interface IThemeProps {
backgroundColor: string; backgroundColor: string;
} }
const defaultTheme: IThemeProps = { export const defaultTheme: IThemeProps = {
label: "Classic", label: "Classic",
value: 0, value: 0,
mainColor: "#000000", mainColor: "#000000",
@ -18,8 +18,8 @@ const defaultTheme: IThemeProps = {
* Writes a given theme into localStorage * Writes a given theme into localStorage
* @param {string} theme - the theme that shall be saved (in stringified JSON) * @param {string} theme - the theme that shall be saved (in stringified JSON)
*/ */
export const setTheme = (theme: string) => { export const setTheme = (theme: IThemeProps) => {
if (theme !== undefined) localStorage.setItem("theme", theme); localStorage.setItem("theme", JSON.stringify(theme));
window.location.reload(); window.location.reload();
}; };
@ -27,12 +27,11 @@ export const setTheme = (theme: string) => {
* Function that gets the saved theme from localStorage or returns the default * Function that gets the saved theme from localStorage or returns the default
* @returns {IThemeProps} the saved theme or the default theme * @returns {IThemeProps} the saved theme or the default theme
*/ */
const getTheme = (): IThemeProps => { export const getTheme = (): IThemeProps => {
let selectedTheme = defaultTheme; let selectedTheme = defaultTheme;
if (localStorage.getItem("theme") !== null) { let theme = localStorage.getItem("theme");
selectedTheme = JSON.parse(localStorage.getItem("theme") || "{}"); if (theme !== null) selectedTheme = JSON.parse(theme || "{}");
}
return selectedTheme; return selectedTheme;
}; };

View file

@ -11,13 +11,13 @@
// opt-in, read https://bit.ly/CRA-PWA // opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean( const isLocalhost = Boolean(
window.location.hostname === 'localhost' || window.location.hostname === "localhost" ||
// [::1] is the IPv6 localhost address. // [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' || window.location.hostname === "[::1]" ||
// 127.0.0.0/8 are considered localhost for IPv4. // 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match( window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
) ),
); );
type Config = { type Config = {
@ -26,12 +26,9 @@ type Config = {
}; };
export function register(config?: Config) { export function register(config?: Config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
// The URL constructor is available in all browsers that support SW. // The URL constructor is available in all browsers that support SW.
const publicUrl = new URL( const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
process.env.PUBLIC_URL,
window.location.href
);
if (publicUrl.origin !== window.location.origin) { if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin // Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to // from what our page is served on. This might happen if a CDN is used to
@ -39,7 +36,7 @@ export function register(config?: Config) {
return; return;
} }
window.addEventListener('load', () => { window.addEventListener("load", () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) { if (isLocalhost) {
@ -50,8 +47,8 @@ export function register(config?: Config) {
// service worker/PWA documentation. // service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => { navigator.serviceWorker.ready.then(() => {
console.log( console.log(
'This web app is being served cache-first by a service ' + "This web app is being served cache-first by a service " +
'worker. To learn more, visit https://bit.ly/CRA-PWA' "worker. To learn more, visit https://bit.ly/CRA-PWA",
); );
}); });
} else { } else {
@ -65,21 +62,21 @@ export function register(config?: Config) {
function registerValidSW(swUrl: string, config?: Config) { function registerValidSW(swUrl: string, config?: Config) {
navigator.serviceWorker navigator.serviceWorker
.register(swUrl) .register(swUrl)
.then(registration => { .then((registration) => {
registration.onupdatefound = () => { registration.onupdatefound = () => {
const installingWorker = registration.installing; const installingWorker = registration.installing;
if (installingWorker == null) { if (installingWorker == null) {
return; return;
} }
installingWorker.onstatechange = () => { installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') { if (installingWorker.state === "installed") {
if (navigator.serviceWorker.controller) { if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched, // At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older // but the previous service worker will still serve the older
// content until all client tabs are closed. // content until all client tabs are closed.
console.log( console.log(
'New content is available and will be used when all ' + "New content is available and will be used when all " +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.' "tabs for this page are closed. See https://bit.ly/CRA-PWA.",
); );
// Execute callback // Execute callback
@ -90,7 +87,7 @@ function registerValidSW(swUrl: string, config?: Config) {
// At this point, everything has been precached. // At this point, everything has been precached.
// It's the perfect time to display a // It's the perfect time to display a
// "Content is cached for offline use." message. // "Content is cached for offline use." message.
console.log('Content is cached for offline use.'); console.log("Content is cached for offline use.");
// Execute callback // Execute callback
if (config && config.onSuccess) { if (config && config.onSuccess) {
@ -101,25 +98,25 @@ function registerValidSW(swUrl: string, config?: Config) {
}; };
}; };
}) })
.catch(error => { .catch((error) => {
console.error('Error during service worker registration:', error); console.error("Error during service worker registration:", error);
}); });
} }
function checkValidServiceWorker(swUrl: string, config?: Config) { function checkValidServiceWorker(swUrl: string, config?: Config) {
// Check if the service worker can be found. If it can't reload the page. // Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, { fetch(swUrl, {
headers: { 'Service-Worker': 'script' } headers: { "Service-Worker": "script" },
}) })
.then(response => { .then((response) => {
// Ensure service worker exists, and that we really are getting a JS file. // Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type'); const contentType = response.headers.get("content-type");
if ( if (
response.status === 404 || response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1) (contentType != null && contentType.indexOf("javascript") === -1)
) { ) {
// No service worker found. Probably a different app. Reload the page. // No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => { navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => { registration.unregister().then(() => {
window.location.reload(); window.location.reload();
}); });
@ -131,18 +128,18 @@ function checkValidServiceWorker(swUrl: string, config?: Config) {
}) })
.catch(() => { .catch(() => {
console.log( console.log(
'No internet connection found. App is running in offline mode.' "No internet connection found. App is running in offline mode.",
); );
}); });
} }
export function unregister() { export function unregister() {
if ('serviceWorker' in navigator) { if ("serviceWorker" in navigator) {
navigator.serviceWorker.ready navigator.serviceWorker.ready
.then(registration => { .then((registration) => {
registration.unregister(); registration.unregister();
}) })
.catch(error => { .catch((error) => {
console.error(error.message); console.error(error.message);
}); });
} }

View file

@ -2,4 +2,4 @@
// allows you to do things like: // allows you to do things like:
// expect(element).toHaveTextContent(/react/i) // expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom // learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect'; import "@testing-library/jest-dom/extend-expect";

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Tests Button 1`] = `[Function]`;
exports[`Tests Headline 1`] = `[Function]`;
exports[`Tests Lists 1`] = `[Function]`;
exports[`Tests SubHeadline 1`] = `[Function]`;

View file

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

View file

@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Icon test (no size) 1`] = `[Function]`;
exports[`Icon test 1`] = `[Function]`;
exports[`IconButton test 1`] = `[Function]`;

View file

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

View file

@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`modal.tsx Tests modal with icon 1`] = `[Function]`;
exports[`modal.tsx Tests modal with icon button 1`] = `[Function]`;
exports[`modal.tsx Tests modal with neither 1`] = `[Function]`;
exports[`modal.tsx Tests modal with text button 1`] = `[Function]`;

View file

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

View file

@ -0,0 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`settings.tsx Tests forms 1`] = `[Function]`;
exports[`settings.tsx Tests sections 1`] = `[Function]`;
exports[`settings.tsx Tests settings rendering 1`] = `[Function]`;
exports[`settings.tsx Tests settings rendering 2`] = `[Function]`;
exports[`settings.tsx Tests settings rendering 3`] = `[Function]`;
exports[`settings.tsx Tests settings rendering 4`] = `[Function]`;
exports[`settings.tsx Tests tables 1`] = `[Function]`;

View file

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

View file

@ -0,0 +1,30 @@
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

@ -0,0 +1,43 @@
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,
},
],
};
it("AppList snapshot test", () => {
const { asFragment } = render(
<AppList categories={props.categories} apps={props.apps} />,
);
expect(asFragment).toMatchSnapshot();
});

View file

@ -0,0 +1,39 @@
import { render } from "@testing-library/react";
import BookmarkList, {
BookmarkGroup,
IBookmarkGroupProps,
IBookmarkListProps,
} from "../../components/bookmarks";
const bookmarkGroupProps: IBookmarkGroupProps = {
name: "Test Group",
items: [
{
name: "Bookmark Test",
url: "#",
},
],
};
const bookmarkListProps: IBookmarkListProps = {
groups: [bookmarkGroupProps, bookmarkGroupProps],
};
it("BookmarkGroup snapshot test", () => {
const { asFragment } = render(
<BookmarkGroup
name={bookmarkGroupProps.name}
items={bookmarkGroupProps.items}
/>,
);
expect(asFragment).toMatchSnapshot();
});
it("BookmarkList snapshot test", () => {
const { asFragment } = render(
<BookmarkList groups={bookmarkListProps.groups} />,
);
expect(asFragment).toMatchSnapshot();
});

View file

@ -0,0 +1,39 @@
import { render } from "@testing-library/react";
import {
ListContainer,
Headline,
SubHeadline,
ItemList,
Item,
Button,
} from "../../components/elements";
it("Tests Lists", () => {
const { asFragment } = render(
<ListContainer>
<ItemList>
<Item>Test</Item>
</ItemList>
</ListContainer>,
);
expect(asFragment).toMatchSnapshot();
});
it("Tests Headline", () => {
const { asFragment } = render(<Headline>Test</Headline>);
expect(asFragment).toMatchSnapshot();
});
it("Tests SubHeadline", () => {
const { asFragment } = render(<SubHeadline>Test</SubHeadline>);
expect(asFragment).toMatchSnapshot();
});
it("Tests Button", () => {
const { asFragment } = render(<Button>Test</Button>);
expect(asFragment).toMatchSnapshot();
});

View file

@ -0,0 +1,77 @@
import { render } from "@testing-library/react";
import Greeter, {
IGreeterProps,
isBetween,
getExtension,
} from "../../components/greeter";
const props: IGreeterProps = {
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: 24,
},
],
dateformat: "%wd, %m %d%e %y",
};
it("isBetween test", () => {
expect(isBetween(5, 1, 3)).toBeFalsy;
expect(isBetween(64, 1, 8)).toBeFalsy;
expect(isBetween(-1, -5, -3)).toBeFalsy;
expect(isBetween(4, 4, 4)).toBeTruthy;
expect(isBetween(3, 1, 8)).toBeTruthy;
expect(isBetween(-3, -5, -1)).toBeTruthy;
});
it("getExtension test", () => {
expect(getExtension(1)).toEqual("st");
expect(getExtension(2)).toEqual("nd");
expect(getExtension(3)).toEqual("rd");
expect(getExtension(4)).toEqual("th");
expect(getExtension(55)).toEqual("th");
});
it("Greeter snapshot test", () => {
const { asFragment } = render(<Greeter data={props} />);
expect(asFragment).toMatchSnapshot();
});

View file

@ -0,0 +1,30 @@
import { fireEvent, render, screen } from "@testing-library/react";
import Icon, { IconButton } from "../../components/icon";
const props = {
name: "bug_report",
size: "20px",
onClick: () => console.log("test"),
};
it("Icon test", () => {
const { asFragment } = render(<Icon name={props.name} size={props.size} />);
expect(asFragment).toMatchSnapshot();
});
it("Icon test (no size)", () => {
const { asFragment } = render(<Icon name={props.name} />);
expect(asFragment).toMatchSnapshot();
});
it("IconButton test", () => {
const { asFragment } = render(
<IconButton icon={props.name} onClick={props.onClick} />,
);
expect(asFragment).toMatchSnapshot();
const handleClick = jest.fn();
render(<IconButton icon="question_answer" onClick={handleClick} />);
fireEvent.click(screen.getByText(/question_answer/i));
expect(handleClick).toHaveBeenCalledTimes(1);
});

View file

@ -0,0 +1,61 @@
import { fireEvent, render } from "@testing-library/react";
import Imprint, { IImprintProps, ImprintField } from "../../components/imprint";
const props: IImprintProps = {
name: {
text: "Test Name",
link: "#",
},
address: {
text: "Test Address",
link: "#",
},
phone: {
text: "Test Phone",
link: "#",
},
email: {
text: "Test Email",
link: "#",
},
url: {
text: "Test URL",
link: "#",
},
text: "This is a test!",
};
describe("imprint.tsx", () => {
beforeEach(() => {
delete global.window.location;
global.window = Object.create(window);
global.window.location = {
port: "123",
protocol: "http:",
hostname: "localhost",
href: "test",
};
});
it("Tests Imprint", () => {
const { asFragment } = render(<Imprint imprint={props} />);
expect(asFragment).toMatchSnapshot();
});
it("Tests #imprint", () => {
const location = window.location.href;
window.location.href = location + "#imprint";
const imprintModal = render(<Imprint imprint={props} />);
fireEvent.click(imprintModal.getByTestId("toggle-button"));
//fireEvent.click(imprintModal.getByTestId("close-button"));
expect(window.location.href).toEqual(location);
});
it("Tests ImprintField", () => {
const { asFragment } = render(<ImprintField field={props.name} />);
expect(asFragment).toMatchSnapshot();
});
});

View file

@ -0,0 +1,78 @@
import { fireEvent, render } from "@testing-library/react";
import Modal, { IModalProps } from "../../components/modal";
const iconProps: IModalProps = {
element: "icon",
icon: "bug_report",
title: "Test Modal",
onClose: jest.fn(),
children: <p>Hello</p>,
};
const invalidIconProps: IModalProps = {
element: "icon",
title: "Test Modal",
onClose: jest.fn(),
children: <p>Hello</p>,
};
const textProps: IModalProps = {
element: "text",
text: "Test",
title: "Test Modal",
onClose: jest.fn(),
children: <p>Hello</p>,
};
const noneProps: IModalProps = {
element: "none",
title: "Test Modal",
children: <p>Hello</p>,
};
const setup = (props: IModalProps) => {
const modal = render(
<Modal
element={props.element}
icon={props.icon}
text={props.text}
title={props.title}
onClose={props.onClose}
>
{props.children}
</Modal>,
);
return modal;
};
describe("modal.tsx", () => {
it("Tests modal with icon button", () => {
const modal = setup(iconProps);
expect(modal.asFragment).toMatchSnapshot();
fireEvent.click(modal.getByTestId("toggle-button"));
fireEvent.click(modal.getByTestId("close-button"));
expect(iconProps.onClose).toHaveBeenCalledTimes(2);
});
it("Tests modal with text button", () => {
const modal = setup(textProps);
expect(modal.asFragment).toMatchSnapshot();
fireEvent.click(modal.getByTestId("toggle-button"));
fireEvent.click(modal.getByTestId("close-button"));
expect(textProps.onClose).toHaveBeenCalledTimes(2);
});
it("Tests modal with neither", () => {
const modal = setup(noneProps);
expect(modal.asFragment).toMatchSnapshot();
});
it("Tests modal with icon", () => {
const modal = setup(invalidIconProps);
expect(modal.asFragment).toMatchSnapshot();
fireEvent.click(modal.getByTestId("toggle-button"));
fireEvent.click(modal.getByTestId("close-button"));
expect(invalidIconProps.onClose).toHaveBeenCalledTimes(2);
});
});

View file

@ -0,0 +1,101 @@
import { fireEvent, render } from "@testing-library/react";
import SearchBar, {
handleQueryWithProvider,
ISearchProviderProps,
ISearchBarProps,
} from "../../components/searchBar";
const providers: Array<ISearchProviderProps> = [
{
name: "Allmusic",
url: "https://www.allmusic.com/search/all/",
prefix: "/a",
},
{
name: "Discogs",
url: "https://www.discogs.com/search/?q=",
prefix: "/di",
},
{
name: "Duck Duck Go",
url: "https://duckduckgo.com/?q=",
prefix: "/d",
},
];
const setup = () => {
const searchBar = render(<SearchBar providers={providers} />);
const input = searchBar.getByTestId("search-input");
const clearButton = searchBar.getByTestId("search-clear");
const submitButton = searchBar.getByTestId("search-submit");
return {
searchBar,
input,
clearButton,
submitButton,
};
};
describe("searchBar.tsx", () => {
beforeEach(() => {
delete global.window.location;
global.window = Object.create(window);
global.window.location = {
port: "123",
protocol: "http:",
hostname: "localhost",
};
});
it("Tests SearchBar rendering", () => {
const { asFragment } = render(<SearchBar providers={providers} />);
expect(asFragment).toMatchSnapshot();
});
it("Tests handleQueryWithProvider", () => {
providers.forEach((provider: ISearchProviderProps) => {
handleQueryWithProvider(providers, provider.prefix + " test");
expect(window.location.href).toEqual(provider.url + "test");
});
});
it("Tests handleQueryWithProvider default", () => {
handleQueryWithProvider(providers, "test");
expect(window.location.href).toEqual("https://google.com/search?q=test");
});
it("Tests handleQueryWithProvider without providers", () => {
handleQueryWithProvider(undefined, "test");
expect(window.location.href).toEqual("https://google.com/search?q=test");
});
it("Tests SearchBar component with default search", () => {
const { input, submitButton } = setup();
fireEvent.change(input, { target: { value: "test" } });
fireEvent.click(submitButton);
expect(window.location.href).toEqual("https://google.com/search?q=test");
});
it("Tests SearchBar component with other search", () => {
const { input, submitButton } = setup();
fireEvent.change(input, { target: { value: "/a test" } });
fireEvent.click(submitButton);
expect(window.location.href).toEqual(
"https://www.allmusic.com/search/all/test",
);
});
it("Tests SearchBar component clear", () => {
const { input, clearButton, submitButton } = setup();
fireEvent.change(input, { target: { value: "/a test" } });
fireEvent.click(clearButton);
fireEvent.click(submitButton);
expect(window.location.href).toEqual("https://google.com/search?q=");
});
});

View file

@ -0,0 +1,143 @@
import { fireEvent, render } from "@testing-library/react";
import Settings, {
FormContainer,
Table,
TableRow,
TableCell,
HeadCell,
Section,
SectionHeadline,
SelectorStyle,
} from "../../components/settings";
import { ISearchProviderProps } from "../../components/searchBar";
import { IThemeProps } from "../../lib/theme";
const themes: Array<IThemeProps> = [
{
label: "Classic",
value: 0,
mainColor: "#000000",
accentColor: "#1e272e",
backgroundColor: "#ffffff",
},
{
label: "Classic",
value: 0,
mainColor: "#000000",
accentColor: "#1e272e",
backgroundColor: "#ffffff",
},
];
const providers: Array<ISearchProviderProps> = [
{
name: "Allmusic",
url: "https://www.allmusic.com/search/all/",
prefix: "/a",
},
{
name: "Discogs",
url: "https://www.discogs.com/search/?q=",
prefix: "/di",
},
{
name: "Duck Duck Go",
url: "https://duckduckgo.com/?q=",
prefix: "/d",
},
];
const propsList = [
{
themes: themes,
providers: providers,
},
{
themes: themes,
providers: undefined,
},
{
themes: undefined,
providers: providers,
},
{
themes: undefined,
providers: undefined,
},
];
describe("settings.tsx", () => {
const location: Location = window.location;
8;
beforeEach(() => {
// @ts-ignore
delete window.location;
window.location = {
...location,
reload: jest.fn(),
};
});
it("Tests forms", () => {
const { asFragment } = render(<FormContainer />);
expect(asFragment).toMatchSnapshot();
});
it("Tests tables", () => {
const { asFragment } = render(
<Table>
<tbody>
<TableRow>
<HeadCell>Test</HeadCell>
</TableRow>
<TableRow>
<TableCell>Test</TableCell>
</TableRow>
</tbody>
</Table>,
);
expect(asFragment).toMatchSnapshot();
});
it("Tests sections", () => {
const { asFragment } = render(
<Section>
<SectionHeadline>Test</SectionHeadline>
</Section>,
);
expect(asFragment).toMatchSnapshot();
});
it("Tests settings rendering", () => {
propsList.forEach((props) => {
const settings = render(
<Settings themes={props.themes} providers={props.providers} />,
);
expect(settings.asFragment).toMatchSnapshot();
});
});
// TODO: Finish this test
it("Tests theme setting", () => {
const settings = render(
<Settings
themes={propsList[0].themes}
providers={propsList[0].providers}
/>,
);
const toggleButton = settings.getByTestId("toggle-button");
const submitButton = settings.getByTestId("button-submit");
const refreshButton = settings.getByTestId("button-refresh");
fireEvent.click(toggleButton);
fireEvent.click(submitButton);
});
});

View file

@ -0,0 +1,46 @@
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

@ -0,0 +1,54 @@
import { getTheme, IThemeProps, setTheme } from "../../lib/theme";
const props: IThemeProps = {
label: "Classic",
value: 0,
mainColor: "#000000",
accentColor: "#1e272e",
backgroundColor: "#ffffff",
};
const location: Location = window.location;
const setup = () => {
Object.defineProperty(window, "localStorage", {
value: {
getItem: jest.fn(() => JSON.stringify(props)),
setItem: jest.fn(() => null),
},
writable: true,
});
// @ts-ignore
delete window.location;
window.location = {
...location,
reload: jest.fn(),
};
};
describe("theme.tsx", () => {
it("setTheme test", () => {
setup();
setTheme(props);
expect(window.localStorage.setItem).toHaveBeenCalledTimes(1);
expect(window.localStorage.setItem).toHaveBeenCalledWith(
"theme",
JSON.stringify(props),
);
expect(window.location.reload).toHaveBeenCalledTimes(1);
});
it("Tests getTheme", () => {
setup();
let themeTest = getTheme();
expect(themeTest).toEqual(props);
});
it("Tests getTheme with empty parameters", () => {
localStorage.setItem("theme", "");
expect(getTheme()).toEqual({});
});
});

View file

@ -1,12 +1,8 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es5",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom", "allowJs": false,
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
@ -20,7 +16,5 @@
"jsx": "react-jsx", "jsx": "react-jsx",
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true
}, },
"include": [ "include": ["src", "src/test"]
"src"
]
} }

3030
yarn.lock

File diff suppressed because it is too large Load diff