introducing redux and fixing stuff

develop
Jasper Levin Spahl 3 years ago
parent c6bef04c73
commit 66a61d4cf9
Signed by: jasper
GPG Key ID: 91991C9808A18BB0

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preload" href="/src/fonts/font.ttf" as="font"/>
<link rel="load" href="/src/fonts/font.ttf" as="font"/>
<title>WokAble</title>
</head>
<body>

@ -12,8 +12,10 @@
"@fortawesome/free-regular-svg-icons": "^6.0.0",
"@fortawesome/free-solid-svg-icons": "^6.0.0",
"@fortawesome/react-fontawesome": "^0.1.17",
"@reduxjs/toolkit": "^1.8.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-redux": "^7.2.6",
"react-router-dom": "^6.2.2",
"styled-components": "^5.3.3",
"wired-elements-react": "^0.1.5"

@ -5,6 +5,7 @@ specifiers:
'@fortawesome/free-regular-svg-icons': ^6.0.0
'@fortawesome/free-solid-svg-icons': ^6.0.0
'@fortawesome/react-fontawesome': ^0.1.17
'@reduxjs/toolkit': ^1.8.0
'@types/react': ^17.0.33
'@types/react-dom': ^17.0.10
'@types/styled-components': ^5.1.24
@ -12,6 +13,7 @@ specifiers:
axios: ^0.26.1
react: ^17.0.2
react-dom: ^17.0.2
react-redux: ^7.2.6
react-router-dom: ^6.2.2
styled-components: ^5.3.3
typescript: ^4.5.4
@ -23,8 +25,10 @@ dependencies:
'@fortawesome/free-regular-svg-icons': 6.0.0
'@fortawesome/free-solid-svg-icons': 6.0.0
'@fortawesome/react-fontawesome': 0.1.17_4bd8f766d7cd56ce339ee1b51a510026
'@reduxjs/toolkit': 1.8.0_react-redux@7.2.6+react@17.0.2
react: 17.0.2
react-dom: 17.0.2_react@17.0.2
react-redux: 7.2.6_react-dom@17.0.2+react@17.0.2
react-router-dom: 6.2.2_react-dom@17.0.2+react@17.0.2
styled-components: 5.3.3_react-dom@17.0.2+react@17.0.2
wired-elements-react: 0.1.5
@ -402,6 +406,25 @@ packages:
resolution: {integrity: sha512-0TKSIuJHXNLM0k98fi0AdMIdUoHIYlDHTP+0Vruc2SOs4T6vU1FinXgSvYd8mSrkt+8R+qdRAXvjpqrMXMyBgw==}
dev: false
/@reduxjs/toolkit/1.8.0_react-redux@7.2.6+react@17.0.2:
resolution: {integrity: sha512-cdfHWfcvLyhBUDicoFwG1u32JqvwKDxLxDd7zSmSoFw/RhYLOygIRtmaMjPRUUHmVmmAGAvquLLsKKU/677kSQ==}
peerDependencies:
react: ^16.9.0 || ^17.0.0 || 18.0.0-beta
react-redux: ^7.2.1 || ^8.0.0-beta
peerDependenciesMeta:
react:
optional: true
react-redux:
optional: true
dependencies:
immer: 9.0.12
react: 17.0.2
react-redux: 7.2.6_react-dom@17.0.2+react@17.0.2
redux: 4.1.2
redux-thunk: 2.4.1_redux@4.1.2
reselect: 4.1.5
dev: false
/@rollup/pluginutils/4.2.0:
resolution: {integrity: sha512-2WUyJNRkyH5p487pGnn4tWAsxhEFKN/pT8CMgHshd5H+IXkOnKvKZwsz5ZWz+YCXkleZRAU5kwbfgF8CPfDRqA==}
engines: {node: '>= 8.0.0'}
@ -415,11 +438,9 @@ packages:
dependencies:
'@types/react': 17.0.40
hoist-non-react-statics: 3.3.2
dev: true
/@types/prop-types/15.7.4:
resolution: {integrity: sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==}
dev: true
/@types/react-dom/17.0.13:
resolution: {integrity: sha512-wEP+B8hzvy6ORDv1QBhcQia4j6ea4SFIBttHYpXKPFZRviBvknq0FRh3VrIxeXUmsPkwuXVZrVGG7KUVONmXCQ==}
@ -427,17 +448,24 @@ packages:
'@types/react': 17.0.40
dev: true
/@types/react-redux/7.1.23:
resolution: {integrity: sha512-D02o3FPfqQlfu2WeEYwh3x2otYd2Dk1o8wAfsA0B1C2AJEFxE663Ozu7JzuWbznGgW248NaOF6wsqCGNq9d3qw==}
dependencies:
'@types/hoist-non-react-statics': 3.3.1
'@types/react': 17.0.40
hoist-non-react-statics: 3.3.2
redux: 4.1.2
dev: false
/@types/react/17.0.40:
resolution: {integrity: sha512-UrXhD/JyLH+W70nNSufXqMZNuUD2cXHu6UjCllC6pmOQgBX4SGXOH8fjRka0O0Ee0HrFxapDD8Bwn81Kmiz6jQ==}
dependencies:
'@types/prop-types': 15.7.4
'@types/scheduler': 0.16.2
csstype: 3.0.11
dev: true
/@types/scheduler/0.16.2:
resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==}
dev: true
/@types/styled-components/5.1.24:
resolution: {integrity: sha512-mz0fzq2nez+Lq5IuYammYwWgyLUE6OMAJTQL9D8hFLP4Pkh7gVYJii/VQWxq8/TK34g/OrkehXaFNdcEKcItug==}
@ -555,7 +583,6 @@ packages:
/csstype/3.0.11:
resolution: {integrity: sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==}
dev: true
/debug/4.3.3:
resolution: {integrity: sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==}
@ -860,6 +887,10 @@ packages:
dependencies:
react-is: 16.13.1
/immer/9.0.12:
resolution: {integrity: sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==}
dev: false
/is-core-module/2.8.1:
resolution: {integrity: sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==}
dependencies:
@ -998,6 +1029,32 @@ packages:
/react-is/16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
/react-is/17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
dev: false
/react-redux/7.2.6_react-dom@17.0.2+react@17.0.2:
resolution: {integrity: sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ==}
peerDependencies:
react: ^16.8.3 || ^17
react-dom: '*'
react-native: '*'
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
dependencies:
'@babel/runtime': 7.17.2
'@types/react-redux': 7.1.23
hoist-non-react-statics: 3.3.2
loose-envify: 1.4.0
prop-types: 15.8.1
react: 17.0.2
react-dom: 17.0.2_react@17.0.2
react-is: 17.0.2
dev: false
/react-refresh/0.11.0:
resolution: {integrity: sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==}
engines: {node: '>=0.10.0'}
@ -1032,10 +1089,28 @@ packages:
object-assign: 4.1.1
dev: false
/redux-thunk/2.4.1_redux@4.1.2:
resolution: {integrity: sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==}
peerDependencies:
redux: ^4
dependencies:
redux: 4.1.2
dev: false
/redux/4.1.2:
resolution: {integrity: sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==}
dependencies:
'@babel/runtime': 7.17.2
dev: false
/regenerator-runtime/0.13.9:
resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==}
dev: false
/reselect/4.1.5:
resolution: {integrity: sha512-uVdlz8J7OO+ASpBYoz1Zypgx0KasCY20H+N8JD13oUMtPvSHQuscrHop4KbXrbsBcdB9Ds7lVK7eRkBIfO43vQ==}
dev: false
/resolve/1.22.0:
resolution: {integrity: sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==}
hasBin: true

@ -5,69 +5,103 @@ import {
Route,
Navigate,
} from "react-router-dom";
import { useAppDispatch, useAppSelector } from "./app/hooks";
import RequiresAuthentication from "./components/AuthenticatedComponent";
import AuthRedirect from "./components/AuthRedirect";
import { verify } from "./features/auth/auth-slice";
import { GlobalStyles } from "./styles/GlobalStyles";
const Login = React.lazy(() => import("./pages/login"));
const Register = React.lazy(() => import("./pages/register"));
const Learn = React.lazy(() => import("./pages/Learn/Learn"));
const Decks = React.lazy(() => import("./pages/Decks/Decks"));
const Deck = React.lazy(() => import("./pages/Deck/Deck"));
const BaseLayout = React.lazy(() => import("./layouts/Base/Base"));
const Loading = () => <p>Loading</p>;
const App: React.FC = () => {
const dispatch = useAppDispatch();
const state = useAppSelector((state) => state.auth.state);
useEffect(() => {
dispatch(verify());
}, []);
return (
<>
<GlobalStyles />
<Router>
<Routes>
<Route
path="login"
element={
<Suspense fallback={<Loading />}>
<Login />
</Suspense>
}
/>
<Route
path="register"
element={
<Suspense fallback={<Loading />}>
<Register />
</Suspense>
}
/>
<Route
path="app"
element={
<RequiresAuthentication>
<Suspense fallback={<Loading />}>
<BaseLayout />
</Suspense>
</RequiresAuthentication>
}
>
{state == "loading" ? (
<Loading />
) : (
<Router>
<Routes>
<Route
index
path="login"
element={
<Suspense fallback={<Loading />}>
<Learn />
</Suspense>
<AuthRedirect isAuthenticated={false} to="/app">
<Suspense fallback={<Loading />}>
<Login />
</Suspense>
</AuthRedirect>
}
/>
<Route
path="learn"
path="register"
element={
<Suspense fallback={<Loading />}>
<Learn />
</Suspense>
<AuthRedirect isAuthenticated={false} to="/app">
<Suspense fallback={<Loading />}>
<Register />
</Suspense>
</AuthRedirect>
}
/>
<Route path="*" element={<div>NotFound</div>} />
</Route>
<Route path="*" element={<Navigate to="app" replace />} />
</Routes>
</Router>
<Route
path="app"
element={
<AuthRedirect isAuthenticated={true} to="/login">
<Suspense fallback={<Loading />}>
<BaseLayout />
</Suspense>
</AuthRedirect>
}
>
<Route
index
element={
<Suspense fallback={<Loading />}>
<Learn />
</Suspense>
}
/>
<Route
path="learn"
element={
<Suspense fallback={<Loading />}>
<Learn />
</Suspense>
}
/>
<Route
path="decks"
element={
<Suspense fallback={<Loading />}>
<Decks />
</Suspense>
}
/>
<Route
path="decks/:id"
element={
<Suspense fallback={<Loading />}>
<Deck />
</Suspense>
}
/>
<Route path="decks/new" element={<h2>Not Implemented</h2>} />
<Route path="*" element={<div>NotFound</div>} />
</Route>
<Route path="*" element={<Navigate to="app" replace />} />
</Routes>
</Router>
)}
</>
);
};

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

@ -18,15 +18,17 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { Link, useNavigate } from "react-router-dom";
import { WiredDivider } from "wired-elements-react";
import { useAuthUser } from "../../providers/AuthUser";
import {useAppDispatch, useAppSelector} from "../../app/hooks";
import {logout} from "../../features/auth/auth-slice";
const Navbar = () => {
const { user, logout } = useAuthUser();
const user = useAppSelector(state => state.auth.user);
const dispatch = useAppDispatch();
const name = user?.username ?? "Fallback";
const navigate = useNavigate();
const handleLogout = async () => {
await logout();
const handleLogout = () => {
dispatch(logout());
navigate("/login");
};
return (

@ -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,9 +1,10 @@
import React, { useCallback, useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { useAuthUser } from "../providers/AuthUser";
import {useAppDispatch} from "../app/hooks";
import { login } from "../features/auth/auth-slice";
const Login: React.FC = () => {
const { login } = useAuthUser();
const dispatch = useAppDispatch();
const navigate = useNavigate();
const [username, setUsername] = useState("");
@ -12,9 +13,7 @@ const Login: React.FC = () => {
console.log("handeling login");
console.log(username);
console.log(password);
login({ username, password }).then((res) => {
res ? navigate("/app") : alert("Someting went wrong");
});
dispatch(login({ username, password }));
};
return (
<div>

@ -1,9 +1,10 @@
import React, { useCallback, useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { useAuthUser } from "../providers/AuthUser";
import { useAppDispatch } from "../app/hooks";
import { register } from "../features/auth/auth-slice";
const Register: React.FC = () => {
const { register } = useAuthUser();
const dispatch = useAppDispatch();
const navigate = useNavigate();
const [username, setUsername] = useState("");
@ -16,9 +17,7 @@ const Register: React.FC = () => {
return;
}
register({ email, username, password }).then((res) => {
res ? navigate("/app") : alert("Someting went wrong");
});
dispatch(register({ email, username, password }));
};
return (
<div>

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

@ -16,6 +16,7 @@ export const GlobalStyles = createGlobalStyle`
width: 100%;
height: 100%;
max-height: calc(100% + 0px);
overflow:hidden;
}
#root {
width: 100%;
@ -68,4 +69,4 @@ export const GlobalStyles = createGlobalStyle`
}
`;
export default GlobalStyles;
export default GlobalStyles;

@ -11,6 +11,7 @@ const Variables = css`
--background: white;
--foreground: black;
--card-size: 300px;
}
`;

@ -6,8 +6,8 @@ import (
)
func UserScope(c *gin.Context) func(db *gorm.DB) *gorm.DB {
userId := int(c.GetFloat64("user_id"))
return func(db *gorm.DB) *gorm.DB {
userId := c.GetUint("user_id")
return db.Where("user_id", userId)
}
}

@ -26,16 +26,17 @@ func getCardById(c *gin.Context) {
}
func createCard(c *gin.Context) {
var card models.Card
var card models.CardDto
if err := c.BindJSON(&card); err != nil {
return
}
card.UserID = c.GetUint("user_id")
cardDbo := card.ToDbo()
cardDbo.UserID = uint(c.GetFloat64("user_id"))
if models.DB.Create(&card).Save(&card).Error != nil {
if models.DB.Create(&cardDbo).Save(&cardDbo).Error != nil {
return
}
c.IndentedJSON(http.StatusCreated, card.ToDto())
c.IndentedJSON(http.StatusCreated, cardDbo.ToDto())
}
func updateCard(c *gin.Context) {

@ -40,7 +40,7 @@ func createCardDeck(c *gin.Context) {
if err := c.BindJSON(&cardDeck); err != nil {
return
}
cardDeck.UserID = c.GetUint("user_id")
cardDeck.UserID = uint(c.GetFloat64("user_id"))
if models.DB.Scopes(auth.UserScope(c)).Create(&cardDeck).Save(&cardDeck).Error != nil {
return

@ -31,6 +31,9 @@ type CardDto struct {
func (c Card) ToDto() CardDto {
return CardDto{c.ID, c.Front, c.Back, c.Hint, c.CardDeckID}
}
func (c CardDto) ToDbo() Card {
return Card{Front: c.Front, Back: c.Back, Hint: c.Hint, CardDeckID: c.CardDeckID}
}
func (c *Card) BeforeCreate(tx *gorm.DB) (err error) {
c.PhaseID = phaseOneID

Loading…
Cancel
Save