diff --git a/assets/index.html b/assets/index.html index 3a47835..d388569 100644 --- a/assets/index.html +++ b/assets/index.html @@ -4,7 +4,7 @@ - + WokAble diff --git a/assets/package.json b/assets/package.json index 9210770..5247300 100644 --- a/assets/package.json +++ b/assets/package.json @@ -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" diff --git a/assets/pnpm-lock.yaml b/assets/pnpm-lock.yaml index 6826db3..71da03b 100644 --- a/assets/pnpm-lock.yaml +++ b/assets/pnpm-lock.yaml @@ -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 diff --git a/assets/src/App.tsx b/assets/src/App.tsx index 4e4c14d..cc73c82 100644 --- a/assets/src/App.tsx +++ b/assets/src/App.tsx @@ -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 = () =>

Loading

; const App: React.FC = () => { + const dispatch = useAppDispatch(); + const state = useAppSelector((state) => state.auth.state); + useEffect(() => { + dispatch(verify()); + }, []); return ( <> - - - }> - - - } - /> - }> - - - } - /> - - }> - - - - } - > + {state == "loading" ? ( + + ) : ( + + }> - - + + }> + + + } /> }> - - + + }> + + + } /> - NotFound} /> - - } /> - - + + }> + + + + } + > + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + Not Implemented} /> + NotFound} /> + + } /> + + + )} ); }; diff --git a/assets/src/app/hooks.ts b/assets/src/app/hooks.ts new file mode 100644 index 0000000..4864148 --- /dev/null +++ b/assets/src/app/hooks.ts @@ -0,0 +1,6 @@ +import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; +import {AppDispatch, RootState} from "./store"; + +export const useAppDispatch = () => useDispatch(); + +export const useAppSelector: TypedUseSelectorHook = useSelector; diff --git a/assets/src/app/store.ts b/assets/src/app/store.ts new file mode 100644 index 0000000..03ae4bf --- /dev/null +++ b/assets/src/app/store.ts @@ -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; diff --git a/assets/src/components/AuthRedirect.tsx b/assets/src/components/AuthRedirect.tsx new file mode 100644 index 0000000..1a04972 --- /dev/null +++ b/assets/src/components/AuthRedirect.tsx @@ -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 = ({ children , isAuthenticated, to}) => { + const authenticated = useAppSelector(state => state.auth.authenticated); + + const { pathname } = useLocation(); + + if (authenticated == isAuthenticated) return (<>{children}); + return (); +}; + +export default AuthRedirect; diff --git a/assets/src/components/AuthenticatedComponent.tsx b/assets/src/components/AuthenticatedComponent.tsx deleted file mode 100644 index 98c2808..0000000 --- a/assets/src/components/AuthenticatedComponent.tsx +++ /dev/null @@ -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 ; -}; - -export default AuthenticatedComponent; diff --git a/assets/src/components/Navbar/Navbar.tsx b/assets/src/components/Navbar/Navbar.tsx index be57c26..c994757 100644 --- a/assets/src/components/Navbar/Navbar.tsx +++ b/assets/src/components/Navbar/Navbar.tsx @@ -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 ( diff --git a/assets/src/components/WiredCard/WiredCard.tsx b/assets/src/components/WiredCard/WiredCard.tsx index b3da691..c415719 100644 --- a/assets/src/components/WiredCard/WiredCard.tsx +++ b/assets/src/components/WiredCard/WiredCard.tsx @@ -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 {childrenWithType}; +interface IWiredCard { + elevation?: number; +} +const WiredCard: React.FC = ({ children, elevation}) => { + var childrenWithType = ({children}) as HTMLCollection & React.ReactNode; + return {childrenWithType}; }; export default WiredCard; diff --git a/assets/src/components/WiredCard/styled.tsx b/assets/src/components/WiredCard/styled.tsx new file mode 100644 index 0000000..e2d70d7 --- /dev/null +++ b/assets/src/components/WiredCard/styled.tsx @@ -0,0 +1,5 @@ +import styled from "styled-components"; + +export const CardWrapper = styled.div` + padding: 10px; +`; diff --git a/assets/src/features/auth/auth-slice.ts b/assets/src/features/auth/auth-slice.ts new file mode 100644 index 0000000..e908048 --- /dev/null +++ b/assets/src/features/auth/auth-slice.ts @@ -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("/api/auth/login", credentials); + return responce.data; +}) + +export const register = createAsyncThunk("auth/register", async (credentials: RegisterCredentials) => { + const responce = await axios.post("/api/auth/register", credentials); + return responce.data; +}) + +export const verify = createAsyncThunk("auth/verify", async () => { + const responce = await axios.get("/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; diff --git a/assets/src/features/cardDecks/cardDeck-api-slice.ts b/assets/src/features/cardDecks/cardDeck-api-slice.ts new file mode 100644 index 0000000..571ea08 --- /dev/null +++ b/assets/src/features/cardDecks/cardDeck-api-slice.ts @@ -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({ + query: (_) => "carddeck" + }), + fetchCardDeckById: builder.query({ + query: (id) => `carddeck/${id}` + }) + } + } +}) + +export const { useFetchCardDecksQuery, useFetchCardDeckByIdQuery } = cardDeckApiSlice; diff --git a/assets/src/main.tsx b/assets/src/main.tsx index a63b5ea..cb19a5f 100644 --- a/assets/src/main.tsx +++ b/assets/src/main.tsx @@ -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( - + - + , document.getElementById("root") ); diff --git a/assets/src/models/Types.ts b/assets/src/models/Types.ts new file mode 100644 index 0000000..988d0e0 --- /dev/null +++ b/assets/src/models/Types.ts @@ -0,0 +1 @@ +export type LoadingType = "idle" | "loading"; diff --git a/assets/src/models/User.ts b/assets/src/models/User.ts deleted file mode 100644 index fe125a5..0000000 --- a/assets/src/models/User.ts +++ /dev/null @@ -1,6 +0,0 @@ -interface User { - username: string, - email: string, -} - -export default User \ No newline at end of file diff --git a/assets/src/pages/Deck/Deck.tsx b/assets/src/pages/Deck/Deck.tsx new file mode 100644 index 0000000..d7934f2 --- /dev/null +++ b/assets/src/pages/Deck/Deck.tsx @@ -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 ( + <> +
+ + {isFetching ? ( +
Loading
+ ) : ( + <> + +

{data?.description}

+
+ {data?.cards.map((card) => ( + + +
+
{card.front}
+
{card.back}
+
+
+
+ ))} + + +
+ +
+
+ +
+
+ + )} +
+ + ); +}; + +export default Decks; diff --git a/assets/src/pages/Deck/styled.tsx b/assets/src/pages/Deck/styled.tsx new file mode 100644 index 0000000..9e803ae --- /dev/null +++ b/assets/src/pages/Deck/styled.tsx @@ -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; + } + } +`; diff --git a/assets/src/pages/Decks/Decks.tsx b/assets/src/pages/Decks/Decks.tsx new file mode 100644 index 0000000..6ba4f5e --- /dev/null +++ b/assets/src/pages/Decks/Decks.tsx @@ -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 ( + <> +
+ + {isFetching ? ( +
Loading
+ ) : ( + + {data.map((cardDeck) => ( + + +
+
{cardDeck.title}
+
{cardDeck.description}
+
Karten: {cardDeck.cards.length}
+
+
+ + ))} + + +
+ +
+
+ +
+ )} +
+ + ); +}; + +export default Decks; diff --git a/assets/src/pages/Decks/styled.tsx b/assets/src/pages/Decks/styled.tsx new file mode 100644 index 0000000..ecb4cf1 --- /dev/null +++ b/assets/src/pages/Decks/styled.tsx @@ -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; + } + } +`; diff --git a/assets/src/pages/login.tsx b/assets/src/pages/login.tsx index cebae4f..3549d9b 100644 --- a/assets/src/pages/login.tsx +++ b/assets/src/pages/login.tsx @@ -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 (
diff --git a/assets/src/pages/register.tsx b/assets/src/pages/register.tsx index ff2d316..0ead1d0 100644 --- a/assets/src/pages/register.tsx +++ b/assets/src/pages/register.tsx @@ -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 (
diff --git a/assets/src/providers/AuthUser.tsx b/assets/src/providers/AuthUser.tsx deleted file mode 100644 index 896372b..0000000 --- a/assets/src/providers/AuthUser.tsx +++ /dev/null @@ -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; - logout: () => Promise; - register: (user: RegisterCredentials) => Promise; -} -const AuthUserContext = createContext(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(false); - const [user, setUser] = useState(); - const login = useCallback(async (user: AuthCredentials) => { - try { - const res = await axios.post("/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("/api/auth/register", user); - setUser(res.data); - setAuthenticated(true); - return true; - } catch { - return false; - } - }, []); - - useEffect(() => { - const verify = async () => { - try { - const res = await axios.get("/api/auth/verify"); - setUser(res.data); - setAuthenticated(true); - } catch { - setAuthenticated(false); - setUser(undefined); - } finally { - setLoading(false); - } - }; - verify(); - }, []); - - return loading ? ( -

Loading...

- ) : ( - - {children} - - ); -}; - -export const useAuthUser = () => { - const context = useContext(AuthUserContext); - - if (!context) - throw new Error("useAuthUser can only be used inside AuthContextProvider"); - - return context; -}; diff --git a/assets/src/styles/GlobalStyles.ts b/assets/src/styles/GlobalStyles.ts index 5e1f346..353e491 100644 --- a/assets/src/styles/GlobalStyles.ts +++ b/assets/src/styles/GlobalStyles.ts @@ -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; \ No newline at end of file +export default GlobalStyles; diff --git a/assets/src/styles/Variables.ts b/assets/src/styles/Variables.ts index f495df0..7a5666d 100644 --- a/assets/src/styles/Variables.ts +++ b/assets/src/styles/Variables.ts @@ -11,6 +11,7 @@ const Variables = css` --background: white; --foreground: black; + --card-size: 300px; } `; diff --git a/auth/userscope.go b/auth/userscope.go index 34532b9..a1f168a 100644 --- a/auth/userscope.go +++ b/auth/userscope.go @@ -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) } } diff --git a/controllers/v1/card/card.controller.go b/controllers/v1/card/card.controller.go index 38db7e9..94db5cb 100644 --- a/controllers/v1/card/card.controller.go +++ b/controllers/v1/card/card.controller.go @@ -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) { diff --git a/controllers/v1/carddeck/carddeck.controller.go b/controllers/v1/carddeck/carddeck.controller.go index 05b5b98..ea77a2e 100644 --- a/controllers/v1/carddeck/carddeck.controller.go +++ b/controllers/v1/carddeck/carddeck.controller.go @@ -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 diff --git a/models/card.go b/models/card.go index 418cf89..4e69160 100644 --- a/models/card.go +++ b/models/card.go @@ -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