introducing redux and fixing stuff
This commit is contained in:
@@ -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"
|
||||
|
||||
85
assets/pnpm-lock.yaml
generated
85
assets/pnpm-lock.yaml
generated
@@ -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,46 +5,62 @@ 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 />
|
||||
{state == "loading" ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route
|
||||
path="login"
|
||||
element={
|
||||
<AuthRedirect isAuthenticated={false} to="/app">
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Login />
|
||||
</Suspense>
|
||||
</AuthRedirect>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="register"
|
||||
element={
|
||||
<AuthRedirect isAuthenticated={false} to="/app">
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Register />
|
||||
</Suspense>
|
||||
</AuthRedirect>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="app"
|
||||
element={
|
||||
<RequiresAuthentication>
|
||||
<AuthRedirect isAuthenticated={true} to="/login">
|
||||
<Suspense fallback={<Loading />}>
|
||||
<BaseLayout />
|
||||
</Suspense>
|
||||
</RequiresAuthentication>
|
||||
</AuthRedirect>
|
||||
}
|
||||
>
|
||||
<Route
|
||||
@@ -63,11 +79,29 @@ const App: React.FC = () => {
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
6
assets/src/app/hooks.ts
Normal file
6
assets/src/app/hooks.ts
Normal file
@@ -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;
|
||||
17
assets/src/app/store.ts
Normal file
17
assets/src/app/store.ts
Normal file
@@ -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>;
|
||||
19
assets/src/components/AuthRedirect.tsx
Normal file
19
assets/src/components/AuthRedirect.tsx
Normal file
@@ -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;
|
||||
|
||||
5
assets/src/components/WiredCard/styled.tsx
Normal file
5
assets/src/components/WiredCard/styled.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
export const CardWrapper = styled.div`
|
||||
padding: 10px;
|
||||
`;
|
||||
100
assets/src/features/auth/auth-slice.ts
Normal file
100
assets/src/features/auth/auth-slice.ts
Normal file
@@ -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;
|
||||
34
assets/src/features/cardDecks/cardDeck-api-slice.ts
Normal file
34
assets/src/features/cardDecks/cardDeck-api-slice.ts
Normal file
@@ -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")
|
||||
);
|
||||
|
||||
1
assets/src/models/Types.ts
Normal file
1
assets/src/models/Types.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type LoadingType = "idle" | "loading";
|
||||
@@ -1,6 +0,0 @@
|
||||
interface User {
|
||||
username: string,
|
||||
email: string,
|
||||
}
|
||||
|
||||
export default User
|
||||
52
assets/src/pages/Deck/Deck.tsx
Normal file
52
assets/src/pages/Deck/Deck.tsx
Normal file
@@ -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;
|
||||
46
assets/src/pages/Deck/styled.tsx
Normal file
46
assets/src/pages/Deck/styled.tsx
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
49
assets/src/pages/Decks/Decks.tsx
Normal file
49
assets/src/pages/Decks/Decks.tsx
Normal file
@@ -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;
|
||||
41
assets/src/pages/Decks/styled.tsx
Normal file
41
assets/src/pages/Decks/styled.tsx
Normal file
@@ -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%;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user