Refactor Part 1

This commit is contained in:
phntxx 2021-03-21 18:05:24 +01:00
parent bc897a6bfb
commit 9fef36eae3
18 changed files with 314 additions and 310 deletions

View file

@ -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,13 @@ const GlobalStyle = createGlobalStyle`
}
`;
/**
* Renders the entire app by calling individual components
* @returns
*/
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 (
<>

View file

@ -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;
@ -50,6 +50,11 @@ export interface IAppProps {
displayURL: string;
}
/**
* Renders one app in the list
* @param
* @returns
*/
export const App = ({ name, icon, URL, displayURL }: IAppProps) => (
<AppContainer>
<IconContainer>

View file

@ -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;

View file

@ -1,6 +1,6 @@
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.
@ -62,6 +62,7 @@ export const Button = styled.button`
const StyledButton = styled.button`
float: right;
border: none;
padding: 0;
background: none;
&:hover {
@ -69,16 +70,6 @@ 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;

View file

@ -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 };
};

View file

@ -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;

View file

@ -1,11 +1,20 @@
import React from "react";
import styled from "styled-components";
import selectedTheme from "./themeManager";
import selectedTheme from "../lib/theme";
export const RawIcon = styled.i`
interface IIconProps {
name: string;
size?: string;
}
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;
@ -19,18 +28,7 @@ export const RawIcon = styled.i`
font-feature-settings: "liga";
`;
interface IIconProps {
name: string;
size?: string;
}
export const ComponentIcon = ({ name, size }: IIconProps) => {
let IconContainer = styled(RawIcon)`
font-size: ${size ? size : "24px"};
color: ${selectedTheme.mainColor};
`;
return <IconContainer>{name}</IconContainer>;
};
export default ComponentIcon;
export default Icon;

View file

@ -1,7 +1,7 @@
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,
@ -14,11 +14,8 @@ const Headline = styled(Hl)`
padding: 1rem 0;
`;
const SubHeadline = styled(SHl)`
const ModalSubHeadline = styled(SHl)`
display: block;
`;
const ModalSubHeadline = styled(SubHeadline)`
padding: 0.5rem 0;
`;
@ -80,6 +77,7 @@ const Imprint = ({ imprint }: IImprintComponentProps) => (
<Modal
element="text"
text="View Imprint"
title="Legal Disclosure"
condition={!window.location.href.endsWith("#imprint")}
onClose={() => {
if (window.location.href.endsWith("#imprint")) {
@ -88,7 +86,6 @@ const Imprint = ({ imprint }: IImprintComponentProps) => (
}
}}
>
<Headline>Legal Disclosure</Headline>
<ModalSubHeadline>
Information in accordance with section 5 TMG
</ModalSubHeadline>

View file

@ -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,47 @@ const Text = styled.p`
}
`;
const TitleContainer = styled.div`
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
`;
interface IModalInterface {
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);
const Modal = ({ element, icon, text, condition, title, onClose, children }: IModalInterface) => {
const [modalHidden, setModalHidden] = useState(condition ?? true);
const closeModal = () => {
if (props.onClose) props.onClose();
if (onClose) onClose();
setModalHidden(!modalHidden);
};
return (
<>
{props.element === "icon" && (
<IconButton icon={props.icon ?? ""} onClick={() => closeModal()} />
{element === "icon" && (
<IconButton icon={icon ?? ""} onClick={() => closeModal()} />
)}
{props.element === "text" && (
<Text onClick={() => closeModal()}>{props.text}</Text>
{element === "text" && (
<Text onClick={() => closeModal()}>{text}</Text>
)}
<ModalContainer hidden={modalHidden}>
<IconButton icon="close" onClick={() => closeModal()} />
{props.children}
<TitleContainer>
<Headline>{title}</Headline>
<IconButton icon="close" onClick={() => closeModal()} />
</TitleContainer>
{children}
</ModalContainer>
</>
);

View file

@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react";
import styled from "styled-components";
import selectedTheme from "./themeManager";
import selectedTheme from "../lib/theme";
import { Button } from "./elements";

View file

@ -1,21 +1,31 @@
import React, { useState } from "react";
import styled from "styled-components";
import Select from "react-select";
import Select, { Styles } from "react-select";
import { ISearchProviderProps } from "./searchBar";
import selectedTheme, { setTheme, IThemeProps } from "./themeManager";
import { Button, Headline as hl } from "./elements";
import selectedTheme, { setTheme, IThemeProps } from "../lib/theme";
import { Button, SubHeadline } from "./elements";
import Modal from "./modal";
const Headline = styled(hl)`
padding: 0.5rem 0;
`;
/**
* Complementary code to get hover pseudo-classes working in React
* @param color the color of the element on hover
* @param backgroundColor the background color of the element on hover
* @param border the border of the element on hover
* @param borderColor the border color of the element on hover
*/
interface IHoverProps {
color?: string;
backgroundColor?: string;
border?: string;
borderColor?: string;
}
const SelectContainer = styled.div`
padding-bottom: 1rem;
`;
interface IPseudoProps extends React.CSSProperties {
"&:hover": IHoverProps
}
const FormContainer = styled.div`
display: grid;
@ -42,47 +52,72 @@ const HeadCell = styled.th`
background: none;
`;
const SelectorStyle = {
control: (provided: any) => ({
...provided,
fontWeight: "500",
const Section = styled.div`
padding: 1rem 0;
`;
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,
textTransform: "uppercase",
width: "12rem",
background: "none",
borderRadius: "0px",
border: "1px solid " + selectedTheme.mainColor,
boxShadow: 0,
borderRadius: 0,
border: "1px solid",
borderColor: selectedTheme.mainColor,
boxShadow: "none",
"&:hover": {
border: "1px solid " + selectedTheme.mainColor,
border: "1px solid",
borderColor: selectedTheme.mainColor
},
}),
menu: (provided: any) => ({
...provided,
menu: (base: React.CSSProperties): React.CSSProperties => ({
...base,
backgroundColor: selectedTheme.backgroundColor,
border: "1px solid " + selectedTheme.mainColor,
borderRadius: 0,
boxShadow: 0,
boxShadow: "none",
margin: "4px 0"
}),
option: (provided: any) => ({
...provided,
fontWeight: "500",
option: (base: React.CSSProperties): IPseudoProps => ({
...base,
fontWeight: 500,
color: selectedTheme.mainColor,
textTransform: "uppercase",
borderRadius: 0,
boxShadow: 0,
boxShadow: "none",
backgroundColor: selectedTheme.backgroundColor,
"&:hover": {
backgroundColor: selectedTheme.mainColor,
color: selectedTheme.backgroundColor,
},
}),
singleValue: (provided: any) => {
return {
...provided,
color: selectedTheme.mainColor,
};
},
singleValue: (base: React.CSSProperties): React.CSSProperties => ({
...base,
color: selectedTheme.mainColor,
}),
};
interface ISettingsProps {
@ -95,10 +130,13 @@ const Settings = ({ themes, providers }: ISettingsProps) => {
if (themes && providers) {
return (
<Modal element="icon" icon="settings">
<Modal element="icon" icon="settings" title="Settings">
{themes && (
<SelectContainer>
<Headline>Theme:</Headline>
<Section>
<SectionHeadline>Theme:</SectionHeadline>
<FormContainer>
<Select
options={themes}
@ -108,29 +146,33 @@ const Settings = ({ themes, providers }: ISettingsProps) => {
}}
styles={SelectorStyle}
/>
<Button onClick={() => setTheme(JSON.stringify(newTheme))}>
Apply
</Button>
<Button onClick={() => window.location.reload()}>Refresh</Button>
</FormContainer>
</SelectContainer>
</Section>
)}
{providers && (
<Table>
<tbody>
<TableRow>
<HeadCell>Search Provider</HeadCell>
<HeadCell>Prefix</HeadCell>
</TableRow>
{providers.map((provider, index) => (
<TableRow key={provider.name + index}>
<TableCell>{provider.name}</TableCell>
<TableCell>{provider.prefix}</TableCell>
<Section>
<SectionHeadline>Search Providers</SectionHeadline>
<Table>
<tbody>
<TableRow>
<HeadCell>Search Provider</HeadCell>
<HeadCell>Prefix</HeadCell>
</TableRow>
))}
</tbody>
</Table>
{providers.map((provider, index) => (
<TableRow key={provider.name + index}>
<TableCell>{provider.name}</TableCell>
<TableCell>{provider.prefix}</TableCell>
</TableRow>
))}
</tbody>
</Table>
</Section>
)}
</Modal>
);

166
src/lib/fetcher.tsx Normal file
View file

@ -0,0 +1,166 @@
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";
const handleResponse = (response: any) => {
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: "" },
},
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
*/
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.
* @returns all of the data the function was able to fetch and the callback function to refresh the data
*/
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;

View file

@ -15,17 +15,10 @@ const defaultTheme: IThemeProps = {
};
export const setTheme = (theme: string) => {
if (theme !== undefined) {
localStorage.setItem("theme", theme);
}
if (theme !== undefined) localStorage.setItem("theme", theme);
window.location.reload();
};
export const resetTheme = () => {
localStorage.removeItem("theme");
};
const getTheme = () => {
let selectedTheme = defaultTheme;
@ -37,5 +30,4 @@ const getTheme = () => {
};
const selectedTheme = getTheme();
export default selectedTheme;