diff --git a/LICENSE b/LICENSE index 0a79592..a5616c1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ 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 of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index ad57616..5b9cef3 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,61 @@ # 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") 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 -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 -- Theming through JSON -- Search providers customizable through JSON (SUI has them both in a JSON and hardcoded) +- "Display URL" functionality (The URL displayed for apps can differ from the actual URL) +- Categorization for apps +- Themes and search providers can be changed using JSON +- Imprint functionality ## 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 $ cd dashboard/ $ yarn @@ -35,196 +63,139 @@ $ yarn build $ yarn serve:production ``` -2. Using Docker +## Configuration -``` -$ docker run -d \ - -v $(pwd)/data:/app/data - -p 8080:8080 \ - --name dashboard \ - phntxx/dashboard -``` +There's a couple of things you can / need to configure to get Dashboard to look and behave just as you want it to. -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. -``` -version: "3" +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: -services: - dashboard: - image: phntxx/dashboard:latest - restart: unless-stopped - volumes: - - [path to data directory]:/app/data - ports: - - 8080:8080 -``` +### Apps -**Note: You might still need to clone the repository in order to get the JSON-files which are required for the -app to run** +To show the apps you want to show, change `apps.json` to resemble the following: -## Customization - -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. - -``` +```json { - "name": "[Name of the Application]", - "displayURL": "[URL you want to show]", - "URL": "[URL to redirect to]", - "icon": "[Icon code]" + "categories": [ + { + "name": "[Name of the category]", + "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. -**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 -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]", - "items": [ - { - "name": "[Bookmark name]", - "url": "[URL to redirect to]" - }, - { - "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]" + "groups": [ + { + "name": "[Group Name]", + "items": [ + { + "name": "[Bookmark Name]", + "url": "[Bookmark URL]" + }, + ... + ] + }, + ... + ] } ``` -### 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]", - "url":"[Link that processes searches on that website]", - "prefix":"[a custom prefix]" -}, + "themes": [ + { + "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. diff --git a/data/imprint.json b/data/imprint.json index 38355be..c143aed 100644 --- a/data/imprint.json +++ b/data/imprint.json @@ -19,6 +19,7 @@ "url": { "text": "example.com", "link": "#" - } + }, + "text": "This is the place where you should put whatever you want it to say on the imprint page." } } diff --git a/src/app.tsx b/src/app.tsx index ac1799e..96c9ec8 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React from "react"; import { createGlobalStyle } from "styled-components"; import SearchBar from "./components/searchBar"; @@ -8,14 +8,8 @@ import BookmarkList from "./components/bookmarkList"; import Settings from "./components/settings"; import Imprint from "./components/imprint"; -import selectedTheme from "./components/themeManager"; -import { - useAppData, - useSearchProviderData, - useBookmarkData, - useThemeData, - useImprintData, -} from "./components/fetch"; +import selectedTheme from "./lib/theme"; +import useFetcher from "./lib/fetcher"; const GlobalStyle = createGlobalStyle` body { @@ -32,12 +26,12 @@ const GlobalStyle = createGlobalStyle` } `; +/** + * Renders the entire app by calling individual components + */ const App = () => { - const { appData } = useAppData(); - const { searchProviderData } = useSearchProviderData(); - const { bookmarkData } = useBookmarkData(); - const { themeData } = useThemeData(); - const { imprintData } = useImprintData(); + + const { appData, bookmarkData, searchProviderData, themeData, imprintData } = useFetcher(); return ( <> diff --git a/src/components/app.tsx b/src/components/app.tsx index ebcc082..8987287 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -1,7 +1,7 @@ import React from "react"; import Icon from "./icon"; import styled from "styled-components"; -import selectedTheme from "./themeManager"; +import selectedTheme from "../lib/theme"; const AppContainer = styled.div` display: flex; @@ -46,17 +46,21 @@ const AppDescription = styled.p` export interface IAppProps { name: string; icon: string; - URL: string; + url: 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) => ( - {name} + {name} {displayURL} diff --git a/src/components/appCategory.tsx b/src/components/appCategory.tsx index 84edcd0..c13aacd 100644 --- a/src/components/appCategory.tsx +++ b/src/components/appCategory.tsx @@ -16,6 +16,10 @@ export interface IAppCategoryProps { items: Array; } +/** + * Renders one app category + * @param {IAppCategoryProps} props - The props of the given category + */ export const AppCategory = ({ name, items }: IAppCategoryProps) => ( {name && {name}} @@ -25,7 +29,7 @@ export const AppCategory = ({ name, items }: IAppCategoryProps) => ( diff --git a/src/components/appList.tsx b/src/components/appList.tsx index 770d1e6..b09529c 100644 --- a/src/components/appList.tsx +++ b/src/components/appList.tsx @@ -9,22 +9,24 @@ export interface IAppListProps { apps: Array; } -const AppList = ({ categories, apps }: IAppListProps) => { - return ( - - Applications - {categories && - categories.map(({ name, items }, idx) => ( - - ))} - {apps && ( - - )} - - ); -}; +/** + * Renders one list containing all app categories and uncategorized apps + * @param {IAppListProps} props - The props of the given list of apps + */ +const AppList = ({ categories, apps }: IAppListProps) => ( + + Applications + {categories && + categories.map(({ name, items }, idx) => ( + + ))} + {apps && ( + + )} + +); export default AppList; diff --git a/src/components/bookmarkGroup.tsx b/src/components/bookmarkGroup.tsx index d32305a..1a44d27 100644 --- a/src/components/bookmarkGroup.tsx +++ b/src/components/bookmarkGroup.tsx @@ -1,7 +1,7 @@ import React from "react"; import styled from "styled-components"; import { Item, SubHeadline } from "./elements"; -import selectedTheme from "./themeManager"; +import selectedTheme from "../lib/theme"; const GroupContainer = styled.div` display: flex; @@ -32,13 +32,14 @@ export interface IBookmarkGroupProps { items: Array; } -export const BookmarkGroup = ({ - name: groupName, - items, -}: IBookmarkGroupProps) => ( +/** + * Renders a given bookmark group + * @param {IBookmarkGroupProps} props - The given props of the bookmark group + */ +export const BookmarkGroup = ({ name, items }: IBookmarkGroupProps) => ( - {groupName} + {name} {items.map(({ name, url }, idx) => ( {name} diff --git a/src/components/bookmarkList.tsx b/src/components/bookmarkList.tsx index a69886d..7f07852 100644 --- a/src/components/bookmarkList.tsx +++ b/src/components/bookmarkList.tsx @@ -1,24 +1,24 @@ import React from "react"; - import { Headline, ListContainer, ItemList } from "./elements"; - import { BookmarkGroup, IBookmarkGroupProps } from "./bookmarkGroup"; interface IBookmarkListProps { groups: Array; } -const BookmarkList = ({ groups }: IBookmarkListProps) => { - return ( - - Bookmarks - - {groups.map(({ name, items }, idx) => ( - - ))} - - - ); -}; +/** + * Renders a given list of categorized bookmarks + * @param {IBookmarkListProps} props - The props of the given bookmark list + */ +const BookmarkList = ({ groups }: IBookmarkListProps) => ( + + Bookmarks + + {groups.map(({ name, items }, idx) => ( + + ))} + + +); export default BookmarkList; diff --git a/src/components/elements.tsx b/src/components/elements.tsx index 4756c99..060b217 100644 --- a/src/components/elements.tsx +++ b/src/components/elements.tsx @@ -1,10 +1,8 @@ import React from "react"; import styled from "styled-components"; -import selectedTheme from "./themeManager"; +import selectedTheme from "../lib/theme"; import Icon from "./icon"; -// File for elements that are/can be reused across the entire site. - export const ListContainer = styled.div` padding: 2rem 0; `; @@ -50,7 +48,8 @@ export const Button = styled.button` border: 1px solid ${selectedTheme.mainColor}; color: ${selectedTheme.mainColor}; background: none; - min-height: 3em; + + min-height: 2rem; height: 100%; &:hover { @@ -61,6 +60,7 @@ export const Button = styled.button` const StyledButton = styled.button` float: right; border: none; + padding: 0; background: none; &: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 { icon: string; onClick: any; } +/** + * Renders a button with an icon + * @param {IIconProps} props - The props of the given IconButton + */ export const IconButton = ({ icon, onClick }: IIconButtonProps) => ( diff --git a/src/components/fetch.tsx b/src/components/fetch.tsx deleted file mode 100644 index 672e985..0000000 --- a/src/components/fetch.tsx +++ /dev/null @@ -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; - error: string | boolean; -} - -export const useSearchProviderData = () => { - const [ - searchProviderData, - setSearchProviderData, - ] = useState({ 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; - error: string | boolean; -} - -export const useBookmarkData = () => { - const [bookmarkData, setBookmarkData] = useState({ - 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; - apps: Array; - 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; - error: string | boolean; -} - -export const useThemeData = () => { - const [themeData, setThemeData] = useState({ - 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({ - 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 }; -}; diff --git a/src/components/greeter.tsx b/src/components/greeter.tsx index 96af2f3..14bb96a 100644 --- a/src/components/greeter.tsx +++ b/src/components/greeter.tsx @@ -1,7 +1,7 @@ import React from "react"; import styled from "styled-components"; -import selectedTheme from "./themeManager"; +import selectedTheme from "../lib/theme"; const GreeterContainer = styled.div` padding: 2rem 0; @@ -22,37 +22,6 @@ const DateText = styled.h3` 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 = [ "January", "February", @@ -78,6 +47,50 @@ const weekDayNames = [ "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 = () => { let currentDate = new Date(); @@ -93,16 +106,14 @@ const getDateString = () => { ); }; -const Greeter = () => { - let date = getDateString(); - let greeting = getGreeting(); - - return ( - - {date} - {greeting} - - ); -}; +/** + * Renders the Greeter + */ +const Greeter = () => ( + + {getDateString()} + {getGreeting()} + +); export default Greeter; diff --git a/src/components/icon.tsx b/src/components/icon.tsx index 54a718e..8c98bc6 100644 --- a/src/components/icon.tsx +++ b/src/components/icon.tsx @@ -1,36 +1,38 @@ import React from "react"; import styled from "styled-components"; -import selectedTheme from "./themeManager"; - -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"; -`; +import selectedTheme from "../lib/theme"; interface IIconProps { name: 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"}; 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 {name}; }; -export default ComponentIcon; +export default Icon; diff --git a/src/components/imprint.tsx b/src/components/imprint.tsx index 3990e67..8567d86 100644 --- a/src/components/imprint.tsx +++ b/src/components/imprint.tsx @@ -1,24 +1,16 @@ import React from "react"; import Modal from "./modal"; import styled from "styled-components"; -import selectedTheme from "./themeManager"; +import selectedTheme from "../lib/theme"; import { ListContainer, ItemList, - Headline as Hl, - SubHeadline as SHl, + Headline, + SubHeadline, } from "./elements"; -const Headline = styled(Hl)` - display: block; - padding: 1rem 0; -`; - -const SubHeadline = styled(SHl)` - display: block; -`; - const ModalSubHeadline = styled(SubHeadline)` + display: block; padding: 0.5rem 0; `; @@ -56,30 +48,40 @@ export interface IImprintProps { phone: IImprintFieldProps; email: IImprintFieldProps; url: IImprintFieldProps; + text: string; } interface IImprintFieldComponentProps { field: IImprintFieldProps; } -const ImprintField = ({ field }: IImprintFieldComponentProps) => ( - {field.text} -); - interface IImprintComponentProps { imprint: IImprintProps; } +/** + * Renders an imprint field + * @param {IImprintFieldComponentProps} props - The data for the field + */ +const ImprintField = ({ field }: IImprintFieldComponentProps) => ( + {field.text} +); + +/** + * Renders the imprint component + * @param {IImprintProps} props - The contents of the imprint + */ const Imprint = ({ imprint }: IImprintComponentProps) => ( <> - About + About - Imprint + Imprint { if (window.location.href.endsWith("#imprint")) { @@ -88,49 +90,24 @@ const Imprint = ({ imprint }: IImprintComponentProps) => ( } }} > - Legal Disclosure - - Information in accordance with section 5 TMG +
+ + Information in accordance with section 5 TMG - <> - {imprint.name && } - {imprint.address && } - {imprint.email && } - {imprint.phone && } - {imprint.url && } - - Disclaimer - Accountability for content - - The contents of our pages have been created with the utmost care. - However, we cannot guarantee the contents' accuracy, completeness - or topicality. According to statutory provisions, we are - furthermore responsible for our own content on these web pages. In - 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). - - Accountability for links - - 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. - - Copyright - - 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. - + <> + {imprint.name && } + {imprint.address && } + {imprint.email && } + {imprint.phone && } + {imprint.url && } + +
+
+ + Imprint + + {imprint.text && {imprint.text}} +
diff --git a/src/components/modal.tsx b/src/components/modal.tsx index b6cc0c4..566dee3 100644 --- a/src/components/modal.tsx +++ b/src/components/modal.tsx @@ -1,8 +1,8 @@ import React, { useState } from "react"; 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` 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; icon?: string; text?: string; condition?: boolean; + title: string; onClose?: () => void; 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 = () => { - if (props.onClose) props.onClose(); + if (onClose) onClose(); setModalHidden(!modalHidden); }; return ( <> - {props.element === "icon" && ( - closeModal()} /> + {element === "icon" && ( + closeModal()} /> )} - {props.element === "text" && ( - closeModal()}>{props.text} + {element === "text" && ( + closeModal()}>{text} )} ); diff --git a/src/components/searchBar.tsx b/src/components/searchBar.tsx index b12fb64..3e730e1 100644 --- a/src/components/searchBar.tsx +++ b/src/components/searchBar.tsx @@ -1,22 +1,43 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; 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` width: 100%; + font-size: 1rem; + border: none; border-bottom: 1px solid ${selectedTheme.accentColor}; + background: none; border-radius: 0; color: ${selectedTheme.mainColor}; + margin: 0px; + :focus { outline: none; } `; +const SearchButton = styled(Button)` + margin: 0px 2px; + min-height: 0; +`; + export interface ISearchProviderProps { name: string; url: string; @@ -27,11 +48,20 @@ interface ISearchBarProps { providers: Array | undefined; } +/** + * Renders a search bar + * @param {ISearchBarProps} props - The search providers for the search bar to use + */ const SearchBar = ({ providers }: ISearchBarProps) => { - let [input, setInput] = useState(""); + let [input, setInput] = useState(""); + let [buttonsHidden, setButtonsHidden] = useState(true); + + useEffect(() => { + setButtonsHidden(input === ""); + }, [input]); const handleSearchQuery = (e: React.FormEvent) => { - var query = input || ""; + var query: string = input || ""; if (query.split(" ")[0].includes("/")) { handleQueryWithProvider(query); @@ -43,13 +73,14 @@ const SearchBar = ({ providers }: ISearchBarProps) => { }; const handleQueryWithProvider = (query: string) => { - let queryArray = query.split(" "); - let prefix = queryArray[0]; + let queryArray: Array = query.split(" "); + let prefix: string = queryArray[0]; + queryArray.shift(); - let searchQuery = queryArray.join(" "); + let searchQuery: string = queryArray.join(" "); - let providerFound = false; + let providerFound: boolean = false; if (providers) { providers.forEach((provider: ISearchProviderProps) => { if (provider.prefix === prefix) { @@ -64,13 +95,25 @@ const SearchBar = ({ providers }: ISearchBarProps) => { }; return ( -
handleSearchQuery(e)}> + handleSearchQuery(e)}> setInput(e.target.value)} + value={input} + onChange={(e: React.ChangeEvent) => + setInput(e.target.value) + } > -