mirror of
https://github.com/rjNemo/ticket_manager
synced 2026-06-06 00:36:39 +00:00
pull account
This commit is contained in:
commit
58b43ec531
16 changed files with 11637 additions and 33 deletions
|
|
@ -8,9 +8,11 @@ using TicketManager.Data;
|
|||
using TicketManager.Resources;
|
||||
using TicketManager.Models;
|
||||
|
||||
|
||||
namespace TicketManager.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
[Produces("application/json")]
|
||||
[Route("api/v1/[controller]")]
|
||||
[ApiController]
|
||||
public class TicketsController : ControllerBase
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
"@auth0/auth0-spa-js": "^1.6.4",
|
||||
"@material-ui/core": "^4.9.13",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "^4.0.0-alpha.47",
|
||||
"@material-ui/lab": "^4.0.0-alpha.52",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.4.0",
|
||||
"@testing-library/user-event": "^7.2.1",
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-scripts": "3.3.1",
|
||||
"react-scripts": "^3.4.1",
|
||||
"react-swipeable-views": "^0.13.9",
|
||||
"typescript": "^3.8.3",
|
||||
"underscore": "^1.9.2"
|
||||
|
|
@ -48,4 +48,4 @@
|
|||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
AppBar,
|
||||
Button,
|
||||
IconButton,
|
||||
// IconButton,
|
||||
Toolbar,
|
||||
Typography,
|
||||
Avatar,
|
||||
List,
|
||||
ListItem,
|
||||
Popover,
|
||||
} from "@material-ui/core";
|
||||
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
|
||||
import MenuIcon from "@material-ui/icons/Menu";
|
||||
// import MenuIcon from "@material-ui/icons/Menu";
|
||||
import BugReportIcon from "@material-ui/icons/BugReport";
|
||||
import * as ROUTES from "../constants/routes";
|
||||
import { useAuth0 } from "../authentication/auth0";
|
||||
|
|
@ -26,6 +29,9 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||
title: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
typography: {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
|
|
@ -33,18 +39,28 @@ export default function ButtonAppBar() {
|
|||
const classes = useStyles();
|
||||
const { isAuthenticated, loginWithRedirect, logout, user } = useAuth0();
|
||||
|
||||
const [anchor, setAnchor] = useState<HTMLButtonElement | null>(null);
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) =>
|
||||
setAnchor(e.currentTarget);
|
||||
|
||||
const handleClose = () => setAnchor(null);
|
||||
|
||||
const open: boolean = !!anchor;
|
||||
const id = open ? "profile-popover" : undefined;
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
{/* <IconButton
|
||||
edge="start"
|
||||
className={classes.menuButton}
|
||||
color="inherit"
|
||||
aria-label="menu"
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
</IconButton> */}
|
||||
<Typography variant="h6" className={classes.title}>
|
||||
<Button
|
||||
color="inherit"
|
||||
|
|
@ -66,15 +82,54 @@ export default function ButtonAppBar() {
|
|||
) : (
|
||||
<>
|
||||
<Button
|
||||
color="inherit"
|
||||
component={Link}
|
||||
to={`${ROUTES.USERS}/${getUID(user)}`}
|
||||
aria-describedby={id}
|
||||
color="primary"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Avatar src={user.picture} />
|
||||
</Button>
|
||||
<Button color="inherit" onClick={() => logout()}>
|
||||
Log out
|
||||
</Button>
|
||||
<Popover
|
||||
id={id}
|
||||
open={open}
|
||||
anchorEl={anchor}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "right",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
>
|
||||
<List>
|
||||
<ListItem>
|
||||
<Button
|
||||
color="inherit"
|
||||
component={Link}
|
||||
to={`${ROUTES.USERS}/${getUID(user)}`}
|
||||
onClick={handleClose}
|
||||
>
|
||||
Profile
|
||||
</Button>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Button
|
||||
color="inherit"
|
||||
component={Link}
|
||||
to={ROUTES.ACCOUNT}
|
||||
onClick={handleClose}
|
||||
>
|
||||
Edit Profile
|
||||
</Button>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Button color="inherit" onClick={() => logout()}>
|
||||
Log out
|
||||
</Button>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Popover>
|
||||
</>
|
||||
)}
|
||||
</Toolbar>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,43 @@
|
|||
import React, { FC } from "react";
|
||||
import { TextField } from "@material-ui/core";
|
||||
|
||||
interface IProps {
|
||||
label: string;
|
||||
type?: string;
|
||||
state: string;
|
||||
className?: string;
|
||||
multiline?: boolean;
|
||||
setState: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
const InputField: FC<IProps> = ({
|
||||
label,
|
||||
type = "text",
|
||||
multiline = false,
|
||||
state,
|
||||
setState,
|
||||
className,
|
||||
}) => {
|
||||
// update text after user input
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
e.preventDefault();
|
||||
setState(e.target.value);
|
||||
};
|
||||
|
||||
const InputField: FC = () => {
|
||||
return (
|
||||
<div className="input-field">
|
||||
<input id="email" type="text" className="validate" />
|
||||
<label htmlFor="email">Email</label>
|
||||
</div>
|
||||
<TextField
|
||||
label={label}
|
||||
value={state}
|
||||
onChange={handleChange}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
type={type}
|
||||
multiline={multiline}
|
||||
size="small"
|
||||
// autoFocus
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { FC } from "react";
|
||||
import InputField from "./InputField";
|
||||
|
||||
import PasswordField from "./PasswordField";
|
||||
import Button from "./Buttons/Button";
|
||||
|
||||
|
|
@ -10,7 +10,8 @@ export const LogInForm: FC = () => {
|
|||
<div className="center ">
|
||||
<h4>Login</h4>
|
||||
<form className="col s10 offset-s1">
|
||||
<InputField />
|
||||
<></>
|
||||
{/* <InputField /> */}
|
||||
<PasswordField />
|
||||
<Button color="indigo" size="large">
|
||||
Submit
|
||||
|
|
|
|||
5
client/src/constants/api.ts
Normal file
5
client/src/constants/api.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
const API: string = "/api/v1";
|
||||
|
||||
export const PROJECTS: string = API + "/projects";
|
||||
export const TICKETS: string = API + "/tickets";
|
||||
export const USERS: string = API + "/users";
|
||||
|
|
@ -3,5 +3,6 @@ export const PROJECTS: string = "/projects";
|
|||
export const TICKETS: string = "/tickets";
|
||||
export const USERS: string = "/users";
|
||||
export const SIGN_IN: string = "/signin";
|
||||
export const ACCOUNT: string = "/account";
|
||||
export const NOT_FOUND: string = "/404";
|
||||
export const TEST: string = "/test";
|
||||
|
|
|
|||
40
client/src/controllers/AccountController.tsx
Normal file
40
client/src/controllers/AccountController.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import React, { FC, useState, useEffect, useRef } from "react";
|
||||
import AccountPage from "../pages/AccountPage";
|
||||
import Preloader from "../components/Preloader";
|
||||
import User from "../types/User";
|
||||
import { UserService } from "../services";
|
||||
import { useAuth0 } from "../authentication/auth0";
|
||||
import { getUID } from "../authentication/helpers";
|
||||
|
||||
const AccountController: FC = () => {
|
||||
const { getTokenSilently, user } = useAuth0();
|
||||
const [account, setAccount] = useState<User>();
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
const token = useRef<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
const getUserInfo = async () => {
|
||||
// fetch current user data
|
||||
token.current = await getTokenSilently();
|
||||
const Users = new UserService(token.current);
|
||||
const uid: string = getUID(user);
|
||||
|
||||
Users.get(uid)
|
||||
.then((authUser) => setAccount(authUser))
|
||||
.catch((err) => console.error(err));
|
||||
};
|
||||
|
||||
getUserInfo().then(() => setLoading(false));
|
||||
}, [getTokenSilently, user]);
|
||||
|
||||
return loading ? (
|
||||
// display preloader until data is fetched
|
||||
<Preloader />
|
||||
) : // don't render page until data is fetched
|
||||
!!account ? (
|
||||
<AccountPage account={account} token={token.current} />
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default AccountController;
|
||||
|
|
@ -73,7 +73,7 @@ const ProjectController: FC = () => {
|
|||
setHasError(true);
|
||||
setError("Bad Request");
|
||||
}
|
||||
}, [id]);
|
||||
}, [id, getTokenSilently]);
|
||||
|
||||
if (hasError) {
|
||||
return <ErrorController error={error} />;
|
||||
|
|
|
|||
117
client/src/pages/AccountPage.tsx
Normal file
117
client/src/pages/AccountPage.tsx
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import React, { FC, useState } from "react";
|
||||
import { Button, Snackbar } from "@material-ui/core";
|
||||
import { makeStyles, createStyles } from "@material-ui/core/styles";
|
||||
import Alert from "@material-ui/lab/Alert";
|
||||
import User from "../types/User";
|
||||
import InputField from "../components/InputField";
|
||||
import PageLayout from "../layouts/PageLayout";
|
||||
import UserHeader from "../components/UserHeader";
|
||||
import { UserService } from "../services";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
input: {
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
interface IProps {
|
||||
account: User;
|
||||
token: string;
|
||||
}
|
||||
|
||||
const AccountPage: FC<IProps> = ({ account, token }) => {
|
||||
const classes = useStyles();
|
||||
|
||||
const [firstName, setFirstname] = useState(account.firstName);
|
||||
const [lastName, setLastname] = useState(account.lastName);
|
||||
const [presentation, setPresentation] = useState(account.presentation);
|
||||
const [phone, setPhone] = useState(account.phone);
|
||||
|
||||
// user should at least have a name
|
||||
const isDisabled = firstName === "" && lastName === "";
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
// prevent default button behaviour
|
||||
e.preventDefault();
|
||||
// send data to API
|
||||
const Users = new UserService(token);
|
||||
const newUser: User = {
|
||||
id: account.id,
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
fullName: `${firstName} ${lastName}`,
|
||||
email: account.email,
|
||||
presentation: presentation,
|
||||
picture: account.picture,
|
||||
phone,
|
||||
creationDate: Date.now().toLocaleString(),
|
||||
activities: account.activities,
|
||||
projects: account.projects,
|
||||
tickets: account.tickets,
|
||||
};
|
||||
Users.update(account.id, newUser)
|
||||
.then(() => console.log("ok"))
|
||||
.catch((err) => console.error(err));
|
||||
// reinitialize inputfiled
|
||||
// setText("");
|
||||
};
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
header={
|
||||
<UserHeader
|
||||
picture={account.picture}
|
||||
fullName={account.fullName}
|
||||
presentation={account.presentation}
|
||||
/>
|
||||
}
|
||||
content={
|
||||
<form onSubmit={handleSubmit}>
|
||||
<InputField
|
||||
label="First Name"
|
||||
state={firstName}
|
||||
setState={setFirstname}
|
||||
className={classes.input}
|
||||
/>
|
||||
<InputField
|
||||
label="Last Name"
|
||||
state={lastName}
|
||||
setState={setLastname}
|
||||
className={classes.input}
|
||||
/>
|
||||
<InputField
|
||||
label="Presentation"
|
||||
state={presentation}
|
||||
setState={setPresentation}
|
||||
multiline
|
||||
className={classes.input}
|
||||
/>
|
||||
<InputField
|
||||
label="Phone"
|
||||
state={phone}
|
||||
setState={setPhone}
|
||||
className={classes.input}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={isDisabled}
|
||||
>
|
||||
Update Info
|
||||
</Button>
|
||||
<Snackbar open={isDisabled} autoHideDuration={6000}>
|
||||
<Alert severity="warning">
|
||||
User should have at least a first or last name!
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</form>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountPage;
|
||||
|
|
@ -5,9 +5,9 @@ import HomeController from "../controllers/HomeController";
|
|||
import ProjectController from "../controllers/ProjectController";
|
||||
import UserController from "../controllers/UserController";
|
||||
import TicketController from "../controllers/TicketController";
|
||||
import AccountController from "../controllers/AccountController";
|
||||
import NotFoundPage from "../pages/NotFoundPage";
|
||||
import TestPage from "../pages/TestPage";
|
||||
// import SigninPage from "../pages/SigninPage";
|
||||
import * as ROUTES from "../constants/routes";
|
||||
|
||||
const AppRouter = () => {
|
||||
|
|
@ -15,7 +15,7 @@ const AppRouter = () => {
|
|||
<Switch>
|
||||
<PrivateRoute path={ROUTES.TEST} component={TestPage} />
|
||||
<Route exact path={ROUTES.HOME} component={HomeController} />
|
||||
{/* <Route path={ROUTES.SIGN_IN} component={SigninPage} /> */}
|
||||
<PrivateRoute path={ROUTES.ACCOUNT} component={AccountController} />
|
||||
<PrivateRoute
|
||||
path={`${ROUTES.PROJECTS}/:id`}
|
||||
component={ProjectController}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import IService from ".";
|
||||
import Project from "../types/Project";
|
||||
import HttpHandler from "./http";
|
||||
import * as API from "../constants/api";
|
||||
|
||||
interface NewProject {
|
||||
title: string;
|
||||
|
|
@ -12,7 +13,7 @@ export default class ProjectService implements IService<Project> {
|
|||
constructor(private key: string) {}
|
||||
|
||||
private http = new HttpHandler<Project>();
|
||||
private path: string = "/api/v1/projects";
|
||||
private path: string = API.PROJECTS;
|
||||
|
||||
all = async (): Promise<Project[]> => {
|
||||
const response = await this.http.get(this.path, this.key);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import IService from ".";
|
||||
import Ticket from "../types/Ticket";
|
||||
import HttpHandler from "./http";
|
||||
import * as API from "../constants/api";
|
||||
|
||||
interface NewTicket {
|
||||
title: string;
|
||||
|
|
@ -17,7 +18,7 @@ export default class TicketService implements IService<Ticket> {
|
|||
constructor(private key: string) {}
|
||||
|
||||
private http = new HttpHandler<Ticket>();
|
||||
private path: string = "/api/v1/tickets";
|
||||
private path: string = API.TICKETS;
|
||||
|
||||
all = async (): Promise<Ticket[]> => {
|
||||
const response = await this.http.get(this.path, this.key);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import IService from ".";
|
||||
import User from "../types/User";
|
||||
import HttpHandler from "./http";
|
||||
import * as API from "../constants/api";
|
||||
|
||||
export default class UserService implements IService<User> {
|
||||
constructor(private key: string) {}
|
||||
|
||||
private http = new HttpHandler<User>();
|
||||
private path: string = "/api/v1/users";
|
||||
private path: string = API.USERS;
|
||||
|
||||
all = async (): Promise<User[]> => {
|
||||
const response = await this.http.get(this.path, this.key);
|
||||
|
|
@ -26,7 +27,10 @@ export default class UserService implements IService<User> {
|
|||
};
|
||||
|
||||
update = async (id: string, item: User): Promise<void> => {
|
||||
throw new Error("Method not implemented.");
|
||||
// const response =
|
||||
await this.http.put(`${this.path}/${id}`, item, this.key);
|
||||
// const body = response.parsedBody;
|
||||
// return body ?? ({} as User);
|
||||
};
|
||||
|
||||
delete = async (id: string): Promise<void> => {
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
export default class Constants {
|
||||
static projectsURI: string = "/api/v1/projects";
|
||||
static ticketsURI: string = "/api/v1/tickets";
|
||||
static usersURI: string = "/api/v1/users";
|
||||
}
|
||||
11350
client/yarn.lock
Normal file
11350
client/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue