diff --git a/package-lock.json b/package-lock.json index ed0179d..5cd4762 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "wokapp-ui", "version": "0.0.0", "dependencies": { + "axios": "^1.7.9", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^7.0.2" @@ -3309,6 +3310,12 @@ "dev": true, "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", @@ -3373,6 +3380,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.12", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz", @@ -3784,6 +3802,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -4084,6 +4114,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -4713,6 +4752,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -4753,6 +4812,20 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -6019,7 +6092,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -6032,7 +6104,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -6642,6 +6713,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index 35fd266..16a9257 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "preview": "vite preview" }, "dependencies": { + "axios": "^1.7.9", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^7.0.2" diff --git a/src/api.tsx b/src/api.tsx new file mode 100644 index 0000000..090bfc6 --- /dev/null +++ b/src/api.tsx @@ -0,0 +1,19 @@ +import axios from "axios"; + + +const api = axios.create({ + baseURL: "http://localhost:5079/api", + headers: { + 'Content-Type': 'application/json' + } +}); + +api.interceptors.request.use((config) => { + const token = localStorage.getItem('token'); + if (token) { + config.headers['Authorization'] = `Bearer ${token}`; + } + return config; +}, (err) => Promise.reject(err)); + +export default api; \ No newline at end of file diff --git a/src/components/UI/Button.tsx b/src/components/UI/Button.tsx index 9473126..5ece97e 100644 --- a/src/components/UI/Button.tsx +++ b/src/components/UI/Button.tsx @@ -2,13 +2,18 @@ import {FC, ReactNode} from "react"; interface ButtonProps { children: ReactNode | null; - onClick: () => void; + onClick?: () => void; className: string | ""; + disabled?: boolean; + type?: "submit"|"button"; } -const Button: FC = ({children, onClick, className}) => { +const Button: FC = ({children, onClick, className, disabled, type}) => { return <> - diff --git a/src/components/UI/HeaderMenu.tsx b/src/components/UI/HeaderMenu.tsx new file mode 100644 index 0000000..5b77fb7 --- /dev/null +++ b/src/components/UI/HeaderMenu.tsx @@ -0,0 +1,23 @@ +import {useAuth} from "../../context/AuthContext.tsx"; +import {useNavigate} from "react-router-dom"; + +const HeaderMenu = () => { + const auth = useAuth() + const navigate = useNavigate(); + function logout() { + auth.logout() + navigate('/'); + } + return <> +
+
+ Menu will be placed here +
+
+ Logout +
+
+ +} + +export default HeaderMenu; \ No newline at end of file diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index eb26d0c..33babfc 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -2,16 +2,21 @@ import React, { createContext, useState, useContext } from 'react'; const AuthContext = createContext({ isAuthenticated: false, - login: () => {}, + login: (_token: string) => {}, logout: () => {} }); export const AuthProvider= ({ children }: { children: React.ReactNode }) => { - const [isAuthenticated, setIsAuthenticated] = useState(false); - - const login = () => setIsAuthenticated(true); - const logout = () => setIsAuthenticated(false); + const [isAuthenticated, setIsAuthenticated] = useState(!!localStorage.getItem('token')); + const login = (token: string) => { + localStorage.setItem('token', token) + setIsAuthenticated(true); + } + const logout = () => { + localStorage.removeItem('token'); + setIsAuthenticated(false); + } return ( {children} diff --git a/src/routes/Home.tsx b/src/routes/Home.tsx index 7728724..4d8187e 100644 --- a/src/routes/Home.tsx +++ b/src/routes/Home.tsx @@ -1,5 +1,10 @@ +import HeaderMenu from "../components/UI/HeaderMenu.tsx"; + function Home() { - return <>

Home

+ return <> + +

Home

+ } export default Home; \ No newline at end of file diff --git a/src/routes/LoginPage.tsx b/src/routes/LoginPage.tsx index ea6bac6..7b0e754 100644 --- a/src/routes/LoginPage.tsx +++ b/src/routes/LoginPage.tsx @@ -1,11 +1,49 @@ import Input from "../components/UI/Input.tsx"; import Button from "../components/UI/Button.tsx"; -import {useState} from "react"; +import React, {useState} from "react"; +import api from "../api.tsx"; +import {useAuth} from "../context/AuthContext.tsx"; +import {useNavigate} from "react-router-dom"; export function LoginPage() { const [login,setLogin] = useState(''); const [password, setPassword] = useState(''); + const [showWrongUserNameAndPasswordError, setShowWrongUserNameAndPasswordError] = useState(false); + const [registerResultMessage, setRegisterResultMessage] = useState(''); + const auth = useAuth(); + const navigate = useNavigate(); + + function loginFormSubmit(event: React.FormEvent) { + event.preventDefault(); + api.post('user/login', { + username: login, + password: password, + }).then((response) => { + auth.login(response.data.token); + setShowWrongUserNameAndPasswordError(false); + navigate('/'); + }).catch((err) => { + if(err.status === 401) { + setShowWrongUserNameAndPasswordError(true); + } + }) + } + + function registerBtnClick() { + api.post('user/register', { + username: login, + password: password, + }).then((response) => { + if(response.status === 201) { + setRegisterResultMessage('User registered successfully. You can now log in.'); + setShowWrongUserNameAndPasswordError(false); + } + }).catch(() => { + setRegisterResultMessage('Something went wrong'); + }); + } + return <>
@@ -17,24 +55,44 @@ export function LoginPage() {
- setLogin(e.target.value) } - error={false} /> + { showWrongUserNameAndPasswordError && ( +
+ Wrong username or password +
+ )} + { registerResultMessage && ( +
+ {registerResultMessage} +
+ )} +
+ setLogin(e.target.value) } + error={false} /> + + setPassword(e.target.value) } /> + +

- or -

+ +
- setPassword(e.target.value) } /> - -

- or -

-