diff --git a/src/app.tsx b/src/app.tsx index 8973b0c..0b716df 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -10,9 +10,17 @@ import { NotFound } from "./pages/not-found"; import { Stories } from "./pages/stories"; import { Blog } from "./pages/blog"; import { Post } from "./pages/post"; +import { SignUp } from "./pages/sign_up"; +import { SignIn } from "./pages/sign_in"; +import { Dashboard } from "./pages/Dashboard"; +import { Edit } from "./pages/Edit"; import { Chapter1 } from "./pages/chapter1"; import { Chapter2 } from "./pages/chapter2"; +import { Ticket } from "./pages/Ticket"; +import { PwResetRequest } from "./pages/PwResetRequest"; +import { PwReset } from "./pages/PwReset"; +import { EmailConfirm } from "./pages/EmailConfirm"; import { Chapter3 } from "./pages/chapter3"; export const App: React.FC = () => ( @@ -27,6 +35,15 @@ export const App: React.FC = () => ( + + + + } /> + + + + + diff --git a/src/components/blog/feed/filter-control/categories.tsx b/src/components/blog/feed/filter-control/categories.tsx index 9bf82db..a3304e7 100644 --- a/src/components/blog/feed/filter-control/categories.tsx +++ b/src/components/blog/feed/filter-control/categories.tsx @@ -1,4 +1,7 @@ import cn from "classnames"; +import React, { useEffect, useState } from "react"; +import { getTopics } from "../../../../users_api/UserAPI"; +import TopicInterface from "../../../user-dashboard"; import { useCategories } from "../../wp/categories"; import { useFilteredCategories } from "../fitering"; @@ -23,7 +26,53 @@ const Button: React.FC<{ onClick(): void; active: boolean }> = ({ }; export function CategoriesFilterControl() { + const [user, setUser] = useState(undefined); + const [userTopicsSelected, setUserTopicsSelected] = useState(false); + const [topics, setTopics] = useState([]); + + + useEffect(() => { + const loggedInUser = localStorage.getItem('user'); + if (loggedInUser) { + const foundUser = JSON.parse(loggedInUser); + setUser(foundUser); + fetchTopics(); + } else { + setUser(undefined); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [localStorage.getItem('user')]); + const { loading, error, data } = useCategories(); + + const selectUserAll = () => { + if (data) { + topics.forEach((topic) => { + const foundCategory = data.categories.nodes.find((category) => category.name === topic.name); + if (foundCategory) { + select(foundCategory); + } + }) + } + } + + const fetchTopics = async () => { + let response; + try { + response = await getTopics() + } catch(error) { + setTopics([]) + } + let responseTopics: Array = [] + if (response) { + // @ts-ignore + response.forEach(element => { + responseTopics.push(element) + }); + setTopics(responseTopics) + } + } + const { items: selectedCategories, add: select, @@ -38,6 +87,19 @@ export function CategoriesFilterControl() { + { user && ( + + )} {data.categories.nodes.map((category, i) => { const isSelected = selectedCategories.indexOf(category) >= 0; diff --git a/src/components/blog/feed/index.tsx b/src/components/blog/feed/index.tsx index 58d0088..1c17c15 100644 --- a/src/components/blog/feed/index.tsx +++ b/src/components/blog/feed/index.tsx @@ -1,16 +1,39 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import { useFeed, FeedItemData } from "../wp"; import { Post } from "./post"; import { FilterControl } from "./filter-control"; import { Spinner } from "../../spinner"; import { SortMethodSelection, defaultSortMethod, SortMethod } from "./sorting"; +import { favoritPosts } from "../../../users_api/UserAPI"; function PostList({ sortMethod }: { sortMethod: SortMethod }) { + const [favoritposts, setFavoritPosts] = useState([]); + const [loaded, setLoaded] = useState(false); const { loading, error, data } = useFeed(); - const [firstRenderDone, setFirstRenderDone] = React.useState(false); + const [firstRenderDone, setFirstRenderDone] = useState(false); - React.useEffect(() => { + const fetchFavorits = async () => { + let response; + try { + response = await favoritPosts(); + } catch (error) { + setFavoritPosts([]); + } + // @ts-ignore + let slugs = response.slugs; + setFavoritPosts(JSON.parse(JSON.stringify(slugs))); + setLoaded(true); + }; + + useEffect(() => { + const loggedInUser = localStorage.getItem("user"); + + if (loggedInUser) { + fetchFavorits(); + } else { + setLoaded(true); + } if (data) setFirstRenderDone(true); }, [data]); @@ -26,15 +49,19 @@ function PostList({ sortMethod }: { sortMethod: SortMethod }) { if (loading && firstRenderDone) return ; - return ( -
- {(items[0] ? (items as FeedItemData[]).sort(sortMethod.fn) : items).map( - (data, i) => ( - - ) - )} -
- ); + if (loaded) { + return ( +
+ {(items[0] ? (items as FeedItemData[]).sort(sortMethod.fn) : items).map( + (data, i) => ( + + ) + )} +
+ ); + } + + return } export const Feed: React.FC = () => { diff --git a/src/components/blog/feed/post/index.tsx b/src/components/blog/feed/post/index.tsx index c958d0c..633dc93 100644 --- a/src/components/blog/feed/post/index.tsx +++ b/src/components/blog/feed/post/index.tsx @@ -9,8 +9,10 @@ import { BookmarkButton } from "../../single/bookmark-button"; import { DetailBar } from "./detail-bar"; import styles from "./post.module.css"; +import { LatestPostData } from "../../wp/latest_post"; export type ParsedPostData = { + slug: string; path: string; title: string; excerpt: string; @@ -22,10 +24,11 @@ export type ParsedPostData = { tags?: TagData[]; }; -export const Post: React.FC<{ data?: FeedItemData }> = ({ data }) => { +export const Post: React.FC<{ data?: FeedItemData | LatestPostData, favorits: string[]}> = ({ data, favorits }) => { const parsedData = React.useMemo( () => data && { + slug: data.slug, path: `/blog/${data.slug}`, title: data.title, excerpt: data.excerpt, @@ -84,7 +87,7 @@ export const Post: React.FC<{ data?: FeedItemData }> = ({ data }) => { )}
- +
); diff --git a/src/components/blog/single/bookmark-button/index.tsx b/src/components/blog/single/bookmark-button/index.tsx index 8ec0884..e666e9b 100644 --- a/src/components/blog/single/bookmark-button/index.tsx +++ b/src/components/blog/single/bookmark-button/index.tsx @@ -1,20 +1,52 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import cn from "classnames"; import { BookmarkIcon } from "./icon"; import styles from "./bookmark.module.css"; +import { deFavorisePost, favorisePost } from "../../../../users_api/UserAPI"; export const BookmarkButton: React.FC<{ disabled?: boolean; onSolidBackground?: boolean; -}> = ({ disabled, onSolidBackground }) => { - const [active, setActive] = React.useState(false); + slug: string; + posts: string[]; +}> = ({ disabled, onSolidBackground, slug, posts }) => { + const [active, setActive] = useState(false); + + const updateFavorise = async (slug: string) => { + let favorite_blogpost = { + slug, + }; + if (active) { + try { + await deFavorisePost(favorite_blogpost); + } catch (error) { + } + } else { + try { + await favorisePost(favorite_blogpost); + } catch (error) { + } + } + }; + + + useEffect(() => { + if (posts.includes(slug)) { + setActive(true); + } + }, [posts, slug]); + + return ( + + + +
+ +
+ + + ); +}; diff --git a/src/components/layout/header/desktop-nav/index.tsx b/src/components/layout/header/desktop-nav/index.tsx index 4edd4fa..9bcadb2 100644 --- a/src/components/layout/header/desktop-nav/index.tsx +++ b/src/components/layout/header/desktop-nav/index.tsx @@ -7,10 +7,12 @@ import type { NavItem } from ".."; import { Button } from "../button"; import { SearchButton } from "../search-button"; -export const DesktopNav: React.FC<{ items: NavItem[]; onDark?: boolean }> = ({ - items, - onDark, -}) => { +export const DesktopNav: React.FC<{ + items: NavItem[]; + onDark?: boolean; + user?: any; + handleLogOut: () => void; +}> = ({ items, onDark, user, handleLogOut }) => { const { pathname } = useLocation(); return ( @@ -37,10 +39,27 @@ export const DesktopNav: React.FC<{ items: NavItem[]; onDark?: boolean }> = ({
- - + {user ? ( + <> + + + + + + ) : ( + <> + + + + + + + + )}
); diff --git a/src/components/layout/header/index.tsx b/src/components/layout/header/index.tsx index 3476fc5..95b85d5 100644 --- a/src/components/layout/header/index.tsx +++ b/src/components/layout/header/index.tsx @@ -1,9 +1,10 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import cn from "classnames"; -import { Link } from "react-router-dom"; +import { Link, useHistory } from "react-router-dom"; import { DesktopNav } from "./desktop-nav"; import { MobileNav } from "./mobile-nav"; +import { logout } from "../../../users_api/UserAPI"; export type NavItem = { path: string; @@ -15,37 +16,63 @@ const items: NavItem[] = [ { path: "/blog", label: "Archiv" }, ]; -export const Header: React.FC<{ onDark?: boolean }> = ({ onDark }) => ( -
-
-
-
-
- -
- Klimabox - -
-
-
- +export const Header: React.FC<{ onDark?: boolean }> = ({ onDark }) => { + const [user, setUser] = useState(undefined); + let history = useHistory(); + + useEffect(() => { + const loggedInUser = localStorage.getItem('user'); + if (loggedInUser) { + const foundUser = JSON.parse(loggedInUser); + setUser(foundUser); + } else { + setUser(undefined) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [localStorage.getItem('user')]); + + const handleLogout = async () => { + try { + await logout() + } catch(error) { + console.log(error) + } + localStorage.removeItem('user') + history.push('/') + } + + return ( +
+
+
+
+
+ +
+ Klimabox +
-
- +
+
+ handleLogout()}/> +
+
+ handleLogout()} /> +
-
-); + ); +}; diff --git a/src/components/layout/header/mobile-nav/index.tsx b/src/components/layout/header/mobile-nav/index.tsx index d251843..46ca996 100644 --- a/src/components/layout/header/mobile-nav/index.tsx +++ b/src/components/layout/header/mobile-nav/index.tsx @@ -10,9 +10,13 @@ import { SearchButton } from "../search-button"; export function MobileNav({ items, onDark, + user, + handleLogOut, }: { items: NavItem[]; onDark?: boolean; + user?: any; + handleLogOut: () => void; }) { const { pathname } = useLocation(); const [open, setOpen] = React.useState(false); @@ -48,12 +52,31 @@ export function MobileNav({
-
- -
-
- -
+ {user ? ( + <> +
+ + + +
+
+ +
+ + ) : ( + <> +
+ + + +
+
+ + + +
+ + )}
diff --git a/src/components/layout/index.tsx b/src/components/layout/index.tsx index 243e49d..5d5a6e9 100644 --- a/src/components/layout/index.tsx +++ b/src/components/layout/index.tsx @@ -1,7 +1,7 @@ import React from "react"; -import { Header } from "./header"; import { Footer } from "./footer"; +import { Header } from "./header"; import { SideNav } from "./side-nav"; type LayoutProps = { diff --git a/src/components/pw_reset/index.tsx b/src/components/pw_reset/index.tsx new file mode 100644 index 0000000..bc01433 --- /dev/null +++ b/src/components/pw_reset/index.tsx @@ -0,0 +1,114 @@ +import React, { useEffect, useState } from "react"; +import { useHistory } from "react-router"; +import { updatePw } from "../../users_api/UserAPI"; + +export const PwResetForm: React.FC = () => { + let history = useHistory(); + const [token, setToken] = useState(""); + const [password, setPassword] = useState(""); + const [password_confirmation, setPasswordConfirmation] = useState(""); + + const displayError = (errors: any) => { + if (errors) { + Object.keys(errors).forEach((error) => { + if (error === 'other') { + let field = document.querySelector(`#form`); + let errorMessage = document.createElement("div"); + errorMessage.id = "error"; + errorMessage.className += + "flex font-medium tracking-wide text-red-500 text-xs mt-1 mb-2"; + errorMessage.innerHTML += errors[error]; + console.log(field) + if (field) { + field.prepend(errorMessage); + } + return; + } + let field = document.querySelector(`#${error}`); + let errorMessage = document.createElement("div"); + errorMessage.id = "error"; + errorMessage.className += + "flex font-medium tracking-wide text-red-500 text-xs mt-1 mb-2"; + errorMessage.innerHTML += errors[error]; + if (field) { + field.parentElement?.append(errorMessage); + } + }); + } + }; + + const removeErrors = () => { + document.querySelectorAll("#error").forEach((e) => e.remove()); + }; + + useEffect(() => { + const token = history.location.search; + if (token) { + setToken(token.substring(7, token.length)); + } else { + setToken(""); + } + }, [history.location.search]); + + const handleUpdatePw = async () => { + removeErrors(); + let data = { + token: token, + password: password, + password_confirmation: password_confirmation, + }; + try { + await updatePw(data); + } catch (error) { + // @ts-ignore + displayError(error.body.errors); + return; + } + history.push("/signin"); + }; + + return ( +
+
+
+ +
+
+ + setPassword(e.target.value)} + className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" + id="password" + type="password" + placeholder="Passwort" + /> +
+
+ + setPasswordConfirmation(e.target.value)} + className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline" + id="password_confirmation" + type="password" + placeholder="Passwort bestätigen" + /> +
+
+ +
+
+
+ ); +}; diff --git a/src/components/pw_reset_request/index.tsx b/src/components/pw_reset_request/index.tsx new file mode 100644 index 0000000..055f8bb --- /dev/null +++ b/src/components/pw_reset_request/index.tsx @@ -0,0 +1,58 @@ +import React, { useState } from "react"; +import { useHistory } from "react-router"; +import { requestPwReset } from "../../users_api/UserAPI"; + +export const PwResetRequestForm: React.FC = () => { + let history = useHistory(); + const [hasError, setHasError] = useState(false); + const [email, setEmail] = useState(""); + + const handleResetRequest = async () => { + let data = { + email: email, + }; + setHasError(false); + try { + await requestPwReset(data); + } catch (error) { + setHasError(true); + return; + } + history.push("/email_confirm"); + }; + + return ( +
+
+ {hasError && ( + + Es konnte kein Account gefunden werden. Bitte überprüfe die Angaben + zur E-Mail Adresse. + + )} +
+ + setEmail(e.target.value)} + className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" + id="username" + type="text" + placeholder="E-Mail" + /> +
+
+ +
+
+
+ ); +}; diff --git a/src/components/signin/index.tsx b/src/components/signin/index.tsx new file mode 100644 index 0000000..4b904dd --- /dev/null +++ b/src/components/signin/index.tsx @@ -0,0 +1,82 @@ +import React, { useState } from "react"; +import { Link, useHistory } from "react-router-dom"; +import { login } from "../../users_api/UserAPI"; + +export const SignInForm: React.FC = () => { + let history = useHistory(); + const [hasError, setHasError] = useState(false); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + + const handleSignIn = async () => { + let data = { + email: email, + password: password, + }; + let response; + setHasError(false); + try { + response = await login(data); + } catch (error) { + setHasError(true); + return; + } + + localStorage.setItem("user", JSON.stringify(response)); + history.push("/dashboard"); + }; + + return ( +
+
+ {hasError && ( + + Es konnte kein Account gefunden werden. Bitte überprüfe die Angaben + zur E-Mail Adresse und Passwort. + + )} +
+ + setEmail(e.target.value)} + className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" + id="username" + type="text" + placeholder="E-Mail" + /> +
+
+ + setPassword(e.target.value)} + className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline" + id="password" + type="password" + placeholder="Passwort" + /> +
+
+ +
+
+ Passwort vergessen? + +

Jetzt Passwort zurücksetzen

+ +
+
+
+ ); +}; diff --git a/src/components/signup/index.tsx b/src/components/signup/index.tsx new file mode 100644 index 0000000..e5b3012 --- /dev/null +++ b/src/components/signup/index.tsx @@ -0,0 +1,106 @@ +import React, { useState } from "react"; +import { useHistory } from "react-router"; +import { createUser } from "../../users_api/UserAPI"; + +export const SignUpForm: React.FC = () => { + let history = useHistory(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [password_confirmation, setPasswordConfirmation] = useState(""); + + const displayError = (errors: any) => { + if (errors) { + Object.keys(errors).forEach( (error) => { + let field = document.querySelector(`#${error}`) + let errorMessage = document.createElement("div"); + errorMessage.id = 'error'; + errorMessage.className += "flex font-medium tracking-wide text-red-500 text-xs mt-1 mb-2"; + errorMessage.innerHTML += errors[error] + if (field) { + field.parentElement?.append(errorMessage) + } + }) + } + } + + const removeErrors = () => { + document.querySelectorAll("#error").forEach((e) => e.remove()); + } + + const handleSignUp = async () => { + removeErrors(); + let data = { + email: email, + password: password, + password_confirmation: password_confirmation, + }; + let hasError = false; + try { + await createUser(data); + } catch (error) { + hasError = true; + // @ts-ignore + displayError(error.body.errors); + return; + } + if (!hasError) { + history.push("/email_confirm"); + } + }; + + return ( +
+
+
+ + setEmail(e.target.value)} + className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" + id="email" + type="text" + placeholder="E-Mail" + /> +
+
+ + setPassword(e.target.value)} + className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" + id="password" + type="password" + placeholder="Passwort" + /> +
+
+ + setPasswordConfirmation(e.target.value)} + className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline" + id="password_confirmation" + type="password" + placeholder="Passwort bestätigen" + /> +
+
+ +
+
+
+ ); +}; + diff --git a/src/components/ticket/comment/index.tsx b/src/components/ticket/comment/index.tsx new file mode 100644 index 0000000..c9bb34d --- /dev/null +++ b/src/components/ticket/comment/index.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { CommentInterface } from "../../user-dashboard/tickets"; + +export const Comment: React.FC <{ comment: CommentInterface, handleDeleteComment: (id: string) => void}> = ({ comment, handleDeleteComment }) => { + const date = new Date(comment.created_at) + const loggedInUser = localStorage.getItem('user') + let foundUser; + if (loggedInUser) { + foundUser = JSON.parse(loggedInUser); + } + return ( + <> +
+
+
+

+ Von: {comment.user_email} +

+

+ Am: {date.toUTCString()} +

+
+
+ { comment.user_email === foundUser.email && ( + + )} +
+
+
+ {comment.message} +
+
+ + ) +}; diff --git a/src/components/ticket/index.tsx b/src/components/ticket/index.tsx new file mode 100644 index 0000000..c8444b9 --- /dev/null +++ b/src/components/ticket/index.tsx @@ -0,0 +1,183 @@ +import React, { useEffect, useState } from "react"; +import { + createComment, + deleteComment, + deleteTicket, + getTicket, +} from "../../users_api/UserAPI"; +import { Comment } from "./comment"; + +import { TicketInterface } from "../user-dashboard/tickets"; +import { Link, useHistory } from "react-router-dom"; + +export const TicketShow: React.FC<{ id: string }> = ({ id }) => { + let history = useHistory(); + const [hasError, setHasError] = useState(false); + const [ticket, setTicket] = useState(); + const [comment, setComment] = useState(""); + + const displayError = (errors: any) => { + if (errors) { + Object.keys(errors).forEach( (error) => { + let field = document.querySelector(`#${error}`) + let errorMessage = document.createElement("div"); + errorMessage.id = 'error'; + errorMessage.className += "flex font-medium tracking-wide text-red-500 text-xs mt-1 mb-2"; + errorMessage.innerHTML += errors[error] + if (field) { + field.parentElement?.append(errorMessage) + } + }) + } + } + + const removeErrors = () => { + document.querySelectorAll("#error").forEach((e) => e.remove()); + } + + const fetchTicket = async () => { + let response; + setHasError(false); + try { + response = await getTicket(parseInt(id, 10)); + } catch (error) { + setHasError(true); + return; + } + if (response) { + // @ts-ignore + setTicket(response); + } + }; + + useEffect(() => { + fetchTicket(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleSendComment = async (comment: string) => { + removeErrors(); + let data = { + message: comment, + }; + try { + await createComment(data, parseInt(id, 10)); + } catch (error) { + // @ts-ignore + displayError(error.body.errors); + } + setComment(""); + fetchTicket(); + }; + + const handleDeleteTicket = async () => { + setHasError(false); + try { + await deleteTicket(parseInt(id, 10)); + } catch (error) { + setHasError(true); + return; + } + history.push("/dashboard"); + }; + + const handleDeleteComment = async (id: string) => { + setHasError(false); + try { + await deleteComment(parseInt(id, 10)); + } catch (error) { + setHasError(true); + return; + } + fetchTicket(); + }; + + if (ticket) { + return ( +
+
+ + Zurück + +

Ticket Übersicht

+ {hasError && ( +
+
+ Es ist etwas schief gelaufen. +
+
+ +

Zu deinem Bereich

+ +
+
+ )} +
+
+

{ticket.message}

+ +
+
Kommentare
+ {ticket.comments.length === 0 ? ( +

Noch keine Kommentare vorhaden

+ ) : ( +
+

+ {ticket.comments.length} Kommentare +

+ + {ticket.comments.map((comment) => { + return ( + + ); + })} +
+ )} +
+
+
+ +