introducing redux and fixing stuff
parent
c6bef04c73
commit
66a61d4cf9
@ -0,0 +1,6 @@
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
||||
import {AppDispatch, RootState} from "./store";
|
||||
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
@ -0,0 +1,17 @@
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
|
||||
import AuthReducer from "../features/auth/auth-slice";
|
||||
import {cardDeckApiSlice} from "../features/cardDecks/cardDeck-api-slice";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
auth: AuthReducer,
|
||||
[cardDeckApiSlice.reducerPath]: cardDeckApiSlice.reducer
|
||||
},
|
||||
middleware: (getDefaultMiddleware) => {
|
||||
return getDefaultMiddleware().concat(cardDeckApiSlice.middleware);
|
||||
}
|
||||
});
|
||||
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import { Navigate, useLocation } from "react-router-dom";
|
||||
import { useAppSelector } from "../app/hooks";
|
||||
|
||||
interface IAuthRedirect {
|
||||
isAuthenticated: boolean;
|
||||
to: string;
|
||||
}
|
||||
|
||||
const AuthRedirect: React.FC<IAuthRedirect> = ({ children , isAuthenticated, to}) => {
|
||||
const authenticated = useAppSelector(state => state.auth.authenticated);
|
||||
|
||||
const { pathname } = useLocation();
|
||||
|
||||
if (authenticated == isAuthenticated) return (<>{children}</>);
|
||||
return (<Navigate to={to} replace state={{ path: pathname }} />);
|
||||
};
|
||||
|
||||
export default AuthRedirect;
|
@ -1,12 +0,0 @@
|
||||
import React from "react";
|
||||
import { Navigate, useLocation } from "react-router-dom";
|
||||
import { useAuthUser } from "../providers/AuthUser";
|
||||
|
||||
const AuthenticatedComponent = ({ children }: { children: any }) => {
|
||||
const { authenticated } = useAuthUser();
|
||||
const { pathname } = useLocation();
|
||||
if (authenticated) return children;
|
||||
return <Navigate to="/login" replace state={{ path: pathname }} />;
|
||||
};
|
||||
|
||||
export default AuthenticatedComponent;
|
@ -1,9 +1,13 @@
|
||||
import React from "react";
|
||||
import { WiredCard as Card } from "wired-elements-react";
|
||||
import {CardWrapper} from "./styled";
|
||||
|
||||
const WiredCard: React.FC = ({ children }) => {
|
||||
var childrenWithType = children as HTMLCollection & React.ReactNode;
|
||||
return <Card>{childrenWithType}</Card>;
|
||||
interface IWiredCard {
|
||||
elevation?: number;
|
||||
}
|
||||
const WiredCard: React.FC<IWiredCard> = ({ children, elevation}) => {
|
||||
var childrenWithType = (<CardWrapper>{children}</CardWrapper>) as HTMLCollection & React.ReactNode;
|
||||
return <Card elevation={elevation}>{childrenWithType}</Card>;
|
||||
};
|
||||
|
||||
export default WiredCard;
|
||||
|
@ -0,0 +1,5 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
export const CardWrapper = styled.div`
|
||||
padding: 10px;
|
||||
`;
|
@ -0,0 +1,100 @@
|
||||
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import axios from "axios";
|
||||
import {LoadingType} from "../../models/Types";
|
||||
|
||||
interface AuthState {
|
||||
state: LoadingType,
|
||||
authenticated: boolean;
|
||||
user?: User;
|
||||
}
|
||||
|
||||
interface User {
|
||||
email: string;
|
||||
username: string;
|
||||
}
|
||||
interface LoginCredentials {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface RegisterCredentials {
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const initialState: AuthState = {
|
||||
state: "idle",
|
||||
authenticated: false,
|
||||
user: undefined,
|
||||
}
|
||||
|
||||
export const logout = createAsyncThunk("auth/logout", async () => {
|
||||
await axios.get("/api/auth/logout");
|
||||
})
|
||||
|
||||
export const login = createAsyncThunk("auth/login", async (credentials: LoginCredentials) => {
|
||||
const responce = await axios.post<User>("/api/auth/login", credentials);
|
||||
return responce.data;
|
||||
})
|
||||
|
||||
export const register = createAsyncThunk("auth/register", async (credentials: RegisterCredentials) => {
|
||||
const responce = await axios.post<User>("/api/auth/register", credentials);
|
||||
return responce.data;
|
||||
})
|
||||
|
||||
export const verify = createAsyncThunk("auth/verify", async () => {
|
||||
const responce = await axios.get<User>("/api/auth/verify");
|
||||
return responce.data;
|
||||
});
|
||||
|
||||
const authSlice = createSlice({
|
||||
name: "auth",
|
||||
initialState,
|
||||
reducers: {
|
||||
},
|
||||
extraReducers: builder => {
|
||||
builder
|
||||
.addCase(logout.fulfilled, (state) => {
|
||||
state.authenticated = false;
|
||||
state.user = undefined;
|
||||
})
|
||||
.addCase(login.pending, (state) => {
|
||||
state.state = 'loading';
|
||||
})
|
||||
.addCase(login.fulfilled, (state, action) => {
|
||||
state.state = "idle";
|
||||
state.authenticated = true;
|
||||
state.user = action.payload;
|
||||
})
|
||||
.addCase(login.rejected, (state) => {
|
||||
state.state = "idle";
|
||||
state.authenticated = false;
|
||||
state.user = undefined;
|
||||
})
|
||||
.addCase(register.pending, (state) => {
|
||||
state.state = "loading";
|
||||
})
|
||||
.addCase(register.fulfilled, (state, action) => {
|
||||
state.state = "idle";
|
||||
state.authenticated = true;
|
||||
state.user = action.payload;
|
||||
})
|
||||
.addCase(verify.pending, (state) => {
|
||||
state.state = 'loading';
|
||||
})
|
||||
.addCase(verify.fulfilled, (state, action) => {
|
||||
state.state = "idle";
|
||||
state.authenticated = true;
|
||||
state.user = action.payload;
|
||||
})
|
||||
.addCase(verify.rejected, (state) => {
|
||||
state.state = "idle";
|
||||
state.authenticated = false;
|
||||
state.user = undefined;
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export const { } = authSlice.actions;
|
||||
export default authSlice.reducer;
|
@ -0,0 +1,34 @@
|
||||
import { createApi, fetchBaseQuery} from "@reduxjs/toolkit/query/react";
|
||||
|
||||
export interface Card {
|
||||
id: number;
|
||||
front: string;
|
||||
back: string;
|
||||
hint?: string;
|
||||
deck_id: number;
|
||||
}
|
||||
export interface CardDeck {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string
|
||||
cards: Card[];
|
||||
}
|
||||
|
||||
export const cardDeckApiSlice = createApi({
|
||||
reducerPath: 'carddeck',
|
||||
baseQuery: fetchBaseQuery({
|
||||
baseUrl: '/api/v1/'
|
||||
}),
|
||||
endpoints(builder) {
|
||||
return {
|
||||
fetchCardDecks: builder.query<CardDeck[], string>({
|
||||
query: (_) => "carddeck"
|
||||
}),
|
||||
fetchCardDeckById: builder.query<CardDeck, number>({
|
||||
query: (id) => `carddeck/${id}`
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const { useFetchCardDecksQuery, useFetchCardDeckByIdQuery } = cardDeckApiSlice;
|
@ -1,15 +1,16 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import App from "./App";
|
||||
import { AuthUserProvider } from "./providers/AuthUser";
|
||||
import { Provider } from "react-redux";
|
||||
import { store } from "./app/store";
|
||||
import GlobalStyles from "./styles/GlobalStyles";
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<GlobalStyles />
|
||||
<AuthUserProvider>
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</AuthUserProvider>
|
||||
</Provider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root")
|
||||
);
|
||||
|
@ -0,0 +1 @@
|
||||
export type LoadingType = "idle" | "loading";
|
@ -1,6 +0,0 @@
|
||||
interface User {
|
||||
username: string,
|
||||
email: string,
|
||||
}
|
||||
|
||||
export default User
|
@ -0,0 +1,52 @@
|
||||
import React from "react";
|
||||
import WiredCard from "../../components/WiredCard/WiredCard";
|
||||
|
||||
import { DecksOuterWrapper, DecksInnerWrapper } from "./styled";
|
||||
|
||||
import Header from "../../components/Header/Header";
|
||||
import { useFetchCardDeckByIdQuery } from "../../features/cardDecks/cardDeck-api-slice";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
const Decks: React.FC = () => {
|
||||
const { id } = useParams();
|
||||
const { data, isFetching } = useFetchCardDeckByIdQuery(Number(id));
|
||||
return (
|
||||
<>
|
||||
<Header title={`Stapel${isFetching ? "" : `: ${data?.title}`}`} />
|
||||
<DecksOuterWrapper>
|
||||
{isFetching ? (
|
||||
<div>Loading</div>
|
||||
) : (
|
||||
<>
|
||||
<DecksInnerWrapper>
|
||||
<h2>{data?.description}</h2>
|
||||
<div className="flex">
|
||||
{data?.cards.map((card) => (
|
||||
<span key={card.id}>
|
||||
<WiredCard>
|
||||
<div className="deck">
|
||||
<div className="title">{card.front}</div>
|
||||
<div className="desc">{card.back}</div>
|
||||
</div>
|
||||
</WiredCard>
|
||||
</span>
|
||||
))}
|
||||
<Link to="new">
|
||||
<WiredCard elevation={4}>
|
||||
<div className="centered">
|
||||
<FontAwesomeIcon icon={faPlus} size="3x" />
|
||||
</div>
|
||||
</WiredCard>
|
||||
</Link>
|
||||
</div>
|
||||
</DecksInnerWrapper>
|
||||
</>
|
||||
)}
|
||||
</DecksOuterWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Decks;
|
@ -0,0 +1,46 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
export const DecksOuterWrapper = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding-top: calc(var(--toolbar-height));
|
||||
`;
|
||||
|
||||
export const DecksInnerWrapper = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
.flex {
|
||||
gap: 16px;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
gap: 16px;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
wired-card {
|
||||
overflow: hidden;
|
||||
width: var(--card-size);
|
||||
height: var(--card-size);
|
||||
.deck {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
.title {
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
.count {
|
||||
color: gray;
|
||||
}
|
||||
.centered {
|
||||
width: calc(var(--card-size) - 20px);
|
||||
height: calc(var(--card-size) - 20px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
`;
|
@ -0,0 +1,49 @@
|
||||
import React, { useState } from "react";
|
||||
import WiredCard from "../../components/WiredCard/WiredCard";
|
||||
|
||||
import { DecksOuterWrapper, DecksInnerWrapper } from "./styled";
|
||||
|
||||
import Header from "../../components/Header/Header";
|
||||
import { useFetchCardDecksQuery } from "../../features/cardDecks/cardDeck-api-slice";
|
||||
import { Link } from "react-router-dom";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useAppSelector } from "../../app/hooks";
|
||||
|
||||
const Decks: React.FC = () => {
|
||||
const username = useAppSelector((state) => state.auth.user?.username);
|
||||
const { data = [], isFetching } = useFetchCardDecksQuery(username ?? "");
|
||||
return (
|
||||
<>
|
||||
<Header title="Stapel" />
|
||||
<DecksOuterWrapper>
|
||||
{isFetching ? (
|
||||
<div>Loading</div>
|
||||
) : (
|
||||
<DecksInnerWrapper>
|
||||
{data.map((cardDeck) => (
|
||||
<Link key={cardDeck.id} to={`${cardDeck.id}`}>
|
||||
<WiredCard elevation={Math.min(cardDeck.cards.length, 4)}>
|
||||
<div className="deck">
|
||||
<div className="title">{cardDeck.title}</div>
|
||||
<div className="desc">{cardDeck.description}</div>
|
||||
<div className="count">Karten: {cardDeck.cards.length}</div>
|
||||
</div>
|
||||
</WiredCard>
|
||||
</Link>
|
||||
))}
|
||||
<Link to="new">
|
||||
<WiredCard elevation={4}>
|
||||
<div className="centered">
|
||||
<FontAwesomeIcon icon={faPlus} size="3x" />
|
||||
</div>
|
||||
</WiredCard>
|
||||
</Link>
|
||||
</DecksInnerWrapper>
|
||||
)}
|
||||
</DecksOuterWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Decks;
|
@ -0,0 +1,41 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
export const DecksOuterWrapper = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding-top: calc(var(--toolbar-height));
|
||||
`;
|
||||
|
||||
export const DecksInnerWrapper = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
wired-card {
|
||||
overflow: hidden;
|
||||
width: var(--card-size);
|
||||
height: var(--card-size);
|
||||
.deck {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
.title {
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
.count {
|
||||
color: gray;
|
||||
}
|
||||
.centered {
|
||||
width: calc(var(--card-size) - 20px);
|
||||
height: calc(var(--card-size) - 20px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
`;
|
@ -1,95 +0,0 @@
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import axios from "axios";
|
||||
import User from "../models/User";
|
||||
|
||||
interface IAuthUserContext {
|
||||
authenticated: boolean;
|
||||
user?: User | undefined;
|
||||
login: (user: AuthCredentials) => Promise<boolean>;
|
||||
logout: () => Promise<void>;
|
||||
register: (user: RegisterCredentials) => Promise<boolean>;
|
||||
}
|
||||
const AuthUserContext = createContext<null | IAuthUserContext>(null);
|
||||
|
||||
export interface AuthCredentials {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface RegisterCredentials extends AuthCredentials {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export const AuthUserProvider: React.FC = ({ children }) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [authenticated, setAuthenticated] = useState<boolean>(false);
|
||||
const [user, setUser] = useState<User>();
|
||||
const login = useCallback(async (user: AuthCredentials) => {
|
||||
try {
|
||||
const res = await axios.post<User>("/api/auth/login", user);
|
||||
setUser(res.data);
|
||||
setAuthenticated(true);
|
||||
return true;
|
||||
} catch {
|
||||
setAuthenticated(false);
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
const logout = useCallback(async () => {
|
||||
await axios.get("/api/auth/logout");
|
||||
setAuthenticated(false);
|
||||
setUser(undefined);
|
||||
}, []);
|
||||
|
||||
const register = useCallback(async (user: RegisterCredentials) => {
|
||||
try {
|
||||
const res = await axios.post<User>("/api/auth/register", user);
|
||||
setUser(res.data);
|
||||
setAuthenticated(true);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const verify = async () => {
|
||||
try {
|
||||
const res = await axios.get<User>("/api/auth/verify");
|
||||
setUser(res.data);
|
||||
setAuthenticated(true);
|
||||
} catch {
|
||||
setAuthenticated(false);
|
||||
setUser(undefined);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
verify();
|
||||
}, []);
|
||||
|
||||
return loading ? (
|
||||
<h1>Loading...</h1>
|
||||
) : (
|
||||
<AuthUserContext.Provider
|
||||
value={{ authenticated, user, login, logout, register }}
|
||||
>
|
||||
{children}
|
||||
</AuthUserContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuthUser = () => {
|
||||
const context = useContext(AuthUserContext);
|
||||
|
||||
if (!context)
|
||||
throw new Error("useAuthUser can only be used inside AuthContextProvider");
|
||||
|
||||
return context;
|
||||
};
|
Loading…
Reference in New Issue