commit
b5c0e400ea
24 changed files with 693 additions and 657 deletions
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2020 Bastian Meissner
|
Copyright (c) 2021 Bastian Meissner
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
339
README.md
339
README.md
|
@ -1,33 +1,61 @@
|
||||||
# Dashboard
|
# Dashboard
|
||||||
|
|
||||||
## IMPORTANT: UPDATE
|
|
||||||
|
|
||||||
Yesterday, an update has been released that changed a couple of things:
|
|
||||||
|
|
||||||
- The serving port has been changed from `3000` to `8080`.
|
|
||||||
- The structure of `imprint.json` has been changed. Make sure that the format of your `imprint.json`-file matches the format of the ones within this repository.
|
|
||||||
|
|
||||||
![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.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
So what makes this thing better than SUI?
|
So what makes this project different from (or even better than) SUI?
|
||||||
|
|
||||||
- "Display URL" functionality, in case the URL you want to show is different than the URL you want to be redirected to
|
- "Display URL" functionality (The URL displayed for apps can differ from the actual URL)
|
||||||
- Theming through JSON
|
- Categorization for apps
|
||||||
- Search providers customizable through JSON (SUI has them both in a JSON and hardcoded)
|
- Themes and search providers can be changed using JSON
|
||||||
|
- Imprint functionality
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Getting Dashboard to run is fairly simple and can be accomplished with two techniques:
|
The recommended way of installation is using [Docker](https://docker.com). You could also build your own version from source, but do proceed at your own risk.
|
||||||
|
|
||||||
1. Locally
|
### Docker
|
||||||
|
|
||||||
Prerequisites: yarn, npm, node
|
The Docker image is built on top of [this image](https://github.com/ratisbona-coding/nginx-cloudflare-cache), as it's based on Nginx and also provides functionality to purge the Cloudflare cache every time the container restarts (though this functionality is entirely optional).
|
||||||
|
|
||||||
|
1. Using the Docker CLI:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ docker run -d \
|
||||||
|
-e CLOUDFLARE_ZONE_ID=[OPTIONAL CLOUDFLARE V4 ZONE ID] \
|
||||||
|
-e CLOUDFLARE_PURGE_TOKEN=[OPTIONAL CLOUDFLARE PURGE TOKEN] \
|
||||||
|
-v $(pwd)/data:/app/data
|
||||||
|
-p 8080:8080 \
|
||||||
|
--name dashboard \
|
||||||
|
phntxx/dashboard
|
||||||
```
|
```
|
||||||
|
|
||||||
|
2. Using Docker-Compose:
|
||||||
|
|
||||||
|
```yml
|
||||||
|
version: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
dashboard:
|
||||||
|
image: phntxx/dashboard:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- CLOUDFLARE_ZONE_ID=[OPTIONAL CLOUDFLARE V4 ZONE ID]
|
||||||
|
- CLOUDFLARE_PURGE_TOKEN=[OPTIONAL CLOUDFLARE PURGE TOKEN]
|
||||||
|
volumes:
|
||||||
|
- [path to data directory]:/app/data
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile from source
|
||||||
|
|
||||||
|
I really don't anticipate people to use this, so go forth at your own risk.
|
||||||
|
|
||||||
|
```bash
|
||||||
$ git clone https://github.com/phntxx/dashboard.git
|
$ git clone https://github.com/phntxx/dashboard.git
|
||||||
$ cd dashboard/
|
$ cd dashboard/
|
||||||
$ yarn
|
$ yarn
|
||||||
|
@ -35,196 +63,139 @@ $ yarn build
|
||||||
$ yarn serve:production
|
$ yarn serve:production
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Using Docker
|
## Configuration
|
||||||
|
|
||||||
```
|
There's a couple of things you can / need to configure to get Dashboard to look and behave just as you want it to.
|
||||||
$ docker run -d \
|
|
||||||
-v $(pwd)/data:/app/data
|
|
||||||
-p 8080:8080 \
|
|
||||||
--name dashboard \
|
|
||||||
phntxx/dashboard
|
|
||||||
```
|
|
||||||
|
|
||||||
Sample Docker Compose configuration:
|
If you don't require a specific component, just remove the file from your `data`-directory. Dashboard won't render the components whose files are not present. With no files present, only the greeter will be shown.
|
||||||
|
|
||||||
```
|
If you're running into problems with configuring your files and you can't seem to get them to work, feel free to open an issue, I'd be happy to help! :smile:
|
||||||
version: "3"
|
|
||||||
|
|
||||||
services:
|
### Apps
|
||||||
dashboard:
|
|
||||||
image: phntxx/dashboard:latest
|
|
||||||
restart: unless-stopped
|
|
||||||
volumes:
|
|
||||||
- [path to data directory]:/app/data
|
|
||||||
ports:
|
|
||||||
- 8080:8080
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note: You might still need to clone the repository in order to get the JSON-files which are required for the
|
To show the apps you want to show, change `apps.json` to resemble the following:
|
||||||
app to run**
|
|
||||||
|
|
||||||
## Customization
|
```json
|
||||||
|
|
||||||
Dashboard is designed to be customizable. Everything is handled using four .json-files, which can be found at /src/components/data
|
|
||||||
|
|
||||||
### Applications
|
|
||||||
|
|
||||||
To add an application, append the following to apps.json or simply edit one of the examples given.
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
{
|
||||||
"name": "[Name of the Application]",
|
"categories": [
|
||||||
"displayURL": "[URL you want to show]",
|
{
|
||||||
"URL": "[URL to redirect to]",
|
"name": "[Name of the category]",
|
||||||
"icon": "[Icon code]"
|
"items": [
|
||||||
|
{
|
||||||
|
"name": "[Name of the app]",
|
||||||
|
"displayURL": "[URL you want to show]",
|
||||||
|
"URL": "[URL to redirect to]",
|
||||||
|
"icon": "[Icon code]"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
},
|
||||||
|
...
|
||||||
|
],
|
||||||
|
"apps": [
|
||||||
|
{
|
||||||
|
"name": "[Name of the app]",
|
||||||
|
"displayURL": "[URL you want to show]",
|
||||||
|
"URL": "[URL to redirect to]",
|
||||||
|
"icon": "[Icon code]"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
}
|
}
|
||||||
...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Wherein either `apps` or `categories` can be omitted as needed.
|
||||||
|
|
||||||
To find icons, simply go to the [Material Design Icon Library](https://material.io/icons/) and copy one of the codes for an icon there.
|
To find icons, simply go to the [Material Design Icon Library](https://material.io/icons/) and copy one of the codes for an icon there.
|
||||||
|
|
||||||
**NEW FEATURE: CATEGORIES**
|
|
||||||
|
|
||||||
To add a category to your dashboard, change apps.json to resemble the following:
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"categories": [
|
|
||||||
...
|
|
||||||
],
|
|
||||||
"apps": [
|
|
||||||
...
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, a category can be added by entering the following within the "categories" field:
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"name": "[Name of the category]",
|
|
||||||
"items": [
|
|
||||||
[Application goes here]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
In the end, your apps.json file should look something like this:
|
|
||||||
|
|
||||||
1. Without categories
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"apps": [
|
|
||||||
{
|
|
||||||
"name": "[Name of the Application]",
|
|
||||||
"displayURL": "[URL you want to show]",
|
|
||||||
"URL": "[URL to redirect to]",
|
|
||||||
"icon": "[Icon code]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "[Name of the Application]",
|
|
||||||
"displayURL": "[URL you want to show]",
|
|
||||||
"URL": "[URL to redirect to]",
|
|
||||||
"icon": "[Icon code]"
|
|
||||||
},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. With apps and categories
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"categories": [
|
|
||||||
{
|
|
||||||
"name": "[Name of the category]",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"name": "[Name of the Application]",
|
|
||||||
"displayURL": "[URL you want to show]",
|
|
||||||
"URL": "[URL to redirect to]",
|
|
||||||
"icon": "[Icon code]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "[Name of the Application]",
|
|
||||||
"displayURL": "[URL you want to show]",
|
|
||||||
"URL": "[URL to redirect to]",
|
|
||||||
"icon": "[Icon code]"
|
|
||||||
},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
},
|
|
||||||
...
|
|
||||||
],
|
|
||||||
"apps": [
|
|
||||||
{
|
|
||||||
"name": "[Name of the Application]",
|
|
||||||
"displayURL": "[URL you want to show]",
|
|
||||||
"URL": "[URL to redirect to]",
|
|
||||||
"icon": "[Icon code]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "[Name of the Application]",
|
|
||||||
"displayURL": "[URL you want to show]",
|
|
||||||
"URL": "[URL to redirect to]",
|
|
||||||
"icon": "[Icon code]"
|
|
||||||
},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Bookmarks
|
### Bookmarks
|
||||||
|
|
||||||
To add a bookmark, append the following to bookmarks.json or simply edit one of the examples given.
|
To show bookmarks, `bookmarks.json` needs to resemble the following:
|
||||||
|
|
||||||
```
|
```json
|
||||||
{
|
{
|
||||||
"name": "[Category name]",
|
"groups": [
|
||||||
"items": [
|
{
|
||||||
{
|
"name": "[Group Name]",
|
||||||
"name": "[Bookmark name]",
|
"items": [
|
||||||
"url": "[URL to redirect to]"
|
{
|
||||||
},
|
"name": "[Bookmark Name]",
|
||||||
{
|
"url": "[Bookmark URL]"
|
||||||
"name": "[Bookmark name]",
|
},
|
||||||
"url": "[URL to redirect to]"
|
...
|
||||||
},
|
]
|
||||||
{
|
},
|
||||||
"name": "[Bookmark name]",
|
...
|
||||||
"url": "[URL to redirect to]"
|
]
|
||||||
}
|
|
||||||
...
|
|
||||||
]
|
|
||||||
},
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
### Theming:
|
|
||||||
|
|
||||||
Dashboard also supports themes with the help of a simple JSON-file: themes.json. To add a theme, append the following to themes.json:
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"label": "[Theme Name]",
|
|
||||||
"value": [Number of the theme],
|
|
||||||
"mainColor": "[Main Color as 6-character hex code]",
|
|
||||||
"accentColor": "[Accent Color as 6-character hex code]",
|
|
||||||
"backgroundColor": "[Background Color as 6-character hex code]"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Search Providers:
|
### Themes
|
||||||
|
|
||||||
The searchbar on the top supports shortcuts like "/so", just as SUI does. To add one of your own, simply append the following to search.json
|
In order to customize theming, `themes.json` needs to resemble this:
|
||||||
|
|
||||||
```
|
```json
|
||||||
{
|
{
|
||||||
"name":"[Name of the website]",
|
"themes": [
|
||||||
"url":"[Link that processes searches on that website]",
|
{
|
||||||
"prefix":"[a custom prefix]"
|
"label": "[Theme Name]",
|
||||||
},
|
"value": "[Number of the theme]",
|
||||||
|
"mainColor": "[Main Color as 6-character hex code]",
|
||||||
|
"accentColor": "[Accent Color as 6-character hex code]",
|
||||||
|
"backgroundColor": "[Background Color as 6-character hex code]"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Search Providers
|
||||||
|
|
||||||
|
For search providers to work, make sure your `search.json` resembles the following:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"providers": [
|
||||||
|
{
|
||||||
|
"name": "[Name of the website]",
|
||||||
|
"url": "[Link that processes searches on that website]",
|
||||||
|
"prefix": "[A custom prefix (e.g. '/test')]"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Imprint
|
||||||
|
|
||||||
|
In order for the imprint-modal to show up, make sure your `imprint.json` resembles the following:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"imprint": {
|
||||||
|
"name": {
|
||||||
|
"text": "[Name]",
|
||||||
|
"link": "[Link to the name (to e.g. a portfolio)]"
|
||||||
|
},
|
||||||
|
"address": {
|
||||||
|
"text": "[Address]",
|
||||||
|
"link": "[Link for the address (to e.g. Google Maps)]"
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"text": "[Phone number]",
|
||||||
|
"link": "[Link for the phone number]"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"text": "[Email address]",
|
||||||
|
"link": "[Link for the email address (e.g. for 'mailto')]"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"text": "[URL]",
|
||||||
|
"link": "[Link for the URL]"
|
||||||
|
},
|
||||||
|
"text": "[Text for the imprint]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> :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.
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
"url": {
|
"url": {
|
||||||
"text": "example.com",
|
"text": "example.com",
|
||||||
"link": "#"
|
"link": "#"
|
||||||
}
|
},
|
||||||
|
"text": "This is the place where you should put whatever you want it to say on the imprint page."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
22
src/app.tsx
22
src/app.tsx
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect } from "react";
|
import React from "react";
|
||||||
import { createGlobalStyle } from "styled-components";
|
import { createGlobalStyle } from "styled-components";
|
||||||
|
|
||||||
import SearchBar from "./components/searchBar";
|
import SearchBar from "./components/searchBar";
|
||||||
|
@ -8,14 +8,8 @@ import BookmarkList from "./components/bookmarkList";
|
||||||
import Settings from "./components/settings";
|
import Settings from "./components/settings";
|
||||||
import Imprint from "./components/imprint";
|
import Imprint from "./components/imprint";
|
||||||
|
|
||||||
import selectedTheme from "./components/themeManager";
|
import selectedTheme from "./lib/theme";
|
||||||
import {
|
import useFetcher from "./lib/fetcher";
|
||||||
useAppData,
|
|
||||||
useSearchProviderData,
|
|
||||||
useBookmarkData,
|
|
||||||
useThemeData,
|
|
||||||
useImprintData,
|
|
||||||
} from "./components/fetch";
|
|
||||||
|
|
||||||
const GlobalStyle = createGlobalStyle`
|
const GlobalStyle = createGlobalStyle`
|
||||||
body {
|
body {
|
||||||
|
@ -32,12 +26,12 @@ const GlobalStyle = createGlobalStyle`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the entire app by calling individual components
|
||||||
|
*/
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const { appData } = useAppData();
|
|
||||||
const { searchProviderData } = useSearchProviderData();
|
const { appData, bookmarkData, searchProviderData, themeData, imprintData } = useFetcher();
|
||||||
const { bookmarkData } = useBookmarkData();
|
|
||||||
const { themeData } = useThemeData();
|
|
||||||
const { imprintData } = useImprintData();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import Icon from "./icon";
|
import Icon from "./icon";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import selectedTheme from "./themeManager";
|
import selectedTheme from "../lib/theme";
|
||||||
|
|
||||||
const AppContainer = styled.div`
|
const AppContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -46,17 +46,21 @@ const AppDescription = styled.p`
|
||||||
export interface IAppProps {
|
export interface IAppProps {
|
||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
URL: string;
|
url: string;
|
||||||
displayURL: string;
|
displayURL: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const App = ({ name, icon, URL, displayURL }: IAppProps) => (
|
/**
|
||||||
|
* Renders a single app shortcut
|
||||||
|
* @param {IAppProps} props - The props of the given app
|
||||||
|
*/
|
||||||
|
export const App = ({ name, icon, url, displayURL }: IAppProps) => (
|
||||||
<AppContainer>
|
<AppContainer>
|
||||||
<IconContainer>
|
<IconContainer>
|
||||||
<Icon name={icon} />
|
<Icon name={icon} />
|
||||||
</IconContainer>
|
</IconContainer>
|
||||||
<DetailsContainer>
|
<DetailsContainer>
|
||||||
<AppLink href={URL}>{name}</AppLink>
|
<AppLink href={url}>{name}</AppLink>
|
||||||
<AppDescription>{displayURL}</AppDescription>
|
<AppDescription>{displayURL}</AppDescription>
|
||||||
</DetailsContainer>
|
</DetailsContainer>
|
||||||
</AppContainer>
|
</AppContainer>
|
||||||
|
|
|
@ -16,6 +16,10 @@ export interface IAppCategoryProps {
|
||||||
items: Array<IAppProps>;
|
items: Array<IAppProps>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders one app category
|
||||||
|
* @param {IAppCategoryProps} props - The props of the given category
|
||||||
|
*/
|
||||||
export const AppCategory = ({ name, items }: IAppCategoryProps) => (
|
export const AppCategory = ({ name, items }: IAppCategoryProps) => (
|
||||||
<CategoryContainer>
|
<CategoryContainer>
|
||||||
{name && <CategoryHeadline>{name}</CategoryHeadline>}
|
{name && <CategoryHeadline>{name}</CategoryHeadline>}
|
||||||
|
@ -25,7 +29,7 @@ export const AppCategory = ({ name, items }: IAppCategoryProps) => (
|
||||||
<App
|
<App
|
||||||
name={app.name}
|
name={app.name}
|
||||||
icon={app.icon}
|
icon={app.icon}
|
||||||
URL={app.URL}
|
url={app.url}
|
||||||
displayURL={app.displayURL}
|
displayURL={app.displayURL}
|
||||||
/>
|
/>
|
||||||
</Item>
|
</Item>
|
||||||
|
|
|
@ -9,22 +9,24 @@ export interface IAppListProps {
|
||||||
apps: Array<IAppProps>;
|
apps: Array<IAppProps>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppList = ({ categories, apps }: IAppListProps) => {
|
/**
|
||||||
return (
|
* Renders one list containing all app categories and uncategorized apps
|
||||||
<ListContainer>
|
* @param {IAppListProps} props - The props of the given list of apps
|
||||||
<Headline>Applications</Headline>
|
*/
|
||||||
{categories &&
|
const AppList = ({ categories, apps }: IAppListProps) => (
|
||||||
categories.map(({ name, items }, idx) => (
|
<ListContainer>
|
||||||
<AppCategory key={[name, idx].join("")} name={name} items={items} />
|
<Headline>Applications</Headline>
|
||||||
))}
|
{categories &&
|
||||||
{apps && (
|
categories.map(({ name, items }, idx) => (
|
||||||
<AppCategory
|
<AppCategory key={[name, idx].join("")} name={name} items={items} />
|
||||||
name={categories ? "Uncategorized apps" : ""}
|
))}
|
||||||
items={apps}
|
{apps && (
|
||||||
/>
|
<AppCategory
|
||||||
)}
|
name={categories ? "Uncategorized apps" : ""}
|
||||||
</ListContainer>
|
items={apps}
|
||||||
);
|
/>
|
||||||
};
|
)}
|
||||||
|
</ListContainer>
|
||||||
|
);
|
||||||
|
|
||||||
export default AppList;
|
export default AppList;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { Item, SubHeadline } from "./elements";
|
import { Item, SubHeadline } from "./elements";
|
||||||
import selectedTheme from "./themeManager";
|
import selectedTheme from "../lib/theme";
|
||||||
|
|
||||||
const GroupContainer = styled.div`
|
const GroupContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -32,13 +32,14 @@ export interface IBookmarkGroupProps {
|
||||||
items: Array<IBookmarkProps>;
|
items: Array<IBookmarkProps>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BookmarkGroup = ({
|
/**
|
||||||
name: groupName,
|
* Renders a given bookmark group
|
||||||
items,
|
* @param {IBookmarkGroupProps} props - The given props of the bookmark group
|
||||||
}: IBookmarkGroupProps) => (
|
*/
|
||||||
|
export const BookmarkGroup = ({ name, items }: IBookmarkGroupProps) => (
|
||||||
<Item>
|
<Item>
|
||||||
<GroupContainer>
|
<GroupContainer>
|
||||||
<SubHeadline>{groupName}</SubHeadline>
|
<SubHeadline>{name}</SubHeadline>
|
||||||
{items.map(({ name, url }, idx) => (
|
{items.map(({ name, url }, idx) => (
|
||||||
<Bookmark key={[name, idx].join("")} href={url}>
|
<Bookmark key={[name, idx].join("")} href={url}>
|
||||||
{name}
|
{name}
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Headline, ListContainer, ItemList } from "./elements";
|
import { Headline, ListContainer, ItemList } from "./elements";
|
||||||
|
|
||||||
import { BookmarkGroup, IBookmarkGroupProps } from "./bookmarkGroup";
|
import { BookmarkGroup, IBookmarkGroupProps } from "./bookmarkGroup";
|
||||||
|
|
||||||
interface IBookmarkListProps {
|
interface IBookmarkListProps {
|
||||||
groups: Array<IBookmarkGroupProps>;
|
groups: Array<IBookmarkGroupProps>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BookmarkList = ({ groups }: IBookmarkListProps) => {
|
/**
|
||||||
return (
|
* Renders a given list of categorized bookmarks
|
||||||
<ListContainer>
|
* @param {IBookmarkListProps} props - The props of the given bookmark list
|
||||||
<Headline>Bookmarks</Headline>
|
*/
|
||||||
<ItemList>
|
const BookmarkList = ({ groups }: IBookmarkListProps) => (
|
||||||
{groups.map(({ name, items }, idx) => (
|
<ListContainer>
|
||||||
<BookmarkGroup key={[name, idx].join("")} name={name} items={items} />
|
<Headline>Bookmarks</Headline>
|
||||||
))}
|
<ItemList>
|
||||||
</ItemList>
|
{groups.map(({ name, items }, idx) => (
|
||||||
</ListContainer>
|
<BookmarkGroup key={[name, idx].join("")} name={name} items={items} />
|
||||||
);
|
))}
|
||||||
};
|
</ItemList>
|
||||||
|
</ListContainer>
|
||||||
|
);
|
||||||
|
|
||||||
export default BookmarkList;
|
export default BookmarkList;
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import selectedTheme from "./themeManager";
|
import selectedTheme from "../lib/theme";
|
||||||
import Icon from "./icon";
|
import Icon from "./icon";
|
||||||
|
|
||||||
// File for elements that are/can be reused across the entire site.
|
|
||||||
|
|
||||||
export const ListContainer = styled.div`
|
export const ListContainer = styled.div`
|
||||||
padding: 2rem 0;
|
padding: 2rem 0;
|
||||||
`;
|
`;
|
||||||
|
@ -50,7 +48,8 @@ export const Button = styled.button`
|
||||||
border: 1px solid ${selectedTheme.mainColor};
|
border: 1px solid ${selectedTheme.mainColor};
|
||||||
color: ${selectedTheme.mainColor};
|
color: ${selectedTheme.mainColor};
|
||||||
background: none;
|
background: none;
|
||||||
min-height: 3em;
|
|
||||||
|
min-height: 2rem;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -61,6 +60,7 @@ export const Button = styled.button`
|
||||||
const StyledButton = styled.button`
|
const StyledButton = styled.button`
|
||||||
float: right;
|
float: right;
|
||||||
border: none;
|
border: none;
|
||||||
|
padding: 0;
|
||||||
background: none;
|
background: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -68,21 +68,15 @@ const StyledButton = styled.button`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const RefreshButton = styled(Button)`
|
|
||||||
display: relative;
|
|
||||||
top: 0;
|
|
||||||
float: right;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const ErrorMessage = styled.p`
|
|
||||||
color: red;
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface IIconButtonProps {
|
interface IIconButtonProps {
|
||||||
icon: string;
|
icon: string;
|
||||||
onClick: any;
|
onClick: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a button with an icon
|
||||||
|
* @param {IIconProps} props - The props of the given IconButton
|
||||||
|
*/
|
||||||
export const IconButton = ({ icon, onClick }: IIconButtonProps) => (
|
export const IconButton = ({ icon, onClick }: IIconButtonProps) => (
|
||||||
<StyledButton onClick={onClick}>
|
<StyledButton onClick={onClick}>
|
||||||
<Icon name={icon} />
|
<Icon name={icon} />
|
||||||
|
|
|
@ -1,193 +0,0 @@
|
||||||
import { useCallback, useEffect, useState } from "react";
|
|
||||||
import { ISearchProviderProps } from "./searchBar";
|
|
||||||
import { IBookmarkGroupProps } from "./bookmarkGroup";
|
|
||||||
import { IAppCategoryProps } from "./appCategory";
|
|
||||||
import { IAppProps } from "./app";
|
|
||||||
import { IThemeProps } from "./themeManager";
|
|
||||||
import { IImprintProps } from "./imprint";
|
|
||||||
|
|
||||||
const errorMessage = "Failed to load data.";
|
|
||||||
|
|
||||||
const handleResponse = (response: any) => {
|
|
||||||
if (response.ok) return response.json();
|
|
||||||
throw new Error(errorMessage);
|
|
||||||
};
|
|
||||||
|
|
||||||
// SECTION: Search Provider
|
|
||||||
|
|
||||||
export interface ISearchProviderDataProps {
|
|
||||||
providers: Array<ISearchProviderProps>;
|
|
||||||
error: string | boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useSearchProviderData = () => {
|
|
||||||
const [
|
|
||||||
searchProviderData,
|
|
||||||
setSearchProviderData,
|
|
||||||
] = useState<ISearchProviderDataProps>({ providers: [], error: false });
|
|
||||||
|
|
||||||
const fetchSearchProviderData = useCallback(() => {
|
|
||||||
(process.env.NODE_ENV === "production"
|
|
||||||
? fetch("/data/search.json").then(handleResponse)
|
|
||||||
: import("./data/search.json")
|
|
||||||
)
|
|
||||||
.then((jsonResponse) => {
|
|
||||||
setSearchProviderData({ ...jsonResponse, error: false });
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
setSearchProviderData({ providers: [], error: error.message });
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchSearchProviderData();
|
|
||||||
}, [fetchSearchProviderData]);
|
|
||||||
|
|
||||||
return { searchProviderData, fetchSearchProviderData };
|
|
||||||
};
|
|
||||||
|
|
||||||
// SECTION: Bookmark data
|
|
||||||
|
|
||||||
export interface IBookmarkDataProps {
|
|
||||||
groups: Array<IBookmarkGroupProps>;
|
|
||||||
error: string | boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useBookmarkData = () => {
|
|
||||||
const [bookmarkData, setBookmarkData] = useState<IBookmarkDataProps>({
|
|
||||||
groups: [],
|
|
||||||
error: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchBookmarkData = useCallback(() => {
|
|
||||||
(process.env.NODE_ENV === "production"
|
|
||||||
? fetch("/data/bookmarks.json").then(handleResponse)
|
|
||||||
: import("./data/bookmarks.json")
|
|
||||||
)
|
|
||||||
.then((jsonResponse) => {
|
|
||||||
setBookmarkData({ ...jsonResponse, error: false });
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
setBookmarkData({ groups: [], error: error.message });
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchBookmarkData();
|
|
||||||
}, [fetchBookmarkData]);
|
|
||||||
|
|
||||||
return { bookmarkData, fetchBookmarkData };
|
|
||||||
};
|
|
||||||
|
|
||||||
// SECTION: App data
|
|
||||||
|
|
||||||
export interface IAppDataProps {
|
|
||||||
categories: Array<IAppCategoryProps>;
|
|
||||||
apps: Array<IAppProps>;
|
|
||||||
error: string | boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useAppData = () => {
|
|
||||||
const [appData, setAppData] = useState({
|
|
||||||
categories: [],
|
|
||||||
apps: [],
|
|
||||||
error: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchAppData = useCallback(() => {
|
|
||||||
(process.env.NODE_ENV === "production"
|
|
||||||
? fetch("/data/apps.json").then(handleResponse)
|
|
||||||
: import("./data/apps.json")
|
|
||||||
)
|
|
||||||
.then((jsonResponse) => {
|
|
||||||
setAppData({ ...jsonResponse, error: false });
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
setAppData({ categories: [], apps: [], error: error.message });
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchAppData();
|
|
||||||
}, [fetchAppData]);
|
|
||||||
return { appData, fetchAppData };
|
|
||||||
};
|
|
||||||
|
|
||||||
// Section: Theme Data
|
|
||||||
|
|
||||||
export interface IThemeDataProps {
|
|
||||||
themes: Array<IThemeProps>;
|
|
||||||
error: string | boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useThemeData = () => {
|
|
||||||
const [themeData, setThemeData] = useState<IThemeDataProps>({
|
|
||||||
themes: [],
|
|
||||||
error: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchThemeData = useCallback(() => {
|
|
||||||
(process.env.NODE_ENV === "production"
|
|
||||||
? fetch("/data/themes.json").then(handleResponse)
|
|
||||||
: import("./data/themes.json")
|
|
||||||
)
|
|
||||||
.then((jsonResponse) => {
|
|
||||||
setThemeData({ ...jsonResponse, error: false });
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
setThemeData({ themes: [], error: error.message });
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchThemeData();
|
|
||||||
}, [fetchThemeData]);
|
|
||||||
return { themeData, fetchThemeData };
|
|
||||||
};
|
|
||||||
|
|
||||||
// SECTION: Imprint Data
|
|
||||||
|
|
||||||
export interface IImprintDataProps {
|
|
||||||
imprint: IImprintProps;
|
|
||||||
error: string | boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useImprintData = () => {
|
|
||||||
const [imprintData, setImprintData] = useState<IImprintDataProps>({
|
|
||||||
imprint: {
|
|
||||||
name: { text: "", link: "" },
|
|
||||||
address: { text: "", link: "" },
|
|
||||||
phone: { text: "", link: "" },
|
|
||||||
email: { text: "", link: "" },
|
|
||||||
url: { text: "", link: "" },
|
|
||||||
},
|
|
||||||
error: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchImprintData = useCallback(() => {
|
|
||||||
(process.env.NODE_ENV === "production"
|
|
||||||
? fetch("/data/imprint.json").then(handleResponse)
|
|
||||||
: import("./data/imprint.json")
|
|
||||||
)
|
|
||||||
.then((jsonResponse: any) => {
|
|
||||||
setImprintData({ ...jsonResponse, error: false });
|
|
||||||
})
|
|
||||||
.catch((error: any) => {
|
|
||||||
setImprintData({
|
|
||||||
imprint: {
|
|
||||||
name: { text: "", link: "" },
|
|
||||||
address: { text: "", link: "" },
|
|
||||||
phone: { text: "", link: "" },
|
|
||||||
email: { text: "", link: "" },
|
|
||||||
url: { text: "", link: "" },
|
|
||||||
},
|
|
||||||
error: error.message,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchImprintData();
|
|
||||||
}, [fetchImprintData]);
|
|
||||||
return { imprintData, fetchImprintData };
|
|
||||||
};
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
import selectedTheme from "./themeManager";
|
import selectedTheme from "../lib/theme";
|
||||||
|
|
||||||
const GreeterContainer = styled.div`
|
const GreeterContainer = styled.div`
|
||||||
padding: 2rem 0;
|
padding: 2rem 0;
|
||||||
|
@ -22,37 +22,6 @@ const DateText = styled.h3`
|
||||||
color: ${selectedTheme.accentColor};
|
color: ${selectedTheme.accentColor};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const getGreeting = () => {
|
|
||||||
switch (Math.floor(new Date().getHours() / 6)) {
|
|
||||||
case 0:
|
|
||||||
return "Good night!";
|
|
||||||
case 1:
|
|
||||||
return "Good morning!";
|
|
||||||
case 2:
|
|
||||||
return "Good afternoon!";
|
|
||||||
case 3:
|
|
||||||
return "Good evening!";
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getExtension = (day: number) => {
|
|
||||||
let extension = "";
|
|
||||||
|
|
||||||
if ((day > 4 && day <= 20) || (day > 20 && day % 10 >= 4)) {
|
|
||||||
extension = "th";
|
|
||||||
} else if (day % 10 === 1) {
|
|
||||||
extension = "st";
|
|
||||||
} else if (day % 10 === 2) {
|
|
||||||
extension = "nd";
|
|
||||||
} else if (day % 10 === 3) {
|
|
||||||
extension = "rd";
|
|
||||||
}
|
|
||||||
|
|
||||||
return extension;
|
|
||||||
};
|
|
||||||
|
|
||||||
const monthNames = [
|
const monthNames = [
|
||||||
"January",
|
"January",
|
||||||
"February",
|
"February",
|
||||||
|
@ -78,6 +47,50 @@ const weekDayNames = [
|
||||||
"Saturday",
|
"Saturday",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a greeting based on the current time
|
||||||
|
* @returns {string} - A greeting
|
||||||
|
*/
|
||||||
|
const getGreeting = () => {
|
||||||
|
switch (Math.floor(new Date().getHours() / 6)) {
|
||||||
|
case 0:
|
||||||
|
return "Good night!";
|
||||||
|
case 1:
|
||||||
|
return "Good morning!";
|
||||||
|
case 2:
|
||||||
|
return "Good afternoon!";
|
||||||
|
case 3:
|
||||||
|
return "Good evening!";
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* @returns {string} - The extension for that number
|
||||||
|
*/
|
||||||
|
const getExtension = (day: number) => {
|
||||||
|
let extension = "";
|
||||||
|
|
||||||
|
if ((day > 4 && day <= 20) || (day > 20 && day % 10 >= 4)) {
|
||||||
|
extension = "th";
|
||||||
|
} else if (day % 10 === 1) {
|
||||||
|
extension = "st";
|
||||||
|
} else if (day % 10 === 2) {
|
||||||
|
extension = "nd";
|
||||||
|
} else if (day % 10 === 3) {
|
||||||
|
extension = "rd";
|
||||||
|
}
|
||||||
|
|
||||||
|
return extension;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the current date
|
||||||
|
* @returns {string} - The current date as a string
|
||||||
|
*/
|
||||||
const getDateString = () => {
|
const getDateString = () => {
|
||||||
let currentDate = new Date();
|
let currentDate = new Date();
|
||||||
|
|
||||||
|
@ -93,16 +106,14 @@ const getDateString = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Greeter = () => {
|
/**
|
||||||
let date = getDateString();
|
* Renders the Greeter
|
||||||
let greeting = getGreeting();
|
*/
|
||||||
|
const Greeter = () => (
|
||||||
return (
|
<GreeterContainer>
|
||||||
<GreeterContainer>
|
<DateText>{getDateString()}</DateText>
|
||||||
<DateText>{date}</DateText>
|
<GreetText>{getGreeting()}</GreetText>
|
||||||
<GreetText>{greeting}</GreetText>
|
</GreeterContainer>
|
||||||
</GreeterContainer>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Greeter;
|
export default Greeter;
|
||||||
|
|
|
@ -1,36 +1,38 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import selectedTheme from "./themeManager";
|
import selectedTheme from "../lib/theme";
|
||||||
|
|
||||||
export const RawIcon = 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";
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface IIconProps {
|
interface IIconProps {
|
||||||
name: string;
|
name: string;
|
||||||
size?: string;
|
size?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ComponentIcon = ({ name, size }: IIconProps) => {
|
/**
|
||||||
let IconContainer = styled(RawIcon)`
|
* Renders an Icon
|
||||||
|
* @param {IIconProps} props - The props needed for the given icon
|
||||||
|
*/
|
||||||
|
export const Icon = ({ name, size }: IIconProps) => {
|
||||||
|
|
||||||
|
let IconContainer = styled.i`
|
||||||
|
font-family: "Material Icons";
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
font-size: ${size ? size : "24px"};
|
font-size: ${size ? size : "24px"};
|
||||||
color: ${selectedTheme.mainColor};
|
color: ${selectedTheme.mainColor};
|
||||||
|
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";
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return <IconContainer>{name}</IconContainer>;
|
return <IconContainer>{name}</IconContainer>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ComponentIcon;
|
export default Icon;
|
||||||
|
|
|
@ -1,24 +1,16 @@
|
||||||
import React from "react";
|
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 "./themeManager";
|
import selectedTheme from "../lib/theme";
|
||||||
import {
|
import {
|
||||||
ListContainer,
|
ListContainer,
|
||||||
ItemList,
|
ItemList,
|
||||||
Headline as Hl,
|
Headline,
|
||||||
SubHeadline as SHl,
|
SubHeadline,
|
||||||
} from "./elements";
|
} from "./elements";
|
||||||
|
|
||||||
const Headline = styled(Hl)`
|
|
||||||
display: block;
|
|
||||||
padding: 1rem 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SubHeadline = styled(SHl)`
|
|
||||||
display: block;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ModalSubHeadline = styled(SubHeadline)`
|
const ModalSubHeadline = styled(SubHeadline)`
|
||||||
|
display: block;
|
||||||
padding: 0.5rem 0;
|
padding: 0.5rem 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -56,30 +48,40 @@ export interface IImprintProps {
|
||||||
phone: IImprintFieldProps;
|
phone: IImprintFieldProps;
|
||||||
email: IImprintFieldProps;
|
email: IImprintFieldProps;
|
||||||
url: IImprintFieldProps;
|
url: IImprintFieldProps;
|
||||||
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IImprintFieldComponentProps {
|
interface IImprintFieldComponentProps {
|
||||||
field: IImprintFieldProps;
|
field: IImprintFieldProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ImprintField = ({ field }: IImprintFieldComponentProps) => (
|
|
||||||
<Link href={field.link}>{field.text}</Link>
|
|
||||||
);
|
|
||||||
|
|
||||||
interface IImprintComponentProps {
|
interface IImprintComponentProps {
|
||||||
imprint: IImprintProps;
|
imprint: IImprintProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders an imprint field
|
||||||
|
* @param {IImprintFieldComponentProps} props - The data for the field
|
||||||
|
*/
|
||||||
|
const ImprintField = ({ field }: IImprintFieldComponentProps) => (
|
||||||
|
<Link href={field.link}>{field.text}</Link>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the imprint component
|
||||||
|
* @param {IImprintProps} props - The contents of the imprint
|
||||||
|
*/
|
||||||
const Imprint = ({ imprint }: IImprintComponentProps) => (
|
const Imprint = ({ imprint }: IImprintComponentProps) => (
|
||||||
<>
|
<>
|
||||||
<ListContainer>
|
<ListContainer>
|
||||||
<Hl>About</Hl>
|
<Headline>About</Headline>
|
||||||
<ItemList>
|
<ItemList>
|
||||||
<ItemContainer>
|
<ItemContainer>
|
||||||
<SHl>Imprint</SHl>
|
<SubHeadline>Imprint</SubHeadline>
|
||||||
<Modal
|
<Modal
|
||||||
element="text"
|
element="text"
|
||||||
text="View Imprint"
|
text="View Imprint"
|
||||||
|
title="Legal Disclosure"
|
||||||
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")) {
|
||||||
|
@ -88,49 +90,24 @@ const Imprint = ({ imprint }: IImprintComponentProps) => (
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Headline>Legal Disclosure</Headline>
|
<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} />}
|
||||||
{imprint.email && <ImprintField field={imprint.email} />}
|
{imprint.email && <ImprintField field={imprint.email} />}
|
||||||
{imprint.phone && <ImprintField field={imprint.phone} />}
|
{imprint.phone && <ImprintField field={imprint.phone} />}
|
||||||
{imprint.url && <ImprintField field={imprint.url} />}
|
{imprint.url && <ImprintField field={imprint.url} />}
|
||||||
</>
|
</>
|
||||||
<Headline>Disclaimer</Headline>
|
</div>
|
||||||
<ModalSubHeadline>Accountability for content</ModalSubHeadline>
|
<div>
|
||||||
<Text>
|
<ModalSubHeadline>
|
||||||
The contents of our pages have been created with the utmost care.
|
Imprint
|
||||||
However, we cannot guarantee the contents' accuracy, completeness
|
</ModalSubHeadline>
|
||||||
or topicality. According to statutory provisions, we are
|
{imprint.text && <Text>{imprint.text}</Text>}
|
||||||
furthermore responsible for our own content on these web pages. In
|
</div>
|
||||||
this matter, please note that we are not obliged to monitor the
|
|
||||||
transmitted or saved information of third parties, or investigate
|
|
||||||
circumstances pointing to illegal activity. Our obligations to
|
|
||||||
remove or block the use of information under generally applicable
|
|
||||||
laws remain unaffected by this as per §§ 8 to 10 of the Telemedia
|
|
||||||
Act (TMG).
|
|
||||||
</Text>
|
|
||||||
<ModalSubHeadline>Accountability for links</ModalSubHeadline>
|
|
||||||
<Text>
|
|
||||||
Responsibility for the content of external links (to web pages of
|
|
||||||
third parties) lies solely with the operators of the linked pages.
|
|
||||||
No violations were evident to us at the time of linking. Should
|
|
||||||
any legal infringement become known to us, we will remove the
|
|
||||||
respective link immediately.
|
|
||||||
</Text>
|
|
||||||
<ModalSubHeadline>Copyright</ModalSubHeadline>
|
|
||||||
<Text>
|
|
||||||
Our web pages and their contents are subject to German copyright
|
|
||||||
law. Unless expressly permitted by law, every form of utilizing,
|
|
||||||
reproducing or processing works subject to copyright protection on
|
|
||||||
our web pages requires the prior consent of the respective owner
|
|
||||||
of the rights. Individual reproductions of a work are only allowed
|
|
||||||
for private use. The materials from these pages are copyrighted
|
|
||||||
and any unauthorized use may violate copyright laws.
|
|
||||||
</Text>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
</ItemContainer>
|
</ItemContainer>
|
||||||
</ItemList>
|
</ItemList>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import selectedTheme from "./themeManager";
|
import selectedTheme from "../lib/theme";
|
||||||
|
|
||||||
import { IconButton } from "./elements";
|
import { Headline, IconButton } from "./elements";
|
||||||
|
|
||||||
const ModalContainer = styled.div`
|
const ModalContainer = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -30,36 +30,51 @@ const Text = styled.p`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface IModalInterface {
|
const TitleContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface IModalProps {
|
||||||
element: string;
|
element: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
condition?: boolean;
|
condition?: boolean;
|
||||||
|
title: string;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Modal = (props: IModalInterface) => {
|
/**
|
||||||
const [modalHidden, setModalHidden] = useState(props.condition ?? true);
|
* Renders a modal with button to hide and un-hide
|
||||||
|
* @param {IModalProps} props - The needed props for the modal
|
||||||
|
*/
|
||||||
|
const Modal = ({ element, icon, text, condition, title, onClose, children }: IModalProps) => {
|
||||||
|
const [modalHidden, setModalHidden] = useState(condition ?? true);
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
if (props.onClose) props.onClose();
|
if (onClose) onClose();
|
||||||
setModalHidden(!modalHidden);
|
setModalHidden(!modalHidden);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{props.element === "icon" && (
|
{element === "icon" && (
|
||||||
<IconButton icon={props.icon ?? ""} onClick={() => closeModal()} />
|
<IconButton icon={icon ?? ""} onClick={() => closeModal()} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{props.element === "text" && (
|
{element === "text" && (
|
||||||
<Text onClick={() => closeModal()}>{props.text}</Text>
|
<Text onClick={() => closeModal()}>{text}</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ModalContainer hidden={modalHidden}>
|
<ModalContainer hidden={modalHidden}>
|
||||||
<IconButton icon="close" onClick={() => closeModal()} />
|
<TitleContainer>
|
||||||
{props.children}
|
<Headline>{title}</Headline>
|
||||||
|
<IconButton icon="close" onClick={() => closeModal()} />
|
||||||
|
</TitleContainer>
|
||||||
|
{children}
|
||||||
</ModalContainer>
|
</ModalContainer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,22 +1,43 @@
|
||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
import selectedTheme from "./themeManager";
|
import selectedTheme from "../lib/theme";
|
||||||
|
|
||||||
|
import { Button } from "./elements";
|
||||||
|
|
||||||
|
const Search = styled.form`
|
||||||
|
width: 100%;
|
||||||
|
height: 2rem;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
padding-top: 0.25rem;
|
||||||
|
`;
|
||||||
|
|
||||||
const SearchInput = styled.input`
|
const SearchInput = styled.input`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom: 1px solid ${selectedTheme.accentColor};
|
border-bottom: 1px solid ${selectedTheme.accentColor};
|
||||||
|
|
||||||
background: none;
|
background: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
color: ${selectedTheme.mainColor};
|
color: ${selectedTheme.mainColor};
|
||||||
|
|
||||||
|
margin: 0px;
|
||||||
|
|
||||||
:focus {
|
:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const SearchButton = styled(Button)`
|
||||||
|
margin: 0px 2px;
|
||||||
|
min-height: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
export interface ISearchProviderProps {
|
export interface ISearchProviderProps {
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
@ -27,11 +48,20 @@ interface ISearchBarProps {
|
||||||
providers: Array<ISearchProviderProps> | undefined;
|
providers: Array<ISearchProviderProps> | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a search bar
|
||||||
|
* @param {ISearchBarProps} props - The search providers for the search bar to use
|
||||||
|
*/
|
||||||
const SearchBar = ({ providers }: ISearchBarProps) => {
|
const SearchBar = ({ providers }: ISearchBarProps) => {
|
||||||
let [input, setInput] = useState("");
|
let [input, setInput] = useState<string>("");
|
||||||
|
let [buttonsHidden, setButtonsHidden] = useState<boolean>(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setButtonsHidden(input === "");
|
||||||
|
}, [input]);
|
||||||
|
|
||||||
const handleSearchQuery = (e: React.FormEvent) => {
|
const handleSearchQuery = (e: React.FormEvent) => {
|
||||||
var query = input || "";
|
var query: string = input || "";
|
||||||
|
|
||||||
if (query.split(" ")[0].includes("/")) {
|
if (query.split(" ")[0].includes("/")) {
|
||||||
handleQueryWithProvider(query);
|
handleQueryWithProvider(query);
|
||||||
|
@ -43,13 +73,14 @@ const SearchBar = ({ providers }: ISearchBarProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleQueryWithProvider = (query: string) => {
|
const handleQueryWithProvider = (query: string) => {
|
||||||
let queryArray = query.split(" ");
|
let queryArray: Array<string> = query.split(" ");
|
||||||
let prefix = queryArray[0];
|
let prefix: string = queryArray[0];
|
||||||
|
|
||||||
queryArray.shift();
|
queryArray.shift();
|
||||||
|
|
||||||
let searchQuery = queryArray.join(" ");
|
let searchQuery: string = queryArray.join(" ");
|
||||||
|
|
||||||
let providerFound = false;
|
let providerFound: boolean = false;
|
||||||
if (providers) {
|
if (providers) {
|
||||||
providers.forEach((provider: ISearchProviderProps) => {
|
providers.forEach((provider: ISearchProviderProps) => {
|
||||||
if (provider.prefix === prefix) {
|
if (provider.prefix === prefix) {
|
||||||
|
@ -64,13 +95,25 @@ const SearchBar = ({ providers }: ISearchBarProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={(e) => handleSearchQuery(e)}>
|
<Search onSubmit={(e) => handleSearchQuery(e)}>
|
||||||
<SearchInput
|
<SearchInput
|
||||||
type="text"
|
type="text"
|
||||||
onChange={(e) => setInput(e.target.value)}
|
value={input}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setInput(e.target.value)
|
||||||
|
}
|
||||||
></SearchInput>
|
></SearchInput>
|
||||||
<button type="submit" hidden />
|
<SearchButton
|
||||||
</form>
|
type="button"
|
||||||
|
onClick={() => setInput("")}
|
||||||
|
hidden={buttonsHidden}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</SearchButton>
|
||||||
|
<SearchButton type="submit" hidden={buttonsHidden}>
|
||||||
|
Search
|
||||||
|
</SearchButton>
|
||||||
|
</Search>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,24 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
import Select from "react-select";
|
import Select, { Styles } from "react-select";
|
||||||
|
|
||||||
import { ISearchProviderProps } from "./searchBar";
|
import { ISearchProviderProps } from "./searchBar";
|
||||||
import selectedTheme, { setTheme, IThemeProps } from "./themeManager";
|
import selectedTheme, { setTheme, IThemeProps } from "../lib/theme";
|
||||||
import { Button, Headline as hl } from "./elements";
|
import { Button, SubHeadline } from "./elements";
|
||||||
|
|
||||||
import Modal from "./modal";
|
import Modal from "./modal";
|
||||||
|
|
||||||
const Headline = styled(hl)`
|
interface IHoverProps {
|
||||||
padding: 0.5rem 0;
|
color?: string;
|
||||||
`;
|
backgroundColor?: string;
|
||||||
|
border?: string;
|
||||||
|
borderColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const SelectContainer = styled.div`
|
interface IPseudoProps extends React.CSSProperties {
|
||||||
padding-bottom: 1rem;
|
"&:hover": IHoverProps
|
||||||
`;
|
}
|
||||||
|
|
||||||
const FormContainer = styled.div`
|
const FormContainer = styled.div`
|
||||||
display: grid;
|
display: grid;
|
||||||
|
@ -42,47 +45,72 @@ const HeadCell = styled.th`
|
||||||
background: none;
|
background: none;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SelectorStyle = {
|
const Section = styled.div`
|
||||||
control: (provided: any) => ({
|
padding: 1rem 0;
|
||||||
...provided,
|
`;
|
||||||
fontWeight: "500",
|
|
||||||
|
const SectionHeadline = styled(SubHeadline)`
|
||||||
|
width: 100%;
|
||||||
|
border-bottom: 1px solid ${selectedTheme.accentColor};
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SelectorStyle: Partial<Styles<IThemeProps, false>> = {
|
||||||
|
indicatorSeparator: () => ({
|
||||||
|
display: "none",
|
||||||
|
}),
|
||||||
|
container: (base: React.CSSProperties): React.CSSProperties => ({
|
||||||
|
...base,
|
||||||
|
margin: "0 2px",
|
||||||
|
}),
|
||||||
|
dropdownIndicator: (base: React.CSSProperties): IPseudoProps => ({
|
||||||
|
...base,
|
||||||
|
color: selectedTheme.mainColor,
|
||||||
|
"&:hover": {
|
||||||
|
color: selectedTheme.mainColor
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
control: (base: React.CSSProperties): IPseudoProps => ({
|
||||||
|
...base,
|
||||||
|
fontWeight: 500,
|
||||||
color: selectedTheme.mainColor,
|
color: selectedTheme.mainColor,
|
||||||
textTransform: "uppercase",
|
textTransform: "uppercase",
|
||||||
width: "12rem",
|
width: "12rem",
|
||||||
background: "none",
|
background: "none",
|
||||||
borderRadius: "0px",
|
borderRadius: 0,
|
||||||
border: "1px solid " + selectedTheme.mainColor,
|
border: "1px solid",
|
||||||
boxShadow: 0,
|
borderColor: selectedTheme.mainColor,
|
||||||
|
boxShadow: "none",
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
border: "1px solid " + selectedTheme.mainColor,
|
border: "1px solid",
|
||||||
|
borderColor: selectedTheme.mainColor
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
menu: (provided: any) => ({
|
menu: (base: React.CSSProperties): React.CSSProperties => ({
|
||||||
...provided,
|
...base,
|
||||||
backgroundColor: selectedTheme.backgroundColor,
|
backgroundColor: selectedTheme.backgroundColor,
|
||||||
border: "1px solid " + selectedTheme.mainColor,
|
border: "1px solid " + selectedTheme.mainColor,
|
||||||
borderRadius: 0,
|
borderRadius: 0,
|
||||||
boxShadow: 0,
|
boxShadow: "none",
|
||||||
|
margin: "4px 0"
|
||||||
}),
|
}),
|
||||||
option: (provided: any) => ({
|
option: (base: React.CSSProperties): IPseudoProps => ({
|
||||||
...provided,
|
...base,
|
||||||
fontWeight: "500",
|
fontWeight: 500,
|
||||||
color: selectedTheme.mainColor,
|
color: selectedTheme.mainColor,
|
||||||
textTransform: "uppercase",
|
textTransform: "uppercase",
|
||||||
borderRadius: 0,
|
borderRadius: 0,
|
||||||
boxShadow: 0,
|
boxShadow: "none",
|
||||||
backgroundColor: selectedTheme.backgroundColor,
|
backgroundColor: selectedTheme.backgroundColor,
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
backgroundColor: selectedTheme.mainColor,
|
backgroundColor: selectedTheme.mainColor,
|
||||||
color: selectedTheme.backgroundColor,
|
color: selectedTheme.backgroundColor,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
singleValue: (provided: any) => {
|
singleValue: (base: React.CSSProperties): React.CSSProperties => ({
|
||||||
return {
|
...base,
|
||||||
...provided,
|
color: selectedTheme.mainColor,
|
||||||
color: selectedTheme.mainColor,
|
}),
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ISettingsProps {
|
interface ISettingsProps {
|
||||||
|
@ -90,15 +118,21 @@ interface ISettingsProps {
|
||||||
providers: Array<ISearchProviderProps> | undefined;
|
providers: Array<ISearchProviderProps> | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the settings-modal
|
||||||
|
* @param {Array<IThemeProps>} themes - the list of themes a user can select between
|
||||||
|
* @param {Array<ISearchProviderProps>} providers - the list of search providers
|
||||||
|
*/
|
||||||
const Settings = ({ themes, providers }: ISettingsProps) => {
|
const Settings = ({ themes, providers }: ISettingsProps) => {
|
||||||
const [newTheme, setNewTheme] = useState();
|
const [newTheme, setNewTheme] = useState();
|
||||||
|
|
||||||
if (themes && providers) {
|
if (themes && providers) {
|
||||||
return (
|
return (
|
||||||
<Modal element="icon" icon="settings">
|
<Modal element="icon" icon="settings" title="Settings">
|
||||||
{themes && (
|
{themes && (
|
||||||
<SelectContainer>
|
<Section>
|
||||||
<Headline>Theme:</Headline>
|
<SectionHeadline>Theme:</SectionHeadline>
|
||||||
<FormContainer>
|
<FormContainer>
|
||||||
<Select
|
<Select
|
||||||
options={themes}
|
options={themes}
|
||||||
|
@ -108,29 +142,31 @@ const Settings = ({ themes, providers }: ISettingsProps) => {
|
||||||
}}
|
}}
|
||||||
styles={SelectorStyle}
|
styles={SelectorStyle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button onClick={() => setTheme(JSON.stringify(newTheme))}>
|
<Button onClick={() => setTheme(JSON.stringify(newTheme))}>
|
||||||
Apply
|
Apply
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => window.location.reload()}>Refresh</Button>
|
<Button onClick={() => window.location.reload()}>Refresh</Button>
|
||||||
</FormContainer>
|
</FormContainer>
|
||||||
</SelectContainer>
|
</Section>
|
||||||
)}
|
)}
|
||||||
{providers && (
|
{providers && (
|
||||||
<Table>
|
<Section>
|
||||||
<tbody>
|
<SectionHeadline>Search Providers</SectionHeadline>
|
||||||
<TableRow>
|
<Table>
|
||||||
<HeadCell>Search Provider</HeadCell>
|
<tbody>
|
||||||
<HeadCell>Prefix</HeadCell>
|
<TableRow>
|
||||||
</TableRow>
|
<HeadCell>Search Provider</HeadCell>
|
||||||
{providers.map((provider, index) => (
|
<HeadCell>Prefix</HeadCell>
|
||||||
<TableRow key={provider.name + index}>
|
|
||||||
<TableCell>{provider.name}</TableCell>
|
|
||||||
<TableCell>{provider.prefix}</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
{providers.map((provider, index) => (
|
||||||
</tbody>
|
<TableRow key={provider.name + index}>
|
||||||
</Table>
|
<TableCell>{provider.name}</TableCell>
|
||||||
|
<TableCell>{provider.prefix}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</Section>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
"url": {
|
"url": {
|
||||||
"text": "example.com",
|
"text": "example.com",
|
||||||
"link": "#"
|
"link": "#"
|
||||||
}
|
},
|
||||||
|
"text": "This is the place where you should put whatever you want it to say on the imprint page."
|
||||||
}
|
}
|
||||||
}
|
}
|
173
src/lib/fetcher.tsx
Normal file
173
src/lib/fetcher.tsx
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { ISearchProviderProps } 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";
|
||||||
|
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
const handleResponse = (response: Response) => {
|
||||||
|
if (response.ok) return response.json();
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ISearchProviderDataProps {
|
||||||
|
providers: Array<ISearchProviderProps>;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default values for the respective state variables
|
||||||
|
*/
|
||||||
|
const defaults = {
|
||||||
|
app: {
|
||||||
|
categories: [],
|
||||||
|
apps: [],
|
||||||
|
error: false,
|
||||||
|
},
|
||||||
|
bookmark: {
|
||||||
|
groups: [],
|
||||||
|
error: false,
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
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 }
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all of the data by doing fetch requests (only available in production)
|
||||||
|
*/
|
||||||
|
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)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all of the data by importing it (only available in development)
|
||||||
|
*/
|
||||||
|
const fetchDevelopment = Promise.all([
|
||||||
|
import("../data/apps.json"),
|
||||||
|
import("../data/bookmarks.json"),
|
||||||
|
import("../data/search.json"),
|
||||||
|
import("../data/themes.json"),
|
||||||
|
import("../data/imprint.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<ISearchProviderDataProps>(defaults.search);
|
||||||
|
|
||||||
|
const [themeData, setThemeData] = useState<IThemeDataProps>(defaults.theme);
|
||||||
|
|
||||||
|
const [imprintData, setImprintData] = useState<IImprintDataProps>(
|
||||||
|
defaults.imprint
|
||||||
|
);
|
||||||
|
|
||||||
|
const callback = useCallback(() => {
|
||||||
|
(inProduction ? fetchProduction : fetchDevelopment).then(
|
||||||
|
([appData, bookmarkData, searchData, themeData, imprintData]: any) => {
|
||||||
|
(appData.error) ? setAppData(appData) : setAppData({ ...appData, error: false });
|
||||||
|
(bookmarkData.error) ? setBookmarkData(bookmarkData) : setBookmarkData({ ...bookmarkData, error: false });
|
||||||
|
(searchData.error) ? setSearchProviderData(searchData) : setSearchProviderData({ ...searchData, error: false });
|
||||||
|
(themeData.error) ? setThemeData(themeData) : setThemeData({ ...themeData, error: false });
|
||||||
|
(imprintData.error) ? setImprintData(imprintData) : setImprintData({ ...imprintData, error: false });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => callback(), [callback]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
appData,
|
||||||
|
bookmarkData,
|
||||||
|
searchProviderData,
|
||||||
|
themeData,
|
||||||
|
imprintData,
|
||||||
|
callback
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useFetcher;
|
|
@ -14,22 +14,23 @@ const defaultTheme: IThemeProps = {
|
||||||
backgroundColor: "#ffffff",
|
backgroundColor: "#ffffff",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a given theme into localStorage
|
||||||
|
* @param {string} theme - the theme that shall be saved (in stringified JSON)
|
||||||
|
*/
|
||||||
export const setTheme = (theme: string) => {
|
export const setTheme = (theme: string) => {
|
||||||
if (theme !== undefined) {
|
if (theme !== undefined) localStorage.setItem("theme", theme);
|
||||||
localStorage.setItem("theme", theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resetTheme = () => {
|
/**
|
||||||
localStorage.removeItem("theme");
|
* Function that gets the saved theme from localStorage or returns the default
|
||||||
};
|
* @returns {IThemeProps} the saved theme or the default theme
|
||||||
|
*/
|
||||||
const getTheme = () => {
|
const getTheme = (): IThemeProps => {
|
||||||
let selectedTheme = defaultTheme;
|
let selectedTheme = defaultTheme;
|
||||||
|
|
||||||
if (localStorage.getItem("theme") != null) {
|
if (localStorage.getItem("theme") !== null) {
|
||||||
selectedTheme = JSON.parse(localStorage.getItem("theme") || "{}");
|
selectedTheme = JSON.parse(localStorage.getItem("theme") || "{}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,5 +38,4 @@ const getTheme = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedTheme = getTheme();
|
const selectedTheme = getTheme();
|
||||||
|
|
||||||
export default selectedTheme;
|
export default selectedTheme;
|
Loading…
Reference in a new issue