pull account

This commit is contained in:
Ruidy Nemausat 2020-05-06 21:56:01 +02:00
commit 58b43ec531
16 changed files with 11637 additions and 33 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View 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";

View file

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

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

View file

@ -73,7 +73,7 @@ const ProjectController: FC = () => {
setHasError(true);
setError("Bad Request");
}
}, [id]);
}, [id, getTokenSilently]);
if (hasError) {
return <ErrorController error={error} />;

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

View file

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

View file

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

View file

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

View file

@ -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> => {

View file

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

File diff suppressed because it is too large Load diff