Skip to content
Snippets Groups Projects
Commit 676a3b18 authored by Bernard Roland (Student Com20)'s avatar Bernard Roland (Student Com20)
Browse files

Merge branch 'frontend-devel' into devel

parents a39ac481 17b93b88
No related branches found
No related tags found
No related merge requests found
Showing
with 120 additions and 58 deletions
......@@ -71,7 +71,6 @@ export default function ProjectForm({ project, onSubmit }: Props) {
const [allTeams, setAllTeams] = useState<Team[]>([]);
useEffect(() => {
//TODO refactor
teams.forEach((userTeam) => {
getTeam(userTeam).then((team) => {
setAllTeams(state => {
......@@ -107,9 +106,9 @@ export default function ProjectForm({ project, onSubmit }: Props) {
validateColor(color ?? '') === null &&
validateTeams(teams) === null
) {
onSubmit?.(teams, name ?? '', text ?? '', color ?? '', status ?? Status.OPEN, deadline);
} else {
window.scrollTo(0, 0);
setError('Please fill in the mandatory fields.');
}
}, [onSubmit, setError, name, text, color, deadline, teams, status]);
......
......@@ -114,6 +114,7 @@ export default function TaskForm({ task, onSubmit, project }: Props) {
) {
onSubmit?.(name ?? '', text ?? '', icon ?? '', priority ?? Priority.LOW, tasks ?? [], requirements, assignees, status);
} else {
window.scrollTo(0, 0);
setError('Please fill in the mandatory fields.');
}
}, [onSubmit, setError, name, text, priority, icon, tasks, assignees, requirements, status]);
......
......@@ -17,7 +17,7 @@ export function validateName(name: string): string | null {
return 'The name is required';
}
export default function TeamCreateForm({ onSubmit, onBack, team }: Props) {
export default function TeamForm({ onSubmit, onBack, team }: Props) {
const [name, setName] = useState(team?.name ?? '');
const handleSubmit = useCallback(async (e: FormEvent) => {
e.preventDefault();
......
......@@ -4,31 +4,34 @@ import './user-form.scss';
import TextInput from 'components/ui/TextInput';
import Button from 'components/ui/Button';
import '../form.scss';
import Callout from 'components/ui/Callout';
interface Props {
onSubmit?: (name?: string, email?: string, avatar?: File) => void;
user: User
}
const validTypes = ['image/jpg', 'image/png', 'image/gif', 'image/svg']
function validateEmail(email?: string): string | null {
if (email && email.length > 0) {
if (email.match('^[^\\s]+@[^\\s]+$')) {
return null;
} else {
return 'Please enter a valid email or let this field blank.'
return 'Please enter a valid email or leave this field blank.'
}
} else {
console.log(email);
return null;
}
}
function validateAvatar(avatar?: File): string | null {
const validTypes = ['image/jpg', 'image/png', 'image/gif']
if (avatar) {
if (validTypes.find((type) => type === avatar.type)) {
return null;
} else {
return 'Only files from type jpg, png or gif are allowed'
return 'Only files from type jpg, png or gif are allowed';
}
} else {
return null;
......@@ -38,15 +41,20 @@ function validateAvatar(avatar?: File): string | null {
export default function UserForm({ user, onSubmit }: Props) {
const [name, setName] = useState(user.realname);
const [email, setEmail] = useState(user.email);
const [error, setError] = useState('');
const [avatarError, setAvatarError] = useState('');
const [avatar, setAvatar] = useState<File>();
const handleSubmit = useCallback(async (e: FormEvent) => {
e.preventDefault();
if (validateEmail(email) === null || validateAvatar(avatar) === null) {
if (validateEmail(email) === null && validateAvatar(avatar) === null) {
onSubmit?.(name, email, avatar);
} else {
setError('Please fill in the mandatory fields.');
}
}, [onSubmit, name, email, avatar]);
return (
<form onSubmit={handleSubmit} className="user-form">
{error && <Callout message={error} />}
<div className="fields">
<div className="fields-row">
<div className="col">
......@@ -68,16 +76,21 @@ export default function UserForm({ user, onSubmit }: Props) {
/>
</div>
</div>
<div className="avatar-upload">
<div className="avatar-upload input-element">
<div className="label">Avatar</div>
<label htmlFor="avatar" className="avatar-field">
<input type="file" id="avatar" name="avatar" onChange={(e) => {
if (e.target.files && e.target.files.length > 0) {
setAvatar(e.target.files[0])
if (validateAvatar(e.target.files[0])) {
setAvatarError(validateAvatar(e.target.files[0]) ?? '');
} else {
setAvatar(e.target.files[0])
}
}
}} />
{avatar ? 'Selected file: ' + avatar.name : 'Select a file'}
</label>
{avatarError && <div className="error">{avatarError}</div>}
</div>
</div>
<Button type="submit" className="expanded">
......
......@@ -12,8 +12,8 @@
.label {
position: absolute;
top: -2px;
left: 30px;
top: -10px;
left: 20px;
font-weight: s.$weight-bold;
}
......@@ -28,9 +28,11 @@
margin-bottom: 20px;
background: rgba(0, 0, 0, 0.025);
font-size: 18px;
&:hover {
background: rgba(0, 0, 0, 0.04);
&:hover {
background: rgba(0, 0, 0, 0.04);
}
@include mx.breakpoint(large) {
margin-top: 30px;
}
......
......@@ -6,7 +6,7 @@
padding: 50px;
background: s.$white;
border-radius: 10px;
.error-screen {
width: 100%;
height: 100%;
......@@ -21,7 +21,9 @@
align-items: flex-end;
height: 100%;
position: relative;
&:before, &:after {
&:before,
&:after {
content: ' ';
position: absolute;
bottom: 0;
......@@ -32,10 +34,39 @@
z-index: 0;
border-radius: 5px;
}
&:after {
bottom: 48px;
}
.tooltip {
position: absolute;
top: -8px;
left: 50%;
transform: translate(-50%, -75%);
padding: 10px;
background: s.$white;
color: s.$body-color;
border-radius: 5px;
display: block;
visibility: hidden;
opacity: 0;
box-shadow: 0 0 10px rgba(s.$black, 0.15);
&:before {
content: ' ';
position: absolute;
bottom: 0;
width: 0;
height: 0;
left: 50%;
transform: translate(-50%, 100%);
border-width: 8px 8px 0 8px;
border-color: s.$white transparent transparent transparent;
border-style: solid;
}
}
.bar {
background: s.$linear-gradient;
width: 24px;
......@@ -44,6 +75,16 @@
border-top-left-radius: 5px;
border-top-right-radius: 5px;
&:hover {
opacity: 0.9;
.tooltip {
transform: translate(-50%, -100%);
opacity: 1;
visibility: visible;
}
}
.label {
position: absolute;
bottom: -5px;
......
......@@ -7,9 +7,11 @@ export interface ChartItem {
interface Props {
data: ChartItem[];
unit?: string;
multiplicator?: number;
}
export default function BarChart({ data }: Props) {
export default function BarChart({ data, unit, multiplicator }: Props) {
let maxValue = data.map(e => e.value).sort((a, b) => b - a)[0];
return (
<div className="bar-chart-container">
......@@ -25,6 +27,9 @@ export default function BarChart({ data }: Props) {
<div className="label">
{item.label}
</div>
<div className="tooltip">
{(item.value * (multiplicator ?? 1)).toFixed(2) + (unit ?? '')}
</div>
</div>
))
}
......
......@@ -6,6 +6,7 @@
position: relative;
height: 10px;
border-radius: 5px;
overflow: hidden;
.progress {
position: absolute;
......
@use 'styles/settings.scss' as s;
@use 'styles/settings.scss'as s;
.filter-container {
.search-container {
position: relative;
margin-bottom: 30px;
.icon {
position: absolute;
top: 50%;
......@@ -11,6 +12,7 @@
transform: translateY(-50%);
opacity: 0.8;
}
input {
width: 100%;
padding: 18px;
......@@ -22,17 +24,24 @@
box-shadow: 0 0 15px rgba(s.$black, 0.05);
}
}
.status-filter {
margin-bottom: 40px;
.tags {
display: flex;
flex-wrap: wrap;
margin: -10px;
}
.tag-item {
margin: 10px;
cursor: pointer;
opacity: 0.5;
.tag {
margin: 0;
}
&.active {
opacity: 1;
}
......
......@@ -3,8 +3,7 @@ import { getCurrentUser, User } from 'adapters/user';
import Avatar from 'components/ui/Avatar';
import './comment-list.scss';
import { FormEvent, useCallback, useEffect, useState } from 'react';
import { createComment } from 'adapters/comment';
import { useHistory } from 'react-router';
import { createComment, getComment } from 'adapters/comment';
interface Props {
comments: CommentProps[]
......@@ -12,22 +11,28 @@ interface Props {
}
export default function CommentList({ comments, taskId }: Props) {
const [user, setUser] = useState<User>();
const [comment, setComment] = useState<string>('');
const history = useHistory();
const [allComments, setComments] = useState<CommentProps[]>([]);
useEffect(() => {
getCurrentUser().then((user) => setUser(user));
}, []);
setComments(comments);
}, [comments]);
const handleSubmit = useCallback((e: FormEvent) => {
e.preventDefault();
if(comment.length > 0) {
if(createComment({task: taskId, text: comment})) {
history.go(0);
}
if (comment.length > 0) {
createComment({ task: taskId, text: comment }).then(id => {
getComment(id).then(comment => {
setComments(state => [...state, {
comment: comment
}])
})
});
setComment('');
}
}, [comment, taskId, history])
}, [comment, taskId]);
return (
<div className="comment-list">
......@@ -43,14 +48,14 @@ export default function CommentList({ comments, taskId }: Props) {
</div>
</div>
<form onSubmit={handleSubmit}>
<textarea placeholder="Write a comment..." onChange={(e) => setComment(e.target.value)}></textarea>
<textarea value={comment} placeholder="Write a comment..." onChange={(e) => setComment(e.target.value)}></textarea>
<button type="submit">Send</button>
</form>
</div>
)
}
{comments.map(comment => (
{allComments.map(comment => (
<Comment key={comment.comment.id} {...comment} />
))}
......
......@@ -5,8 +5,8 @@ import 'react-alice-carousel/lib/alice-carousel.css';
const responsive = {
0: { items: 1 },
568: { items: 2 },
1024: { items: 3 },
560: { items: 2 },
1200: { items: 3 },
};
interface Props {
......@@ -27,7 +27,7 @@ export default function ProjectsSlider({ projects }: Props) {
touchTracking
>
{
projects.map(project => <ProjectSlide {...project} />)
projects.map(project => <ProjectSlide key={project.project.id} {...project} />)
}
</Carousel>
</div> :
......
......@@ -3,8 +3,9 @@
.project-slider {
margin: -10px;
overflow: hidden;
padding: 200px;
margin: -210px;
margin-right: -50px;
padding-right: 50px;
......
......@@ -55,6 +55,7 @@
display: block;
color: s.$body-color;
border-radius: 5px;
min-width: 100px;
&:hover {
box-shadow: 0 0 10px rgba(s.$black, 0.1);
......
......@@ -75,12 +75,9 @@
.site-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 20px;
&.right {
justify-content: flex-end;
}
justify-content: flex-end;
@include mx.breakpoint(large) {
display: none;
......
import './header.scss';
import { useHistory } from 'react-router-dom';
import Navigation from 'components/navigation/Navigation';
import Sidebar from 'components/navigation/Sidebar';
import Page from 'components/layout/Page'
......@@ -8,25 +7,14 @@ import { ReactNode, useState } from 'react';
interface Props {
children?: ReactNode
}
export default function Header({ children }: Props) {
const history = useHistory();
const hasBack = history.location.pathname.split('/').length > 3;
const [showSidebar, setShowSidebar] = useState<boolean>(false);
return (
<div className="full-width">
<Sidebar setMobileShown={setShowSidebar} mobileShown={showSidebar} />
<div className={'page-wrapper' + (showSidebar ? ' moved' : '')} onClick={() => showSidebar && setShowSidebar(false)}>
<Page>
<header className={'site-header' + (!hasBack ? ' right' : '')}>
{
hasBack && (
<span className="material-icons" onClick={history.goBack} >
arrow_back
</span>
)
}
<header className="site-header">
<div className="hamburger-container">
<div className="hamburger" onClick={() => !showSidebar && setShowSidebar(true)}>
<div className="line"></div>
......
......@@ -8,9 +8,9 @@ export default function Navigation() {
<nav className="site-nav">
<NavLink to="/tasks" activeClassName="active" className="nav-link">
<span className="icon material-icons-outlined">
home
task
</span>
<span className="label">Home</span>
<span className="label">Tasks</span>
</NavLink>
<NavLink to="/projects" activeClassName="active" className="nav-link">
<span className="icon material-icons-outlined">
......
......@@ -10,7 +10,7 @@
display: flex;
align-items: center;
justify-content: center;
z-index: 200;
z-index: 2000000;
background: s.$white;
box-shadow: 0 0 25px rgba(s.$black, 0.1);
......
......@@ -23,7 +23,7 @@ export default function Sidebar({ mobileShown, setMobileShown }: Props) {
if (isLoggedIn()) {
getCurrentUser().then((user) => {
setUser(user);
getUserActivity(subtractTime(new Date(), 1, 'week'), new Date()).then((a) =>
getUserActivity(subtractTime(new Date(), 1, 'week'), new Date()).then((a) =>
setActivity(parseActivity(a))
);
}).catch(() => { });
......@@ -70,7 +70,7 @@ export default function Sidebar({ mobileShown, setMobileShown }: Props) {
{
activity ? (
<div className="stats">
<BarChart data={activity} />
<BarChart unit="h" multiplicator={1 / 60 / 60 / 1000} data={activity} />
<div className="comment">Recent activity</div>
</div>
) : <LoadingScreen />
......
......@@ -138,7 +138,7 @@
.bar-chart-container {
background: transparent;
padding: 30px;
padding: 35px 0;
.bar-chart {
&:before,
......
......@@ -2,8 +2,7 @@
.assignee-list {
display: flex;
margin-left: 8px;
margin-left: 16px;
&:hover {
.assignee {
margin-left: 0;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment