merge latest react branch

This commit is contained in:
Ruidy Nemausat 2020-02-24 10:29:03 +01:00
commit 5b57423d68
48 changed files with 939 additions and 374 deletions

2
.gitignore vendored
View file

@ -7,4 +7,4 @@ app.db*
.DS_Store .DS_Store
app.db app.db
client/node_modules client/node_modules
Scripts/ client/src/pages/TestPage.tsx

View file

@ -9,8 +9,8 @@ using Microsoft.AspNetCore.Authorization;
namespace TicketManager.Controllers namespace TicketManager.Controllers
{ {
[Authorize] // [Authorize]
[Route("api/v1/[controller]")] [Route("api/v1/users")]
[ApiController] [ApiController]
public class UsersController : ControllerBase public class UsersController : ControllerBase
{ {

View file

@ -15,7 +15,7 @@ namespace TicketManager.DTO
Progression = project.Progression; Progression = project.Progression;
Status = project.Status.ToString(); Status = project.Status.ToString();
Manager = project.Manager; Manager = project.Manager;
AppUsers = project.GetMembers(); Users = project.GetMembers();
Tickets = project.Tickets; Tickets = project.Tickets;
Activities = project.Activities; Activities = project.Activities;
Files = project.Files; Files = project.Files;
@ -36,7 +36,7 @@ namespace TicketManager.DTO
public string Status { get; set; } public string Status { get; set; }
public AppUser Manager { get; set; } public AppUser Manager { get; set; }
public List<AppUser> AppUsers { get; set; } = new List<AppUser>(); public List<AppUser> Users { get; set; } = new List<AppUser>();
public List<Ticket> Tickets { get; set; } = new List<Ticket>(); public List<Ticket> Tickets { get; set; } = new List<Ticket>();

View file

@ -33,14 +33,17 @@
## TO DO ## TO DO
- Write API tests using Postman: request + test, environment variables, mock server - [ ] Write API tests using Postman: request + test, environment variables, mock server
- Annotate API request in controllers - [ ] Annotate API request in controllers
- Annotate Properties in Models - [ ] Annotate Properties in Models
- Write backend tests - [ ] Write backend tests
- Have a Look at typeahead component - [ ] Have a Look at typeahead component
- Ensure Tickets Edits belong to Project Edits - [ ] Ensure Tickets Edits belong to Project Edits
- Ensure Tickets Files belong to Project Files - [ ] Ensure Tickets Files belong to Project Files
- Async model methods ? - [ ] Async model methods ?
- update assignments automatically from context - [ ] update assignments automatically from context
- use PATCH instead of PUT - [ ] use PATCH instead of PUT
- logging - [ ] logging
- [ ] check useRef, useReducer, dispatch
- [ ] error page redirect when offline.
- [ ] ticket/files/activities list placeholders when empty

1
Scripts/apiQueries.sh Executable file
View file

@ -0,0 +1 @@
curl --insecure https://localhost:5001/api/v1/

1
Scripts/authentication.sh Executable file
View file

@ -0,0 +1 @@
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

View file

@ -1,42 +0,0 @@
.panel {
padding-left: 0px;
padding-top: 10px;
}
.field {
padding-left: 10px;
padding-right: 10px;
}
.city {
display: flex;
background: linear-gradient(
90deg,
rgba(2, 0, 36, 1) 0%,
rgba(25, 112, 245, 0.6399510487788865) 0%,
rgba(0, 212, 255, 1) 100%
);
flex-direction: column;
height: 40vh;
justify-content: center;
align-items: center;
padding: 0px 20px 20px 20px;
margin: 0px 0px 50px 0px;
border: 1px solid;
border-radius: 5px;
box-shadow: 2px 2px #888888;
font-family: "Merriweather", serif;
}
.city h1 {
line-height: 1.2;
}
.city span {
padding-left: 20px;
}
.city .row {
padding-top: 20px;
}
.weatherError {
color: #f16051;
font-size: 20px;
letter-spacing: 1px;
font-weight: 200;
}

View file

@ -1,6 +1,4 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { AppRouter } from "./utils/router";
import "./App.css";
import Layout from "./pages/Layout"; import Layout from "./pages/Layout";
const App: FC = () => { const App: FC = () => {

View file

@ -0,0 +1,38 @@
import { Ticket } from "../types/Ticket";
import { Project } from "../types/Project";
import { AppFile } from "../types/AppFile";
import { Activity } from "../types/Activity";
import { User } from "../types/User";
import { getRemainingdays } from "../utils/methods";
export default class ProjectVM {
public id: number;
public title: string;
public description: string;
public value: number;
public tickets: Ticket[];
public users: User[];
public ticketsTotalCount: number;
public ticketsDone: number;
public remainingDays: number;
public files: AppFile[];
public activities: Activity[];
public constructor(project: Project) {
this.id = project.id;
this.title = project.title;
this.description = project.description;
this.users = project.users;
this.value = project.progression;
this.tickets = project.tickets;
this.ticketsTotalCount =
this.tickets === undefined ? 0 : this.tickets.length;
this.ticketsDone =
this.tickets === undefined
? 0
: this.tickets.filter(t => t.status === "Done").length;
this.files = project.files;
this.activities = project.activities;
this.remainingDays = getRemainingdays(project.plannedEnding);
}
}

View file

@ -0,0 +1,49 @@
import React, { FC } from "react";
import { Activity } from "../types/Activity";
type IProps = {
activities: Activity[];
filterText: string;
};
export const ActivityCollection: FC<IProps> = ({ activities, filterText }) => {
return activities === undefined ? (
<></>
) : (
<>
<ul className="collection">
{activities
.filter(
a =>
a.description.toLowerCase().includes(filterText.toLowerCase()) ||
a.user.firstName
.toLowerCase()
.includes(filterText.toLowerCase()) ||
a.ticket.title.toLowerCase().includes(filterText.toLowerCase())
)
.map((activity: Activity) => (
<li key={activity.id} className="collection-item avatar">
<ActivityEntry activity={activity} />
</li>
))}
</ul>
</>
);
};
type IFProps = {
activity: Activity;
};
export const ActivityEntry: FC<IFProps> = ({ activity }) => {
return (
<>
<img src={activity.user.picture} alt="" className="circle" />
{/* <i className="material-icons circle">folder</i> */}
<span className="title">
{activity.user.firstName} {activity.description} {activity.ticket.title}
</span>
<p>{activity.date.toDateString()}</p>
</>
);
};

View file

@ -0,0 +1,34 @@
import React, { FC, useState, ChangeEvent, MouseEvent } from "react";
import { ActivityCollection } from "./ActivityCollection";
import { Activity } from "../types/Activity";
import { FilterBar } from "./FilterBar";
type IProps = {
activities: Activity[];
};
export const ActivityList: FC<IProps> = ({ activities }) => {
const [filterText, setFilterText] = useState<string>("");
const clearFilterText: (e: MouseEvent) => void = (e: MouseEvent) => {
setFilterText("");
};
const handleChange: (e: ChangeEvent<HTMLInputElement>) => void = (
e: ChangeEvent<HTMLInputElement>
) => {
setFilterText(e.target.value);
};
return (
<>
<div className="row valign-wrapper">
<h3>Activity</h3>
<FilterBar
filterText={filterText}
handleChange={handleChange}
clearFilterText={clearFilterText}
/>
</div>
<ActivityCollection activities={activities} filterText={filterText} />
</>
);
};

View file

@ -0,0 +1,35 @@
import React, { FC, useState, ChangeEvent, MouseEvent } from "react";
import { AppFile } from "../types/AppFile";
import { FileCollection } from "./FileCollection";
import { InputFile } from "./InputFile";
import { FilterBar } from "./FilterBar";
type IProps = {
files: AppFile[];
};
export const FileList: FC<IProps> = ({ files }) => {
const [filterText, setFilterText] = useState<string>("");
const clearFilterText: (e: MouseEvent) => void = (e: MouseEvent) => {
setFilterText("");
};
const handleChange: (e: ChangeEvent<HTMLInputElement>) => void = (
e: ChangeEvent<HTMLInputElement>
) => {
setFilterText(e.target.value);
};
return (
<>
<div className="row valign-wrapper">
<h3>Files</h3>
<FilterBar
filterText={filterText}
handleChange={handleChange}
clearFilterText={clearFilterText}
/>
</div>
<InputFile />
<FileCollection files={files} filterText={filterText} />
</>
);
};

View file

@ -1,15 +1,24 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { FloatingButton } from "./FloatingButton"; import { User } from "../types/User";
interface AvatarListProps { interface AvatarListProps {
avatars: string[]; users: User[];
} }
export const AvatarList: FC<AvatarListProps> = ({ avatars }) => { export const AvatarList: FC<AvatarListProps> = ({ users }) => {
return ( return users === undefined ? (
<></>
) : (
<> <>
{avatars.map((avatar: string) => ( {users.map((user: User, i: number) => (
<img className="circle" src={avatar} width="32vh" height="32vh" /> <img
key={i}
className="circle"
src={user.picture}
width="32vh"
height="32vh"
alt={user.fullName}
/>
))} ))}
</> </>
); );

View file

@ -1,4 +1,4 @@
import React, { FC, Children } from "react"; import React, { FC, MouseEvent } from "react";
interface IProps { interface IProps {
icon?: string; icon?: string;
@ -6,18 +6,20 @@ interface IProps {
shape?: string; shape?: string;
color?: string; color?: string;
text?: string; text?: string;
onClick?: (e: MouseEvent) => void;
} }
export const Button: FC<IProps> = ({ export const Button: FC<IProps> = ({
size = "small", size = "small",
shape = "", shape = "",
color, color,
text, onClick,
children children
}) => { }) => {
return ( return (
<button <button
className={`waves-effect waves-light btn-${size} ${shape} ${color}`} className={`waves-effect waves-light btn-${size} ${shape} ${color}`}
onClick={onClick}
> >
{children} {children}
</button> </button>

View file

@ -0,0 +1,45 @@
import React, { FC } from "react";
import { AppFile } from "../types/AppFile";
type IProps = {
files: AppFile[];
filterText: string;
};
export const FileCollection: FC<IProps> = ({ files, filterText }) => {
return (
<>
<ul className="collection">
{files
.filter(
f =>
f.name.toLowerCase().includes(filterText.toLowerCase()) ||
f.format.toLowerCase().includes(filterText.toLowerCase())
)
.map((file: AppFile) => (
<FileEntry file={file} key={file.id} />
))}
</ul>
</>
);
};
type IFProps = {
file: AppFile;
};
export const FileEntry: FC<IFProps> = ({ file }) => {
return (
<li className="collection-item avatar">
{/* <img src={require("../images/user_1.jpg")} alt="" className="circle" /> */}
<i className="material-icons circle">folder</i>
<span className="title">{file.name}</span>
<p>
{file.size}kb {file.format}
</p>
<a href="#!" className="secondary-content">
<i className="material-icons">more_vert</i>
</a>
</li>
);
};

View file

@ -0,0 +1,41 @@
import React, { FC, ChangeEvent, MouseEvent } from "react";
import { useRouteMatch } from "react-router-dom";
type IProps = {
filterText: string;
handleChange: (e: ChangeEvent<HTMLInputElement>) => void;
clearFilterText: (e: MouseEvent<HTMLInputElement>) => void;
};
export const FilterBar: FC<IProps> = ({
filterText,
handleChange,
clearFilterText
}) => {
const { url } = useRouteMatch();
const placeholder: string = url.split("/")[3] || "users";
return (
<>
<div className="nav-wrapper">
<div className="input-field">
<input
// className="validate"
id="filter"
type="search"
required
name="filter"
value={filterText}
placeholder={`Filter ${placeholder}`}
onChange={handleChange}
/>
<label className="label-icon" htmlFor="search">
<i className="material-icons">filter_list</i>
</label>
<i className="material-icons" onClick={clearFilterText}>
close
</i>
</div>
</div>
</>
);
};

View file

@ -1,20 +1,22 @@
import React, { FC } from "react"; import React, { FC, MouseEvent } from "react";
import { Button } from "./Button"; import { Button } from "./Button";
interface IProps { interface IProps {
icon?: string; icon?: string;
size?: string; size?: string;
color?: string; color?: string;
onClick?: (e: MouseEvent) => void;
} }
export const FloatingButton: FC<IProps> = ({ export const FloatingButton: FC<IProps> = ({
icon = "add", icon = "add",
size = "small", size = "small",
color = "red" color = "red",
onClick
}) => { }) => {
const iconComponent = <i className="material-icons left">{icon}</i>; const iconComponent = <i className="material-icons left">{icon}</i>;
return ( return (
<Button color={color} size={size} shape="btn-floating"> <Button color={color} size={size} shape="btn-floating" onClick={onClick}>
{iconComponent} {iconComponent}
</Button> </Button>
); );

View file

@ -1,54 +1,44 @@
import React, { FC, MouseEvent } from "react"; import React, { FC, MouseEvent } from "react";
import { AvatarList } from "./AvatarList"; import { Link } from "react-router-dom";
import { getRemainingdays } from "../utils/methods";
interface IProps { interface IProps {
title: string; title: string;
tasksTotalCount?: number; remainingDays: string;
tasksDone?: number;
remainingDays?: number;
avatars: string[];
validateTicket: (event: MouseEvent) => void; validateTicket: (event: MouseEvent) => void;
archiveTicket: (event: MouseEvent) => void; archiveTicket: (event: MouseEvent) => void;
} }
export const HorizontalCard: FC<IProps> = ({ export const HorizontalCard: FC<IProps> = ({
title, title,
tasksDone,
tasksTotalCount,
remainingDays, remainingDays,
avatars,
archiveTicket, archiveTicket,
validateTicket validateTicket
}) => { }) => {
return ( return (
<div className="col s12"> <div className="card horizontal">
<div className="card horizontal"> <div className="card-stacked">
<div className="card-stacked"> <div className="card-content">
<div className="card-content"> <div className="row">
<div className="row"> <div className="card-title">
<div className="card-title"> <h6>
<h6>{title}</h6> <Link to="#">
</div> <b>{title}</b>
<span>Due {remainingDays} days</span> </Link>
{/* <AvatarList avatars={avatars} /> */} </h6>
<div className="right"> </div>
{/* <i className=" material-icons">playlist_add_check</i> <span>Due {getRemainingdays(remainingDays)} days</span>
<span> <div className="right">
{" "} <Link to="#">
{tasksDone}/{tasksTotalCount} <i className="material-icons" onClick={validateTicket}>
</span> */} check
</i>
<a> </Link>
<i className="material-icons" onClick={validateTicket}> <Link to="#">
check <i className="material-icons" onClick={archiveTicket}>
</i> archive
</a> </i>
<a> </Link>
<i className="material-icons" onClick={archiveTicket}>
archive
</i>
</a>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,29 @@
import React, { FC } from "react";
type IProps = {};
export const InputFile: FC<IProps> = () => {
return (
<>
<form action="/upload">
<div className="file-field input-field">
<div className="btn">
<i className="material-icons ">cloud_upload</i>
<input
type="file"
multiple
accept=".doc,.docx,.pdf,.md,.gdoc,.zip,image/*"
/>
</div>
<div className="file-path-wrapper">
<input
className="file-path validate"
type="text"
placeholder="Upload one or more files"
/>
</div>
</div>
</form>
</>
);
};

View file

@ -0,0 +1,24 @@
import React, { FC, useState, CSSProperties } from "react";
interface IProps {
handleClose: () => void;
show: boolean;
}
export const Modal: FC<IProps> = ({ handleClose, show, children }) => {
const showHideStyle: CSSProperties = show
? { display: "block", zIndex: 10 }
: { display: "none", zIndex: 10 };
return (
<div className="modal" style={showHideStyle}>
<div className="modal-content">{children}</div>
<div className="modal-footer">
<button
type="submit"
className="modal-close waves-effect waves-green btn"
>
Done
</button>
</div>
</div>
);
};

View file

@ -0,0 +1,55 @@
import React, { FC } from "react";
export const Preloader: FC = () => {
return (
<div className="preloader-wrapper big active">
<div className="spinner-layer spinner-blue">
<div className="circle-clipper left">
<div className="circle"></div>
</div>
<div className="gap-patch">
<div className="circle"></div>
</div>
<div className="circle-clipper right">
<div className="circle"></div>
</div>
</div>
<div className="spinner-layer spinner-red">
<div className="circle-clipper left">
<div className="circle"></div>
</div>
<div className="gap-patch">
<div className="circle"></div>
</div>
<div className="circle-clipper right">
<div className="circle"></div>
</div>
</div>
<div className="spinner-layer spinner-yellow">
<div className="circle-clipper left">
<div className="circle"></div>
</div>
<div className="gap-patch">
<div className="circle"></div>
</div>
<div className="circle-clipper right">
<div className="circle"></div>
</div>
</div>
<div className="spinner-layer spinner-green">
<div className="circle-clipper left">
<div className="circle"></div>
</div>
<div className="gap-patch">
<div className="circle"></div>
</div>
<div className="circle-clipper right">
<div className="circle"></div>
</div>
</div>
</div>
);
};

View file

@ -1,4 +1,4 @@
import React, { FC, HTMLAttributes, CSSProperties } from "react"; import React, { FC, CSSProperties } from "react";
type ProgressBarProps = { type ProgressBarProps = {
value: number; value: number;

View file

@ -1,52 +1,47 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { TabRouterHeader } from "./TabRouterHeader"; import { TabRouterHeader } from "./TabRouterHeader";
import { TicketList } from "./TicketList"; import { TicketList } from "./TicketList";
import { FileList } from "./AppFileList";
import { Ticket } from "../types/Ticket"; import { Ticket } from "../types/Ticket";
import { Switch, Route, useRouteMatch, Redirect } from "react-router-dom"; import { AppFile } from "../types/AppFile";
import { Route, useRouteMatch, Redirect } from "react-router-dom";
import { ActivityList } from "./ActivityList";
import { Activity } from "../types/Activity";
interface IProps { interface IProps {
tickets: Ticket[]; tickets: Ticket[];
tasksTotalCount?: number;
tasksDone?: number;
remainingDays?: number; remainingDays?: number;
avatars: string[]; tabNames: string[];
files: AppFile[];
activities: Activity[];
} }
export const TabRouter: FC<IProps> = ({ export const TabRouter: FC<IProps> = ({
tickets, tickets,
tasksDone, tabNames,
tasksTotalCount, files,
remainingDays, activities
avatars
}) => { }) => {
const { url } = useRouteMatch(); const { url } = useRouteMatch();
return ( return (
<> <>
<Switch> <div className="row">
<div className="row"> <TabRouterHeader tabNames={tabNames} />
<TabRouterHeader />
<Redirect from={url} to={`${url}/tickets`} /> <Redirect from={url} to={`${url}/tickets`} />
<Route path={`${url}/tickets`}> <Route path={`${url}/tickets`}>
<TicketList <TicketList tickets={tickets} />
tickets={tickets} </Route>
tasksDone={tasksDone}
tasksTotalCount={tasksTotalCount}
remainingDays={remainingDays}
avatars={avatars}
/>
</Route>
<Route path={`${url}/files`}> <Route path={`${url}/files`}>
{/* <TicketList tickets={tickets} /> */} <FileList files={files} />
</Route> </Route>
<Route path={`${url}/activity`}> <Route path={`${url}/activity`}>
{/* <TicketList tickets={tickets} /> */} <ActivityList activities={activities} />
</Route> </Route>
</div> </div>
</Switch>
</> </>
); );
}; };

View file

@ -1,12 +1,50 @@
import React, { FC, useState } from "react"; import React, { FC, useState } from "react";
import { Link, useRouteMatch } from "react-router-dom"; import { Link, useRouteMatch } from "react-router-dom";
interface IProps {
tabClass?: string;
tabNames: string[];
}
export const TabRouterHeader: FC<IProps> = ({
tabClass = "tab col s4",
tabNames
}) => {
const [isActive, setIsActive] = useState(0);
const nTabs = tabNames.length;
return (
<>
<ul className="tabs z-depth-1">
{tabNames.map((name, i) => (
<TabUnit
key={i}
text={name}
value={i.toString()}
tabClass={tabClass}
isActive={isActive}
setIsActive={setIsActive}
nTabs={nTabs}
/>
))}
<li
className="indicator"
style={{
left: `${(isActive / nTabs) * 100}%`,
right: `${(1 - (isActive + 1) / nTabs) * 100}%`
}}
></li>
</ul>
</>
);
};
interface TabUnitProps { interface TabUnitProps {
tabClass: string; tabClass: string;
isActive: number; isActive: number;
setIsActive: React.Dispatch<React.SetStateAction<number>>; setIsActive: React.Dispatch<React.SetStateAction<number>>;
text: string; text: string;
value: string; value: string;
nTabs: number;
} }
const TabUnit: FC<TabUnitProps> = ({ const TabUnit: FC<TabUnitProps> = ({
@ -14,15 +52,23 @@ const TabUnit: FC<TabUnitProps> = ({
isActive, isActive,
setIsActive, setIsActive,
text, text,
value value,
nTabs
}) => { }) => {
const { url } = useRouteMatch(); const { url } = useRouteMatch();
return ( return (
<li className={tabClass} key={value}> <li
className={tabClass}
key={value}
style={{
left: `${(isActive / nTabs) * 100}%`,
right: `${(1 - (isActive + 1) / nTabs) * 100}%`
}}
>
<Link <Link
to={`${url}/${text}`} to={`${url}/${text}`}
id={value} id={value}
className={isActive === parseInt(value) ? "active" : ""} className={isActive === parseInt(value) ? "active pink lighten-5" : ""}
onClick={() => setIsActive(parseInt(value))} onClick={() => setIsActive(parseInt(value))}
> >
{text} {text}
@ -30,50 +76,3 @@ const TabUnit: FC<TabUnitProps> = ({
</li> </li>
); );
}; };
interface IProps {
tabClass?: string;
}
export const TabRouterHeader: FC<IProps> = ({
tabClass = "tab col s3",
children
}) => {
const [isActive, setIsActive] = useState(1);
// const switchTab = (e: React.MouseEvent<HTMLAnchorElement>): void => {
// e.preventDefault();
// setIsActive(e.target.id);
// };
return (
<>
<div className="row col s12">
<ul className="tabs">
<TabUnit
text="Tickets"
value="1"
tabClass={tabClass}
isActive={isActive}
setIsActive={setIsActive}
/>
<TabUnit
text="Files"
value="2"
tabClass={tabClass}
isActive={isActive}
setIsActive={setIsActive}
/>
<TabUnit
text="Activity"
value="3"
tabClass={tabClass}
isActive={isActive}
setIsActive={setIsActive}
/>
</ul>
</div>
</>
);
};

View file

@ -1,52 +1,62 @@
import React, { FC } from "react"; import React, { FC, useState, ChangeEvent, MouseEvent } from "react";
import { Ticket } from "../types/Ticket"; import { Ticket } from "../types/Ticket";
import { FloatingButton } from "./FloatingButton"; import { FloatingButton } from "./FloatingButton";
import { HorizontalCard } from "./HorizontalCard"; import { HorizontalCard } from "./HorizontalCard";
import { FilterBar } from "./FilterBar";
type TicketListProps = { type TicketListProps = {
tickets: Ticket[]; tickets: Ticket[];
tasksTotalCount?: number;
tasksDone?: number;
remainingDays?: number;
avatars: string[];
}; };
export const TicketList: FC<TicketListProps> = ({ export const TicketList: FC<TicketListProps> = ({ tickets }) => {
tickets, const [filterText, setFilterText] = useState<string>("");
tasksDone, const clearFilterText: (e: MouseEvent) => void = (e: MouseEvent) => {
tasksTotalCount, setFilterText("");
remainingDays, };
avatars
}) => {
const archiveTicket = () => {}; const archiveTicket = () => {};
const validateTicket = () => {}; const validateTicket = () => {};
const onClick: (e: MouseEvent) => void = (e: MouseEvent) => {
e.preventDefault();
};
const handleChange: (e: ChangeEvent<HTMLInputElement>) => void = (
e: ChangeEvent<HTMLInputElement>
) => {
setFilterText(e.target.value);
};
return ( return (
<div className="col s12"> <>
<div className="row valign-wrapper"> <div className="row valign-wrapper">
<div className="col s6 m4"> <h3>Tickets</h3>
<h2>Tickets</h2> <FloatingButton
</div> color=" blue-grey lighten-4"
<div className="col s6 m8"> size="small"
<FloatingButton color="grey" size="big" /> onClick={onClick}
</div> />
<FilterBar
filterText={filterText}
handleChange={handleChange}
clearFilterText={clearFilterText}
/>
</div> </div>
<div className="col s12 grey">
<ul> <ul>
{tickets.map((t: Ticket) => ( {tickets
<li key={t.id}> .filter(t =>
<HorizontalCard t.title.toLowerCase().includes(filterText.toLowerCase())
title={t.title} )
tasksDone={tasksDone} .map((t: Ticket) => (
tasksTotalCount={tasksTotalCount} <li key={t.id}>
remainingDays={remainingDays} <HorizontalCard
avatars={avatars} title={t.title}
validateTicket={validateTicket} remainingDays={t.plannedEnding}
archiveTicket={archiveTicket} validateTicket={validateTicket}
/> archiveTicket={archiveTicket}
</li> />
))} </li>
</ul> ))}
</div> </ul>
</div>
</>
); );
}; };

View file

@ -0,0 +1,103 @@
import React, { FC, useState, ChangeEvent, useEffect } from "react";
import { Modal } from "./Modal";
import { AvatarList } from "./AvatarList";
import { User } from "../types/User";
import { FilterBar } from "./FilterBar";
import { HttpResponse } from "../types/HttpResponse";
import { get } from "../utils/http";
import { Constants } from "../utils/Constants";
interface IProps {
show: boolean;
handleClose: () => void;
users: User[];
}
export const UsersModal: FC<IProps> = ({ show, handleClose, users }) => {
const [filterText, setFilterText] = useState<string>("");
const handleChange: (e: ChangeEvent<HTMLInputElement>) => void = (
e: ChangeEvent<HTMLInputElement>
) => {
setFilterText(e.target.value);
};
const [allUsers, setAllUsers] = useState();
async function httpGet(): Promise<void> {
try {
const response: HttpResponse<User> = await get<User>(
`${Constants.usersURI}`
);
if (response.parsedBody !== undefined) {
setAllUsers(response.parsedBody);
// setIsLoading(false);
}
} catch (ex) {
// setHasError(true);
// setError(ex);
}
}
useEffect(() => {
// if (id !== undefined) {
httpGet();
// } else {
// setHasError(true);
// setError("Bad Request");
// }
}, []);
return (
<Modal show={show} handleClose={handleClose}>
<div className="row valign-wrapper blue">
<div className="col s10">
<h4 className="white-text">Manage users</h4>
</div>
<div className="col s2">
<i
className="right material-icons blue lighten-3 circle"
onClick={handleClose}
>
close
</i>
</div>
</div>
<div className="center">
<AvatarList users={users} />
<FilterBar
filterText={filterText}
clearFilterText={() => setFilterText("")}
handleChange={handleChange}
/>
</div>
{/* <div className="code">{allUsers}</div> */}
<form>
<ul>
{users.map((u: User) => (
<li key={u.id}>
<div className="row">
<input
id={u.id}
type="checkbox"
name="active"
value="true"
onChange={() => false}
// checked
/>
<span>
{u.fullName}
<img
className="circle"
src={u.picture}
width="32vh"
height="32vh"
alt={u.fullName}
/>
</span>
</div>
</li>
))}
</ul>
</form>
</Modal>
);
};

View file

@ -0,0 +1,19 @@
import React, { FC } from "react";
import { Redirect } from "react-router-dom";
interface IProps {
error: any;
}
export const ErrorController: FC<IProps> = ({ error }) => {
switch (error) {
case "Bad Request":
return <Redirect to="/400" />;
case "Not Found":
return <Redirect to="/404" />;
default:
return <Redirect to="/404" />;
}
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,13 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View file

@ -1,6 +1,5 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import "./index.css";
import App from "./App"; import App from "./App";
import * as serviceWorker from "./serviceWorker"; import * as serviceWorker from "./serviceWorker";

View file

@ -0,0 +1,10 @@
import React, { FC } from "react";
interface IProps {}
export const NotFoundPage: FC<IProps> = () => {
return (
<div className="section">
<p>error</p>
</div>
);
};

View file

@ -1,32 +1,50 @@
import React, { FC } from "react"; import React, { FC, useState } from "react";
import ProjectVM from "../VM/ProjectVM";
import { Header } from "../components/Header"; import { Header } from "../components/Header";
import { AvatarList } from "../components/AvatarList"; import { AvatarList } from "../components/AvatarList";
import { ProgressBar } from "../components/ProgressBar"; import { ProgressBar } from "../components/ProgressBar";
import ProjectVM from "../viewModels/ProjectVM";
import { TabRouter } from "../components/TabRouter"; import { TabRouter } from "../components/TabRouter";
import { FloatingButton } from "../components/FloatingButton"; import { FloatingButton } from "../components/FloatingButton";
import { UsersModal } from "../components/UsersModal";
interface IProps { interface IProps {
viewModel: ProjectVM; viewModel: ProjectVM;
} }
export const ProjectPage: FC<IProps> = ({ viewModel }) => { export const ProjectPage: FC<IProps> = ({ viewModel }) => {
const { const {
title, title,
description, description,
avatars, users,
value, value,
tickets, tickets,
ticketsDone, ticketsDone,
ticketsTotalCount, ticketsTotalCount,
remainingDays remainingDays,
files,
activities
} = viewModel; } = viewModel;
const tabNames: string[] = ["Tickets", "Files", "Activity"];
const [showModal, setShowModal] = useState<boolean>(false);
return ( return (
<div className="section"> <div className="section">
<div className="container"> <div className="container">
<Header title={title} description={description} /> <Header title={title} description={description} />
<div className="row valign-wrapper"> <div className="row valign-wrapper">
<AvatarList avatars={avatars} /> <AvatarList users={users} />
<FloatingButton icon="add" color="grey" size="small" /> <FloatingButton
icon="add"
color="grey"
size="small"
onClick={() => setShowModal(true)}
/>
<UsersModal
show={showModal}
users={users}
handleClose={() => setShowModal(false)}
/>
</div> </div>
<ProgressBar <ProgressBar
value={value} value={value}
@ -35,11 +53,10 @@ export const ProjectPage: FC<IProps> = ({ viewModel }) => {
remainingDays={remainingDays} remainingDays={remainingDays}
/> />
<TabRouter <TabRouter
tabNames={tabNames}
tickets={tickets} tickets={tickets}
tasksDone={ticketsDone} files={files}
tasksTotalCount={ticketsTotalCount} activities={activities}
remainingDays={remainingDays}
avatars={avatars}
/> />
</div> </div>
</div> </div>

View file

@ -10,7 +10,7 @@ export const TicketPage: FC = () => {
description="Research, ideate and present brand concepts for client consideration" description="Research, ideate and present brand concepts for client consideration"
title="Brand Concept and Design" title="Brand Concept and Design"
/> />
<AvatarList avatars={["../images/user_1.jpg", "../images/user_2.jpg"]} /> {/* <AvatarList users={["../images/user_1.jpg", "../images/user_2.jpg"]} /> */}
<ProgressBar value={60} /> <ProgressBar value={60} />
{/* // <TabView> {/* // <TabView>
// <ChildTicket/> // <ChildTicket/>

View file

@ -1,3 +1,10 @@
import { User } from "./User";
import { Ticket } from "./Ticket";
export interface Activity { export interface Activity {
Id: number; id: number;
description: string;
date: Date;
user: User;
ticket: Ticket;
} }

View file

@ -0,0 +1,9 @@
import { User } from "./User";
export interface AppFile {
id: number;
name: string;
description: string;
format: string;
size: number;
}

View file

@ -1,3 +0,0 @@
export interface File {
Id: number;
}

View file

@ -0,0 +1,3 @@
export interface HttpResponse<T> extends Response {
parsedBody?: T;
}

View file

@ -1,5 +1,6 @@
import { Ticket } from "./Ticket"; import { Ticket } from "./Ticket";
import { User } from "./User"; import { User } from "./User";
import { AppFile } from "./AppFile";
import { Activity } from "./Activity"; import { Activity } from "./Activity";
export interface Project { export interface Project {
@ -13,6 +14,6 @@ export interface Project {
manager: User; manager: User;
users: User[]; users: User[];
tickets: Ticket[]; tickets: Ticket[];
files: AppFile[];
activities: Activity[]; activities: Activity[];
files: File[];
} }

View file

@ -1,5 +1,7 @@
export interface Ticket { export interface Ticket {
id: number; id: number;
title: string; title: string;
description: string;
status: string; status: string;
plannedEnding: string;
} }

View file

@ -1,4 +1,6 @@
export interface User { export interface User {
id: string; id: string;
picture: File; picture: string;
firstName: string;
fullName?: string;
} }

View file

@ -1,3 +1,5 @@
export class Constants { export class Constants {
static getProjectURI: string = "/api/projects"; static projectsURI: string = "/api/v1/projects";
static ticketsURI: string = "/api/v1/tickets";
static usersURI: string = "/api/v1/users";
} }

35
client/src/utils/http.ts Normal file
View file

@ -0,0 +1,35 @@
import { HttpResponse } from "../types/HttpResponse";
export async function http<T>(request: RequestInfo): Promise<HttpResponse<T>> {
const response: HttpResponse<T> = await fetch(request);
try {
response.parsedBody = await response.json();
} catch (ex) {}
if (!response.ok) {
throw response.statusText;
}
return response;
}
export async function get<T>(
path: string,
args: RequestInit = { method: "get" }
): Promise<HttpResponse<T>> {
return await http<T>(new Request(path, args));
}
export async function post<T>(
path: string,
body: any,
args: RequestInit = { method: "post", body: JSON.stringify(body) }
): Promise<HttpResponse<T>> {
return await http<T>(new Request(path, args));
}
export async function put<T>(
path: string,
body: any,
args: RequestInit = { method: "put", body: JSON.stringify(body) }
): Promise<HttpResponse<T>> {
return await http<T>(new Request(path, args));
}

View file

@ -0,0 +1,7 @@
export const getRemainingdays: (endDate: string) => number = (
endDate: string
) => {
let endingDate: Date = new Date(endDate);
let today: Date = new Date();
return Math.abs(endingDate.getDate() - today.getDate());
};

View file

@ -1,19 +1,31 @@
import React from "react"; import React from "react";
import { Router, Route, Switch, Link, NavLink } from "react-router-dom"; import {
Router,
Route,
Switch
// Redirect
//Link, NavLink
} from "react-router-dom";
import * as creacteHistory from "history"; import * as creacteHistory from "history";
import { TicketPage } from "../pages/TicketPage"; // import { TicketPage } from "../pages/TicketPage";
import { HomeController } from "../controllers/HomeController"; // import { HomeController } from "../controllers/HomeController";
import { ProjectController } from "../controllers/ProjectController"; import { ProjectController } from "../controllers/ProjectController";
import { UserController } from "../controllers/UserController"; import { NotFoundPage } from "../pages/NotFoundPage";
import { TicketController } from "../controllers/TicketController"; import { TestPage } from "../pages/TestPage";
// import { UserController } from "../controllers/UserController";
// import { TicketController } from "../controllers/TicketController";
export const history = creacteHistory.createBrowserHistory(); export const history = creacteHistory.createBrowserHistory();
export const AppRouter = () => { export const AppRouter = () => {
return ( return (
<Router history={history}> <Router history={history}>
<div> <div className="grey lighten-4">
<Switch> <Switch>
<Route exact path="/">
<TestPage />
</Route>
{/* <Route path="/"> {/* <Route path="/">
<HomeController /> <HomeController />
</Route> </Route>
@ -26,6 +38,14 @@ export const AppRouter = () => {
{/* <Route path="/tickets/:id"> {/* <Route path="/tickets/:id">
<TicketController /> <TicketController />
</Route> */} </Route> */}
<Route path="/404">
<NotFoundPage />
</Route>
{/* <Route path="*">
<Redirect to="/error" />
</Route> */}
</Switch> </Switch>
</div> </div>
</Router> </Router>

View file

@ -1,46 +0,0 @@
import { Ticket } from "../types/Ticket";
import { Project } from "../types/Project";
import { Constants } from "../utils/Constants";
import { User } from "../types/User";
export default class ProjectVM {
public id: number;
public title: string;
public description: string;
public value: number;
public tickets: Ticket[];
public avatars: string[];
public ticketsTotalCount: number;
public ticketsDone: number;
public remainingDays: number;
/**
* getMembers
*/
// public getMembers(): string {
// let res: Promise<Response> = fetch(
// `${Constants.getProjectURI}/${this.id}/members`
// );
// return JSON.stringify(res);
// // res.json();
// }
public constructor(project: Project) {
this.id = project.id;
this.title = project.title;
this.description = project.description;
this.avatars = project.users.map(u => u.picture);
this.value = project.progression;
this.tickets = project.tickets;
this.ticketsTotalCount = this.tickets.length;
this.ticketsDone = this.tickets.filter(t => t.status === "Done").length;
let endingDate: Date = new Date(project.plannedEnding);
let today: Date = new Date();
let plannedEnding: number = Math.abs(
endingDate.getDate() - today.getDate()
);
this.remainingDays = plannedEnding;
}
}