├── routes.json ├── .prettierrc ├── client ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── manifest.json │ └── index.html ├── .storybook │ ├── addons.js │ └── config.js ├── src │ ├── components │ │ ├── Ticket │ │ │ ├── IconsWrapper.js │ │ │ ├── Title.js │ │ │ ├── Priority.js │ │ │ ├── Status.js │ │ │ ├── Progress.js │ │ │ └── Ticket.js │ │ ├── TextStyledH1.js │ │ ├── InputFields │ │ │ ├── InputLabel.js │ │ │ ├── InputDescription.js │ │ │ ├── InputField.js │ │ │ ├── InputSelectBox.js │ │ │ ├── SubmitButton.js │ │ │ └── RadioButton.js │ │ ├── FilterBarOrders │ │ │ ├── SearchButton.js │ │ │ └── Search.js │ │ ├── Header │ │ │ ├── Logo.js │ │ │ ├── Header.js │ │ │ ├── AddOrderButton.js │ │ │ ├── AddTicketButton.js │ │ │ ├── BackMainButton.js │ │ │ ├── BackOrdersButton.js │ │ │ ├── BackTicketsButton.js │ │ │ └── OrdersListButton.js │ │ ├── Filter Bar │ │ │ ├── Button.js │ │ │ ├── DateFilter.js │ │ │ ├── LocationFilter.js │ │ │ ├── PriorityFilter.js │ │ │ ├── StatusFilter.js │ │ │ └── FilterBar.js │ │ └── Order │ │ │ ├── Priority.js │ │ │ └── Order.js │ ├── index.js │ ├── setupTests.js │ ├── assets │ │ ├── icons │ │ │ ├── editIcon.svg │ │ │ ├── search.svg │ │ │ ├── FilterIcon.js │ │ │ ├── LocationIcon.js │ │ │ ├── DeleteIcon.js │ │ │ ├── QuantityIcon.js │ │ │ ├── CalendarIcon.js │ │ │ ├── SerialIcon.js │ │ │ ├── UserIcon.js │ │ │ ├── EditIcon.js │ │ │ ├── ManufacturerIcon.js │ │ │ └── TimeDateIcon.js │ │ └── logo │ │ │ └── logoSmall.svg │ ├── App.test.js │ ├── themes │ │ ├── lightTheme.js │ │ └── darkTheme.js │ ├── GlobalStyles.js │ ├── pages │ │ ├── OrderList.js │ │ ├── TicketList.js │ │ ├── StartPage.js │ │ ├── MainPage.js │ │ ├── NewOrder.js │ │ └── NewTicket.js │ ├── App.js │ └── serviceWorker.js ├── README.md └── package.json ├── .gitignore ├── server.js ├── .eslintrc.js ├── README.md ├── package.json └── db.json /routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api/*": "/$1" 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panchepanevski/ticky/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-links/register'; 3 | -------------------------------------------------------------------------------- /client/src/components/Ticket/IconsWrapper.js: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const IconsWrapper = styled.div` 4 | width: 24px; 5 | height: 24px; 6 | `; 7 | -------------------------------------------------------------------------------- /client/.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | 3 | // automatically import all files ending in *.stories.js 4 | configure(require.context('../src/stories', true, /\.stories\.js$/), module); 5 | -------------------------------------------------------------------------------- /client/src/components/Ticket/Title.js: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const Title = styled.h1` 4 | color: ${props => props.theme.colors.primary}; 5 | font-size: 22px; 6 | font-weight: 700; 7 | text-shadow: 1px 1px 1px #000000; 8 | `; 9 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import * as serviceWorker from "./serviceWorker"; 5 | 6 | ReactDOM.render(, document.getElementById("root")); 7 | 8 | serviceWorker.unregister(); 9 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Ticky 2 | 3 | ## Classify, analyze, and resolve with simplicity. 4 | 5 | With Ticky, you can create and use dynamic sections in your ticket post, to help your team filter tickets easily and resolve them faster. 6 | 7 | ### Link to Ticky - http://ticky-app.herokuapp.com/ 8 | -------------------------------------------------------------------------------- /client/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /client/src/assets/icons/editIcon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /client/src/components/TextStyledH1.js: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const TextStyledH1 = styled.h1` 4 | display: inline-flex; 5 | justify-content: center; 6 | font-size: 24px; 7 | font-weight: 900; 8 | text-shadow: 1px 1px 1px #000000; 9 | text-transform: uppercase; 10 | padding-top: 20px; 11 | `; 12 | -------------------------------------------------------------------------------- /client/src/components/InputFields/InputLabel.js: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const InputLabel = styled.p` 4 | display: flex; 5 | align-content: flex-start; 6 | justify-content: flex-start; 7 | justify-self: start; 8 | font-size: 12px; 9 | text-transform: uppercase; 10 | text-shadow: 1px 1px 1px #000000; 11 | `; 12 | -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /client/src/assets/icons/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | build 13 | storybook-static 14 | 15 | # misc 16 | .DS_Store 17 | .env 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | 28 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const jsonServer = require('json-server'); 2 | 3 | const PORT = process.env.PORT || 8080; 4 | const server = jsonServer.create(); 5 | const router = jsonServer.router('db.json'); 6 | const middlewares = jsonServer.defaults({ 7 | static: './client/build' 8 | }); 9 | 10 | server.use(middlewares); 11 | server.use('/api', router); 12 | 13 | server.listen(PORT, () => { 14 | console.log(`JSON Server is running on http://localhost:${PORT}`); 15 | }); 16 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | jest: true 7 | }, 8 | extends: ["eslint:recommended", "plugin:react/recommended"], 9 | globals: { 10 | Atomics: "readonly", 11 | SharedArrayBuffer: "readonly" 12 | }, 13 | parserOptions: { 14 | ecmaFeatures: { 15 | jsx: true 16 | }, 17 | ecmaVersion: 2018, 18 | sourceType: "module" 19 | }, 20 | plugins: ["react"], 21 | rules: {} 22 | }; 23 | -------------------------------------------------------------------------------- /client/src/themes/lightTheme.js: -------------------------------------------------------------------------------- 1 | const lightTheme = { 2 | colors: { 3 | background: '#EFEFEF', 4 | primary: '#191828', 5 | secondary: '#22A8B0', 6 | tertiary: '#5d38ff', 7 | ticket_bg: '#222031', 8 | status_active: '#7EFFA1', 9 | status_in_progres: '#E8E200', 10 | status_completed: '#727272', 11 | progress: '#21F3FF', 12 | priority_normal: '#21AAFF', 13 | priority_medium: '#FF9B00', 14 | priority_high: '#FF434C' 15 | } 16 | }; 17 | 18 | export default lightTheme; 19 | -------------------------------------------------------------------------------- /client/src/components/FilterBarOrders/SearchButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import search from '../../assets/icons/search.svg'; 4 | 5 | const Button = styled.button` 6 | width: 20px; 7 | height: 20px; 8 | border: none; 9 | margin: 0; 10 | padding: 0; 11 | background-color: transparent; 12 | `; 13 | 14 | const SearchButton = () => { 15 | return ( 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default SearchButton; 23 | -------------------------------------------------------------------------------- /client/src/components/Header/Logo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import logo from '../../assets/logo/logoSmall.svg'; 4 | import { Link } from 'react-router-dom'; 5 | 6 | const LogoWrapper = styled.img` 7 | grid-column: 2 / 3; 8 | width: auto; 9 | height: 50px; 10 | `; 11 | 12 | const HomeLink = styled(Link)` 13 | grid-column: 2 / 3; 14 | width: auto; 15 | height: 50px; 16 | `; 17 | 18 | export default function Logo() { 19 | return ( 20 | 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /client/src/components/InputFields/InputDescription.js: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const InputDescription = styled.textarea` 4 | width: 95%; 5 | padding-left: 10px; 6 | background-color: transparent; 7 | box-shadow: 2px 2px 2px #000000; 8 | border: 1px solid ${props => props.theme.colors.primary}; 9 | border-radius: 5px; 10 | color: ${props => props.theme.colors.primary}; 11 | font-size: 16px; 12 | outline: none; 13 | &:hover, 14 | &:focus { 15 | transition: 0.5s; 16 | border: 1px solid ${props => props.theme.colors.tertiary}; 17 | transition: 0.2s; 18 | } 19 | `; 20 | -------------------------------------------------------------------------------- /client/src/components/InputFields/InputField.js: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const InputField = styled.input` 4 | width: 95%; 5 | height: 33px; 6 | padding: 10px; 7 | background-color: transparent; 8 | box-shadow: 2px 2px 2px #000000; 9 | border: 1px solid ${props => props.theme.colors.primary}; 10 | border-radius: 5px; 11 | color: ${props => props.theme.colors.primary}; 12 | font-size: 16px; 13 | outline: none; 14 | &:hover, 15 | &:focus { 16 | transition: 0.4s; 17 | border: 1px solid ${props => props.theme.colors.tertiary}; 18 | transition: 0.2s; 19 | } 20 | `; 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ticky 2 | 3 | ## Classify, analyze, and resolve with simplicity. 4 | 5 | With Ticky, you can create and use dynamic sections in your ticket post, to help your team filter tickets easily and resolve them faster. 6 | 7 | ## See Ticky in action 8 | 9 | Link - http://ticky-app.herokuapp.com/ 10 | 11 | ## Motivation 12 | 13 | This project is my final work for the intensive Web Development Course @neuefische in Cologne, which we will complete by the end of January 2020. 14 | 15 | ## How to use? 16 | 17 | Install client and server dependencies 18 | 19 | ``` 20 | npm install 21 | cd client 22 | npm install 23 | 24 | ``` 25 | -------------------------------------------------------------------------------- /client/src/components/InputFields/InputSelectBox.js: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const InputSelectBox = styled.select` 4 | width: 95%; 5 | height: 33px; 6 | padding: 5px; 7 | background-color: transparent; 8 | box-shadow: 2px 2px 2px #000000; 9 | border: 1px solid ${props => props.theme.colors.primary}; 10 | border-radius: 5px; 11 | color: ${props => props.theme.colors.primary}; 12 | font-size: 16px; 13 | outline: none; 14 | &:hover, 15 | &:focus { 16 | transition: 0.4s; 17 | border: 1px solid ${props => props.theme.colors.tertiary}; 18 | transition: 0.2s; 19 | } 20 | `; 21 | -------------------------------------------------------------------------------- /client/src/assets/icons/FilterIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | 4 | const Svg = styled.svg` 5 | fill: ${props => props.theme.colors.primary}; 6 | 7 | box-shadow: 0px 4px 10px 1px rgba(0, 0, 0, 0.2); 8 | &:hover { 9 | fill: ${props => props.theme.colors.tertiary}; 10 | } 11 | `; 12 | 13 | export default function FilterIcon(props) { 14 | return ( 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /client/src/assets/icons/LocationIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | 4 | const Svg = styled.svg` 5 | fill: ${props => props.theme.colors.primary}; 6 | box-shadow: 0px 4px 10px 1px rgba(0, 0, 0, 0.2); 7 | &:hover { 8 | fill: ${props => props.theme.colors.tertiary}; 9 | } 10 | `; 11 | 12 | export default function LocationIcon(props) { 13 | return ( 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /client/src/GlobalStyles.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Global, css } from '@emotion/core'; 3 | 4 | function GlobalStyles() { 5 | return ( 6 | css` 8 | *, 9 | *:before, 10 | *:after { 11 | box-sizing: border-box; 12 | } 13 | body { 14 | font-size: 16px; 15 | margin: 0; 16 | padding: 0; 17 | color: ${theme.colors.primary}; 18 | background-color: ${theme.colors.background}; 19 | font-family: 'Ubuntu', sans-serif; 20 | } 21 | `} 22 | /> 23 | ); 24 | } 25 | 26 | export default GlobalStyles; 27 | -------------------------------------------------------------------------------- /client/src/components/Filter Bar/Button.js: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const Button = styled.button` 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | align-content: center; 8 | width: 100%; 9 | height: 100%; 10 | background-color: transparent; 11 | color: ${props => props.theme.colors.primary}; 12 | outline: none; 13 | cursor: pointer; 14 | font-size: 14px; 15 | font-weight: 700; 16 | text-shadow: 1px 1px 1px 1px #000000; 17 | border: none; 18 | &:hover, 19 | &:active { 20 | background-color: ${props => props.theme.colors.background}; 21 | color: ${props => props.theme.colors.tertiary}; 22 | } 23 | `; 24 | -------------------------------------------------------------------------------- /client/src/assets/icons/DeleteIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | 4 | const Svg = styled.svg` 5 | fill: ${props => props.theme.colors.tertiary}; 6 | box-shadow: 0px 4px 10px 1px rgba(0, 0, 0, 0.2); 7 | &:hover { 8 | fill: ${props => props.theme.colors.primary}; 9 | } 10 | `; 11 | 12 | export default function CalendarIcon(props) { 13 | return ( 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /client/src/assets/icons/QuantityIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | 4 | const Svg = styled.svg` 5 | fill: ${props => props.theme.colors.primary}; 6 | box-shadow: 0px 4px 10px 1px rgba(0, 0, 0, 0.2); 7 | &:hover { 8 | fill: ${props => props.theme.colors.tertiary}; 9 | } 10 | `; 11 | 12 | export default function QuantityIcon(props) { 13 | return ( 14 | 21 | 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /client/src/assets/icons/CalendarIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | 4 | const Svg = styled.svg` 5 | fill: ${props => props.theme.colors.primary}; 6 | box-shadow: 0px 4px 10px 1px rgba(0, 0, 0, 0.2); 7 | &:hover { 8 | fill: ${props => props.theme.colors.tertiary}; 9 | } 10 | `; 11 | 12 | export default function CalendarIcon(props) { 13 | return ( 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /client/src/assets/icons/SerialIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | 4 | const Svg = styled.svg` 5 | fill: ${props => props.theme.colors.primary}; 6 | box-shadow: 0px 4px 10px 1px rgba(0, 0, 0, 0.2); 7 | &:hover { 8 | fill: ${props => props.theme.colors.tertiary}; 9 | } 10 | `; 11 | 12 | export default function SerialIcon(props) { 13 | return ( 14 | 21 | 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /client/src/assets/icons/UserIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | 4 | const Svg = styled.svg` 5 | fill: ${props => props.theme.colors.primary}; 6 | box-shadow: 0px 4px 10px 1px rgba(0, 0, 0, 0.2); 7 | &:hover { 8 | fill: ${props => props.theme.colors.tertiary}; 9 | } 10 | `; 11 | 12 | export default function SerialIcon(props) { 13 | return ( 14 | 21 | 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /client/src/assets/icons/EditIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | 4 | const Svg = styled.svg` 5 | fill: ${props => props.theme.colors.tertiary}; 6 | box-shadow: 0px 4px 10px 1px rgba(0, 0, 0, 0.2); 7 | background-color: transparent; 8 | &:hover { 9 | fill: ${props => props.theme.colors.primary}; 10 | } 11 | `; 12 | 13 | export default function EditIcon(props) { 14 | return ( 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /client/src/components/InputFields/SubmitButton.js: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const SubmitButton = styled.button` 4 | align-self: center; 5 | width: 35%; 6 | height: 33px; 7 | margin: 30px 0 30px 0; 8 | background-color: transparent; 9 | border: 1px solid ${props => props.theme.colors.primary}; 10 | border-radius: 5px; 11 | box-shadow: 2px 2px 2px #000000; 12 | color: ${props => props.theme.colors.primary}; 13 | font-size: 16px; 14 | text-transform: uppercase; 15 | text-shadow: 1px 1px 1px #000000; 16 | outline: none; 17 | &:hover, 18 | &:focus { 19 | transition: 0.5s; 20 | border: 1px solid ${props => props.theme.colors.tertiary}; 21 | transition: 0.2s; 22 | color: ${props => props.theme.colors.tertiary}; 23 | } 24 | `; 25 | -------------------------------------------------------------------------------- /client/src/themes/darkTheme.js: -------------------------------------------------------------------------------- 1 | const darkTheme = { 2 | colors: { 3 | background: '#191828', 4 | primary: '#EFEFEF', 5 | secondary: '#22A8B0', 6 | tertiary: '#5d38ff', 7 | elements_bg: '#222031', 8 | status_active: '#44FE76', 9 | status_active_hover: 'rgba(126,255,161,0.3)', 10 | status_in_progres: '#E8E200', 11 | status_in_progres_hover: 'rgba(232,226,0,0.3)', 12 | status_completed: '#727272', 13 | status_completed_hover: 'rgba(114,114,114,0.3)', 14 | progress: '#21F3FF', 15 | priority_normal: '#21AAFF', 16 | priority_normal_hover: 'rgba(33, 170, 255, 0.3)', 17 | priority_medium: '#FF9B00', 18 | priority_medium_hover: 'rgba(255, 155, 0, 0.3)', 19 | priority_high: '#FF434C', 20 | priority_high_hover: 'rgba(255, 67, 76, 0.3)' 21 | } 22 | }; 23 | 24 | export default darkTheme; 25 | -------------------------------------------------------------------------------- /client/src/assets/icons/ManufacturerIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | 4 | const Svg = styled.svg` 5 | fill: ${props => props.theme.colors.primary}; 6 | box-shadow: 0px 4px 10px 1px rgba(0, 0, 0, 0.2); 7 | &:hover { 8 | fill: ${props => props.theme.colors.tertiary}; 9 | } 10 | `; 11 | 12 | export default function ManufacturerIcon(props) { 13 | return ( 14 | 21 | 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /client/src/assets/icons/TimeDateIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | 4 | const Svg = styled.svg` 5 | fill: ${props => props.theme.colors.primary}; 6 | box-shadow: 0px 4px 10px 1px rgba(0, 0, 0, 0.2); 7 | &:hover { 8 | fill: ${props => props.theme.colors.tertiary}; 9 | } 10 | `; 11 | 12 | export default function TimeDateIcon(props) { 13 | return ( 14 | 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /client/src/components/Header/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import PropTypes from 'prop-types'; 4 | import Logo from '../Header/Logo'; 5 | 6 | const HeaderWrapper = styled.div` 7 | display: grid; 8 | grid-template-columns: 5% 20% 20% 25% 25% 5%; 9 | grid-template-rows: 80px; 10 | justify-items: center; 11 | align-items: center; 12 | height: 80px; 13 | width: 100%; 14 | margin-bottom: 1px; 15 | background-color: ${props => props.theme.colors.elements_bg}; 16 | box-shadow: 0px 4px 10px 0px rgba(0, 0, 0, 0.15); 17 | animation: fadeIn 1.5s; 18 | @keyframes fadeIn { 19 | from { 20 | opacity: 0; 21 | } 22 | to { 23 | opacity: 1; 24 | } 25 | } 26 | .fadeIn { 27 | animation-name: fadeIn; 28 | } 29 | `; 30 | 31 | export default function Header({ children }) { 32 | return ( 33 | 34 | 35 | {children} 36 | 37 | ); 38 | } 39 | 40 | Header.propTypes = { 41 | children: PropTypes.node 42 | }; 43 | -------------------------------------------------------------------------------- /client/src/components/Header/AddOrderButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | const ButtonWrapper = styled.div` 6 | grid-column: 4 / 5; 7 | `; 8 | 9 | const ButtonLink = styled(Link)` 10 | text-transform: uppercase; 11 | text-decoration: none; 12 | font-size: 18px; 13 | font-weight: 900; 14 | text-shadow: 1px 1px 1px #000000; 15 | color: ${props => props.theme.colors.primary}; 16 | &:hover, 17 | &:active { 18 | color: ${props => props.theme.colors.tertiary}; 19 | transition: 0.4s; 20 | } 21 | `; 22 | 23 | const Span = styled.span` 24 | font-size: 22px; 25 | margin-right: 3px; 26 | color: ${props => props.theme.colors.tertiary}; 27 | &:hover { 28 | color: ${props => props.theme.colors.primary}; 29 | } 30 | `; 31 | 32 | export default function AddOrderButton() { 33 | return ( 34 | 35 | 36 | +order 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /client/src/components/Header/AddTicketButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | const ButtonWrapper = styled.div` 6 | grid-column: 4 / 5; 7 | `; 8 | 9 | const ButtonLink = styled(Link)` 10 | text-transform: uppercase; 11 | text-decoration: none; 12 | font-size: 18px; 13 | font-weight: 900; 14 | text-shadow: 1px 1px 1px #000000; 15 | color: ${props => props.theme.colors.primary}; 16 | &:hover, 17 | &:active { 18 | color: ${props => props.theme.colors.tertiary}; 19 | transition: 0.4s; 20 | } 21 | `; 22 | 23 | const Span = styled.span` 24 | font-size: 22px; 25 | margin-right: 3px; 26 | color: ${props => props.theme.colors.tertiary}; 27 | &:hover { 28 | color: ${props => props.theme.colors.primary}; 29 | } 30 | `; 31 | 32 | export default function AddTicketButton() { 33 | return ( 34 | 35 | 36 | +ticket 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /client/src/components/Header/BackMainButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | const ButtonWrapper = styled.div` 6 | grid-column: 5 / 6; 7 | `; 8 | 9 | const ButtonLink = styled(Link)` 10 | display: flex; 11 | align-content: center; 12 | align-items: center; 13 | text-transform: uppercase; 14 | text-decoration: none; 15 | font-size: 18px; 16 | font-weight: 900; 17 | text-shadow: 1px 1px 1px #000000; 18 | color: ${props => props.theme.colors.primary}; 19 | &:hover, 20 | &:active { 21 | color: ${props => props.theme.colors.tertiary}; 22 | transition: 0.4s; 23 | } 24 | `; 25 | 26 | const Span = styled.span` 27 | font-size: 22px; 28 | margin-right: 3px; 29 | color: ${props => props.theme.colors.tertiary}; 30 | &:hover { 31 | color: ${props => props.theme.colors.primary}; 32 | } 33 | `; 34 | 35 | export default function BackTicketsButton() { 36 | return ( 37 | 38 | 39 | ←back 40 | 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /client/src/components/Header/BackOrdersButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | const ButtonWrapper = styled.div` 6 | grid-column: 5 / 6; 7 | `; 8 | 9 | const ButtonLink = styled(Link)` 10 | display: flex; 11 | align-content: center; 12 | align-items: center; 13 | text-transform: uppercase; 14 | text-decoration: none; 15 | font-size: 18px; 16 | font-weight: 900; 17 | text-shadow: 1px 1px 1px #000000; 18 | color: ${props => props.theme.colors.primary}; 19 | &:hover, 20 | &:active { 21 | color: ${props => props.theme.colors.tertiary}; 22 | transition: 0.4s; 23 | } 24 | `; 25 | 26 | const Span = styled.span` 27 | font-size: 22px; 28 | margin-right: 3px; 29 | color: ${props => props.theme.colors.tertiary}; 30 | &:hover { 31 | color: ${props => props.theme.colors.primary}; 32 | } 33 | `; 34 | 35 | export default function BackTicketsButton() { 36 | return ( 37 | 38 | 39 | ←back 40 | 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /client/src/components/Header/BackTicketsButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | const ButtonWrapper = styled.div` 6 | grid-column: 5 / 6; 7 | `; 8 | 9 | const ButtonLink = styled(Link)` 10 | display: flex; 11 | align-content: center; 12 | align-items: center; 13 | text-transform: uppercase; 14 | text-decoration: none; 15 | font-size: 18px; 16 | font-weight: 900; 17 | text-shadow: 1px 1px 1px #000000; 18 | color: ${props => props.theme.colors.primary}; 19 | &:hover, 20 | &:active { 21 | color: ${props => props.theme.colors.tertiary}; 22 | transition: 0.4s; 23 | } 24 | `; 25 | 26 | const Span = styled.span` 27 | font-size: 22px; 28 | margin-right: 3px; 29 | color: ${props => props.theme.colors.tertiary}; 30 | &:hover { 31 | color: ${props => props.theme.colors.primary}; 32 | } 33 | `; 34 | 35 | export default function BackTicketsButton() { 36 | return ( 37 | 38 | 39 | ←back 40 | 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /client/src/components/Header/OrdersListButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | const ButtonWrapper = styled.div` 6 | grid-column: 4 / 5; 7 | `; 8 | 9 | const ButtonLink = styled(Link)` 10 | display: flex; 11 | align-content: center; 12 | align-items: center; 13 | text-transform: uppercase; 14 | text-decoration: none; 15 | font-size: 18px; 16 | font-weight: 900; 17 | text-shadow: 1px 1px 1px #000000; 18 | color: ${props => props.theme.colors.primary}; 19 | &:hover, 20 | &:active { 21 | color: ${props => props.theme.colors.tertiary}; 22 | transition: 0.4s; 23 | } 24 | `; 25 | 26 | const Span = styled.span` 27 | font-size: 20px; 28 | margin-right: 5px; 29 | color: ${props => props.theme.colors.tertiary}; 30 | &:hover { 31 | color: ${props => props.theme.colors.primary}; 32 | } 33 | `; 34 | 35 | export default function OrdersListButton() { 36 | return ( 37 | 38 | 39 | ☶Orders 40 | 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ticky", 3 | "version": "1.0.0", 4 | "description": "Ticky Ticket App", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "cd client && npm run build", 8 | "client": "cd client && npm start", 9 | "server": "nodemon server.js", 10 | "storybook": "cd client && npm run storybook", 11 | "test": "cd client && npm test", 12 | "postinstall": "cd client && npm install", 13 | "start": "node server.js" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/panchepanevski/ticky.git" 18 | }, 19 | "keywords": [ 20 | "ticket", 21 | "app" 22 | ], 23 | "author": "Panche Panevski", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/panchepanevski/ticky/issues" 27 | }, 28 | "homepage": "https://github.com/panchepanevski/ticky#readme", 29 | "dependencies": { 30 | "eslint-plugin-react": "^7.17.0", 31 | "react-router-dom": "^5.1.2", 32 | "react-transition-group": "^4.3.0" 33 | }, 34 | "devDependencies": { 35 | "eslint": "^6.8.0", 36 | "json-server": "^0.15.1", 37 | "nodemon": "^2.0.2", 38 | "prettier": "^1.19.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /client/src/pages/OrderList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import Order from '../components/Order/Order'; 4 | import Header from '../components/Header/Header'; 5 | import AddOrderButton from '../components/Header/AddOrderButton'; 6 | import BackMainButton from '../components/Header/BackMainButton'; 7 | 8 | const Container = styled.div` 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | animation: fadeInUp 1s; 13 | @keyframes fadeInUp { 14 | from { 15 | opacity: 0; 16 | transform: translate3d(0, 100%, 0); 17 | } 18 | to { 19 | opacity: 1; 20 | transform: translate3d(0, 0, 0); 21 | } 22 | } 23 | .fadeInUp { 24 | animation-name: fadeInUp; 25 | } 26 | `; 27 | 28 | export default function OrderList() { 29 | const [orders, setOrders] = React.useState([]); 30 | 31 | async function fetchOrders() { 32 | const response = await fetch(`/api/orders`); 33 | const newOrders = await response.json(); 34 | setOrders(newOrders); 35 | } 36 | 37 | React.useEffect(() => { 38 | fetchOrders(); 39 | }, []); 40 | 41 | return ( 42 | <> 43 | 44 | 45 | 46 | 47 | 48 | {orders.map(order => ( 49 | 50 | ))} 51 | 52 | > 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ticky", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/core": "^10.0.22", 7 | "@emotion/styled": "^10.0.23", 8 | "@testing-library/jest-dom": "^4.2.4", 9 | "@testing-library/react": "^9.4.0", 10 | "@testing-library/user-event": "^7.1.2", 11 | "dotenv": "^8.2.0", 12 | "emotion-theming": "^10.0.19", 13 | "prop-types": "^15.7.2", 14 | "react": "^16.12.0", 15 | "react-dom": "^16.12.0", 16 | "react-router": "^5.1.2", 17 | "react-router-dom": "^5.1.2", 18 | "react-scripts": "3.3.0" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build-app": "react-scripts build", 23 | "build": "npm run build-app", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject", 26 | "storybook": "start-storybook -p 9009 -s public", 27 | "build-storybook": "build-storybook -s public -o build/storybook" 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | }, 41 | "devDependencies": { 42 | "@storybook/addon-actions": "^5.2.8", 43 | "@storybook/addon-links": "^5.2.8", 44 | "@storybook/addons": "^5.2.8", 45 | "@storybook/react": "^5.2.8" 46 | }, 47 | "proxy": "http://localhost:8080" 48 | } 49 | -------------------------------------------------------------------------------- /client/src/components/Order/Priority.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const backgroundColors = { 6 | normal: '#21AAFF', 7 | medium: '#FF9B00', 8 | high: '#FF434C' 9 | }; 10 | 11 | const PriorityBox = styled.select` 12 | width: 60px; 13 | height: 12px; 14 | margin-left: 10px; 15 | border: none; 16 | cursor: pointer; 17 | box-shadow: 0px 5px 5px 0px rgba(0, 0, 0, 0.15); 18 | color: transparent; 19 | background-color: ${props => backgroundColors[props.value]}; 20 | outline: none; 21 | `; 22 | 23 | export default function Priority({ orderId, value }) { 24 | const [priorityValue, setPriorityValue] = React.useState(value); 25 | 26 | async function handlePriority(value) { 27 | setPriorityValue(value); 28 | await fetch(`/api/orders/${orderId}`, { 29 | method: 'PATCH', 30 | headers: { 31 | 'Content-Type': 'application/json' 32 | }, 33 | body: JSON.stringify({ 34 | priority: value 35 | }) 36 | }); 37 | } 38 | 39 | return ( 40 | { 43 | handlePriority(event.target.value); 44 | }} 45 | > 46 | Normal 47 | Medium 48 | High 49 | 50 | ); 51 | } 52 | 53 | Priority.propTypes = { 54 | orderId: PropTypes.number, 55 | value: PropTypes.string 56 | }; 57 | -------------------------------------------------------------------------------- /client/src/components/Ticket/Priority.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const backgroundColors = { 6 | normal: '#21AAFF', 7 | medium: '#FF9B00', 8 | high: '#FF434C' 9 | }; 10 | 11 | const PriorityBox = styled.select` 12 | width: 60px; 13 | height: 12px; 14 | margin-left: 10px; 15 | border: none; 16 | cursor: pointer; 17 | box-shadow: 0px 5px 5px 0px rgba(0, 0, 0, 0.15); 18 | color: transparent; 19 | background-color: ${props => backgroundColors[props.value]}; 20 | outline: none; 21 | `; 22 | 23 | export default function Priority({ ticketId, value }) { 24 | const [priorityValue, setPriorityValue] = React.useState(value); 25 | 26 | async function handlePriority(value) { 27 | setPriorityValue(value); 28 | await fetch(`/api/tickets/${ticketId}`, { 29 | method: 'PATCH', 30 | headers: { 31 | 'Content-Type': 'application/json' 32 | }, 33 | body: JSON.stringify({ 34 | priority: value 35 | }) 36 | }); 37 | } 38 | 39 | return ( 40 | { 43 | handlePriority(event.target.value); 44 | }} 45 | > 46 | Normal 47 | Medium 48 | High 49 | 50 | ); 51 | } 52 | 53 | Priority.propTypes = { 54 | ticketId: PropTypes.number, 55 | value: PropTypes.string 56 | }; 57 | -------------------------------------------------------------------------------- /client/src/components/Ticket/Status.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const backgroundColors = { 6 | active: '#44FE76', 7 | inprogress: '#E8E200', 8 | completed: '#727272' 9 | }; 10 | 11 | const StatusBox = styled.select` 12 | width: 60px; 13 | height: 12px; 14 | margin-left: 10px; 15 | border: none; 16 | cursor: pointer; 17 | box-shadow: 0px 5px 5px 0px rgba(0, 0, 0, 0.15); 18 | background-color: ${props => backgroundColors[props.value]}; 19 | color: transparent; 20 | outline: none; 21 | `; 22 | 23 | export default function Status({ ticketId, value }) { 24 | const [statusValue, setStatusValue] = React.useState(value); 25 | 26 | async function handleStatus(value) { 27 | setStatusValue(value); 28 | await fetch(`/api/tickets/${ticketId}`, { 29 | method: 'PATCH', 30 | headers: { 31 | 'Content-Type': 'application/json' 32 | }, 33 | body: JSON.stringify({ 34 | status: value 35 | }) 36 | }); 37 | } 38 | 39 | return ( 40 | { 43 | handleStatus(event.target.value); 44 | }} 45 | > 46 | Active 47 | In Progress 48 | Completed 49 | 50 | ); 51 | } 52 | 53 | Status.propTypes = { 54 | ticketId: PropTypes.number, 55 | value: PropTypes.string 56 | }; 57 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 23 | 24 | 25 | 29 | 30 | Ticky 31 | 32 | 33 | You need to enable JavaScript to run this app. 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import GlobalStyles from './GlobalStyles'; 4 | import { ThemeProvider } from 'emotion-theming'; 5 | import darkTheme from './themes/darkTheme'; 6 | import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; 7 | import NewTicket from './pages/NewTicket'; 8 | import NewOrder from './pages/NewOrder'; 9 | import StartPage from './pages/StartPage'; 10 | import TicketList from './pages/TicketList'; 11 | import OrderList from './pages/OrderList'; 12 | import MainPage from './pages/MainPage'; 13 | 14 | const Main = styled.main` 15 | max-width: 600px; 16 | margin: 0 auto; 17 | `; 18 | 19 | function App() { 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | ); 50 | } 51 | 52 | export default App; 53 | -------------------------------------------------------------------------------- /client/src/pages/TicketList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useLocation } from 'react-router-dom'; 3 | import styled from '@emotion/styled'; 4 | import Ticket from '../components/Ticket/Ticket'; 5 | import FilterBar from '../components/Filter Bar/FilterBar'; 6 | import Header from '../components/Header/Header'; 7 | import AddTicketButton from '../components/Header/AddTicketButton'; 8 | import BackMainButton from '../components/Header/BackMainButton'; 9 | 10 | const Container = styled.div` 11 | display: flex; 12 | flex-direction: column; 13 | align-items: center; 14 | animation: fadeInUp 1s; 15 | @keyframes fadeInUp { 16 | from { 17 | opacity: 0; 18 | transform: translate3d(0, 100%, 0); 19 | } 20 | to { 21 | opacity: 1; 22 | transform: translate3d(0, 0, 0); 23 | } 24 | } 25 | .fadeInUp { 26 | animation-name: fadeInUp; 27 | } 28 | `; 29 | 30 | export default function TicketList() { 31 | const [tickets, setTickets] = React.useState([]); 32 | 33 | //FILTER 34 | let filter = useLocation().search; 35 | 36 | function useQuery() { 37 | return new URLSearchParams(useLocation().search); 38 | } 39 | //FILTER 40 | 41 | React.useEffect(() => { 42 | async function fetchTickets() { 43 | const response = await fetch(`/api/tickets${filter}`); 44 | const newTickets = await response.json(); 45 | setTickets(newTickets); 46 | } 47 | fetchTickets(); 48 | }, [filter]); 49 | 50 | useQuery(); 51 | 52 | return ( 53 | <> 54 | 55 | 56 | 57 | 58 | 59 | 60 | {tickets.map(ticket => ( 61 | 62 | ))} 63 | 64 | > 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /client/src/components/FilterBarOrders/Search.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | 4 | const Wrapper = styled.div` 5 | width: 100%; 6 | height: auto; 7 | display: grid; 8 | grid-template-columns: 2% 96% 2%; 9 | grid-template-rows: auto; 10 | `; 11 | 12 | const Form = styled.form` 13 | grid-area: 1 / 2 / 1 / 3; 14 | align-self: center; 15 | margin-top: 15px; 16 | `; 17 | 18 | const Input = styled.input` 19 | width: 100%; 20 | height: 33px; 21 | padding: 10px; 22 | background-color: transparent; 23 | box-shadow: 2px 2px 2px #000000; 24 | border: 1px solid ${props => props.theme.colors.primary}; 25 | border-radius: 5px; 26 | color: ${props => props.theme.colors.primary}; 27 | font-size: 16px; 28 | outline: none; 29 | &:hover, 30 | &:focus { 31 | transition: 0.4s; 32 | border: 1px solid ${props => props.theme.colors.tertiary}; 33 | transition: 0.2s; 34 | } 35 | `; 36 | 37 | function upperCaseFirstChar(string) { 38 | return string.charAt(0).toUpperCase() + string.slice(1); 39 | } 40 | 41 | export default function Search({ value, onChange }) { 42 | const [searchOrders, setSearchOrders] = React.useState(value); 43 | 44 | function handleSubmit(event) { 45 | event.preventDefault(); 46 | onChange(searchOrders); 47 | } 48 | 49 | React.useEffect(() => { 50 | const timeoutId = setTimeout(() => { 51 | onChange(searchOrders); 52 | }, 500); 53 | 54 | return () => { 55 | clearTimeout(timeoutId); 56 | }; 57 | }, [searchOrders]); 58 | 59 | return ( 60 | 61 | 62 | { 67 | const newSearchValue = upperCaseFirstChar(event.target.value); 68 | setSearchOrders(newSearchValue); 69 | }} 70 | /> 71 | 72 | 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /client/src/components/Ticket/Progress.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const Slider = styled.input` 6 | -webkit-appearance: none; 7 | width: 60%; 8 | height: 12px; 9 | border-radius: 5px; 10 | margin: 10px; 11 | background: linear-gradient( 12 | 90deg, 13 | rgb(33, 243, 255) ${props => props.value}%, 14 | rgb(239, 239, 239) 0% 15 | ); 16 | outline: none; 17 | transition: opacity 0.2s; 18 | box-shadow: 0px 5px 5px 0px rgba(0, 0, 0, 0.15); 19 | outline: none; 20 | 21 | &::-webkit-slider-thumb { 22 | -webkit-appearance: none; 23 | appearance: none; 24 | width: 20px; 25 | height: 20px; 26 | background: ${props => props.theme.colors.primary}; 27 | border-radius: 50%; 28 | border: 1px solid ${props => props.theme.elements_bg}; 29 | cursor: pointer; 30 | } 31 | 32 | &::-moz-range-thumb { 33 | width: 20px; 34 | height: 20px; 35 | background: ${props => props.theme.colors.primary}; 36 | border-radius: 50%; 37 | border: 1px solid ${props => props.theme.elements_bg}; 38 | cursor: pointer; 39 | } 40 | `; 41 | 42 | export default function Progress({ value, ticketId, id }) { 43 | const [progress, setProgress] = React.useState(value); 44 | const firstRender = React.useRef(true); 45 | 46 | function handleProgress(value) { 47 | setProgress(value); 48 | } 49 | 50 | React.useEffect(() => { 51 | if (firstRender.current) { 52 | firstRender.current = false; 53 | return; 54 | } 55 | const timeoutId = setTimeout(() => { 56 | fetch(`/api/tickets/${ticketId}`, { 57 | method: 'PATCH', 58 | headers: { 59 | 'Content-Type': 'application/json' 60 | }, 61 | body: JSON.stringify({ 62 | progress 63 | }) 64 | }); 65 | }, 200); 66 | 67 | return () => { 68 | clearTimeout(timeoutId); 69 | }; 70 | }, [progress, ticketId]); 71 | 72 | return ( 73 | <> 74 | { 81 | handleProgress(event.target.value); 82 | }} 83 | /> 84 | {progress} % 85 | > 86 | ); 87 | } 88 | 89 | Progress.propTypes = { 90 | ticketId: PropTypes.number, 91 | value: PropTypes.string, 92 | id: PropTypes.number 93 | }; 94 | -------------------------------------------------------------------------------- /client/src/pages/StartPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import logo from '../assets/logo/logoSmall.svg'; 4 | import { Redirect } from 'react-router-dom'; 5 | 6 | const PageWrapper = styled.div` 7 | width: 100vw; 8 | height: 100vh; 9 | max-width: 600px; 10 | margin: 0 auto; 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: center; 14 | align-items: center; 15 | align-content: center; 16 | `; 17 | const LogoWrapper = styled.img` 18 | width: auto; 19 | height: 150px; 20 | backface-visibility: visible !important; 21 | animation: flipIn 1s; 22 | @keyframes flipIn { 23 | from { 24 | transform: perspective(400px) rotate3d(1, 0, 0, 90deg); 25 | animation-timing-function: ease-in; 26 | opacity: 0; 27 | } 28 | 40% { 29 | transform: perspective(400px) rotate3d(1, 0, 0, -20deg); 30 | animation-timing-function: ease-in; 31 | } 32 | 60% { 33 | transform: perspective(400px) rotate3d(1, 0, 0, 10deg); 34 | opacity: 1; 35 | } 36 | 80% { 37 | transform: perspective(400px) rotate3d(1, 0, 0, -5deg); 38 | } 39 | to { 40 | transform: perspective(400px); 41 | } 42 | } 43 | `; 44 | const LogoText = styled.h1` 45 | font-size: 24px; 46 | letter-spacing: 9px; 47 | margin-left: 5px; 48 | animation: flipIn 1.3s; 49 | display: flex; 50 | justify-content: center; 51 | @keyframes flipIn { 52 | from { 53 | transform: perspective(400px) rotate3d(1, 0, 0, 90deg); 54 | animation-timing-function: ease-in; 55 | opacity: 0; 56 | } 57 | 40% { 58 | transform: perspective(400px) rotate3d(1, 0, 0, -20deg); 59 | animation-timing-function: ease-in; 60 | } 61 | 60% { 62 | transform: perspective(400px) rotate3d(1, 0, 0, 10deg); 63 | opacity: 1; 64 | } 65 | 80% { 66 | transform: perspective(400px) rotate3d(1, 0, 0, -5deg); 67 | } 68 | to { 69 | transform: perspective(400px); 70 | } 71 | } 72 | `; 73 | 74 | export default function StartPage() { 75 | const [redirect, setRedirect] = React.useState(false); 76 | 77 | function toTicketPage() { 78 | setTimeout(() => setRedirect(true), 1500); 79 | } 80 | 81 | toTicketPage(); 82 | 83 | return ( 84 | 85 | {redirect ? : true} 86 | 87 | TICKY 88 | 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /client/src/components/Filter Bar/DateFilter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useHistory, Link } from 'react-router-dom'; 3 | import styled from '@emotion/styled'; 4 | 5 | const Wrapper = styled.div` 6 | display: grid; 7 | grid-template-columns: 1% 70% 5% 24%; 8 | grid-template-rows: 1fr; 9 | width: auto; 10 | height: 40px; 11 | margin: 10px 0; 12 | `; 13 | 14 | const WrapperAllFilter = styled.div` 15 | grid-column: 4 / 5; 16 | `; 17 | 18 | const DateInput = styled.input` 19 | grid-column: 2 / 3; 20 | align-self: center; 21 | height: 30px; 22 | color: ${props => props.theme.colors.primary}; 23 | background-color: transparent; 24 | border: 1px solid ${props => props.theme.colors.primary}; 25 | border-radius: 5px; 26 | font-size: 16px; 27 | text-align: center; 28 | &:hover, 29 | &:focus { 30 | transition: 0.4s; 31 | border: 1px solid ${props => props.theme.colors.tertiary}; 32 | transition: 0.2s; 33 | } 34 | `; 35 | 36 | const Button = styled.button` 37 | width: 100%; 38 | height: 100%; 39 | background-color: transparent; 40 | color: ${props => props.theme.colors.primary}; 41 | outline: none; 42 | cursor: pointer; 43 | font-size: 12px; 44 | font-weight: 700; 45 | text-shadow: 1px 1px 1px 1px #000000; 46 | border: none; 47 | text-transform: uppercase; 48 | &:hover, 49 | &:active { 50 | background-color: ${props => props.theme.colors.background}; 51 | color: ${props => props.theme.colors.tertiary}; 52 | } 53 | `; 54 | 55 | const AllCircle = styled.div` 56 | position: absolute; 57 | width: 13px; 58 | height: 13px; 59 | border-radius: 30%; 60 | background-color: ${props => props.theme.colors.tertiary}; 61 | margin-top: 1px; 62 | margin-right: -20px; 63 | `; 64 | 65 | const LinkQuery = styled(Link)` 66 | text-transform: uppercase; 67 | text-decoration: none; 68 | font-size: 12px; 69 | font-weight: 700; 70 | text-shadow: 1px 1px 1px #000000; 71 | color: ${props => props.theme.colors.primary}; 72 | `; 73 | 74 | const ONE_DAY = 1000 * 60 * 60 * 24; 75 | export default function DateFilter() { 76 | let history = useHistory(); 77 | 78 | function handleChange(value) { 79 | const testDate = new Date(value); 80 | history.push( 81 | `/tickets/?timestamp_gte=${testDate.getTime()}×tamp_lte=${testDate.getTime() + 82 | ONE_DAY - 83 | 1}` 84 | ); 85 | } 86 | 87 | return ( 88 | 89 | handleChange(event.target.value)} /> 90 | 91 | 92 | 93 | All 94 | 95 | 96 | 97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /client/src/components/Filter Bar/LocationFilter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useHistory, Link } from 'react-router-dom'; 3 | import styled from '@emotion/styled'; 4 | 5 | const Wrapper = styled.div` 6 | display: grid; 7 | grid-template-columns: 1% 70% 5% 20%; 8 | grid-template-rows: 1fr; 9 | width: auto; 10 | height: 40px; 11 | margin: 10px 0; 12 | `; 13 | 14 | const WrapperAllFilter = styled.div` 15 | grid-column: 4 / 5; 16 | `; 17 | 18 | const SelectBox = styled.select` 19 | align-self: center; 20 | grid-column: 2 / 3; 21 | height: 30px; 22 | padding: 1px; 23 | background-color: transparent; 24 | box-shadow: 2px 2px 2px #000000; 25 | border: 1px solid ${props => props.theme.colors.primary}; 26 | border-radius: 5px; 27 | color: ${props => props.theme.colors.primary}; 28 | font-size: 16px; 29 | outline: none; 30 | &:hover, 31 | &:focus { 32 | transition: 0.4s; 33 | border: 1px solid ${props => props.theme.colors.tertiary}; 34 | transition: 0.2s; 35 | } 36 | `; 37 | 38 | const Button = styled.button` 39 | width: 100%; 40 | height: 100%; 41 | background-color: transparent; 42 | color: ${props => props.theme.colors.primary}; 43 | outline: none; 44 | cursor: pointer; 45 | font-size: 12px; 46 | font-weight: 700; 47 | text-shadow: 1px 1px 1px 1px #000000; 48 | border: none; 49 | text-transform: uppercase; 50 | &:hover, 51 | &:active { 52 | background-color: ${props => props.theme.colors.background}; 53 | color: ${props => props.theme.colors.tertiary}; 54 | } 55 | `; 56 | 57 | const AllCircle = styled.div` 58 | position: absolute; 59 | width: 13px; 60 | height: 13px; 61 | border-radius: 30%; 62 | background-color: ${props => props.theme.colors.tertiary}; 63 | margin-top: 1px; 64 | margin-right: -20px; 65 | `; 66 | 67 | const LinkQuery = styled(Link)` 68 | text-transform: uppercase; 69 | text-decoration: none; 70 | font-size: 12px; 71 | font-weight: 700; 72 | text-shadow: 1px 1px 1px #000000; 73 | color: ${props => props.theme.colors.primary}; 74 | `; 75 | 76 | export default function LocationFilter() { 77 | let history = useHistory(); 78 | 79 | function handleChange(value) { 80 | history.push(`/tickets/?location=${value}`); 81 | } 82 | 83 | return ( 84 | 85 | handleChange(event.target.value)}> 86 | Select Location 87 | East Building 88 | West Building 89 | South Building 90 | North Building 91 | 92 | 93 | 94 | 95 | All 96 | 97 | 98 | 99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /client/src/pages/MainPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import logo from '../assets/logo/logoSmall.svg'; 4 | import { Link } from 'react-router-dom'; 5 | 6 | const PageWrapper = styled.div` 7 | width: 100vw; 8 | height: 100vh; 9 | max-width: 600px; 10 | margin: 0 auto; 11 | display: flex; 12 | flex-direction: column; 13 | `; 14 | const LogoWrapper = styled.img` 15 | margin-top: 70px; 16 | width: auto; 17 | height: 70px; 18 | backface-visibility: visible !important; 19 | animation: fadeInDown 1.5s; 20 | @keyframes fadeInDown { 21 | from { 22 | opacity: 0; 23 | transform: translate3d(0, -100%, 0); 24 | } 25 | to { 26 | opacity: 1; 27 | transform: translate3d(0, 0, 0); 28 | } 29 | } 30 | .fadeInDown { 31 | animation-name: fadeInDown; 32 | } 33 | `; 34 | 35 | const LogoText = styled.h1` 36 | font-size: 14px; 37 | letter-spacing: 3px; 38 | margin-left: 5px; 39 | display: flex; 40 | justify-content: center; 41 | animation: fadeInDown 1.8s; 42 | @keyframes fadeInDown { 43 | from { 44 | opacity: 0; 45 | transform: translate3d(0, -100%, 0); 46 | } 47 | to { 48 | opacity: 1; 49 | transform: translate3d(0, 0, 0); 50 | } 51 | } 52 | .fadeInDown { 53 | animation-name: fadeInDown; 54 | } 55 | `; 56 | 57 | const LinksWrapper = styled.div` 58 | margin-top: 150px; 59 | `; 60 | 61 | const Links = styled(Link)` 62 | display: flex; 63 | justify-content: center; 64 | padding: 18px; 65 | text-transform: uppercase; 66 | text-decoration: none; 67 | font-size: 25px; 68 | font-weight: 900; 69 | letter-spacing: 3px; 70 | text-shadow: 1px 1px 1px #000000; 71 | margin: 20px; 72 | color: ${props => props.theme.colors.primary}; 73 | animation: fadeInUp 1.5s; 74 | &:hover, 75 | &:active { 76 | color: ${props => props.theme.colors.tertiary}; 77 | transition: 0.4s; 78 | } 79 | @keyframes fadeInUp { 80 | from { 81 | opacity: 0; 82 | transform: translate3d(0, 100%, 0); 83 | } 84 | to { 85 | opacity: 1; 86 | transform: translate3d(0, 0, 0); 87 | } 88 | } 89 | .fadeInUp { 90 | animation-name: fadeInUp; 91 | } 92 | `; 93 | 94 | const Span = styled.span` 95 | font-size: 24px; 96 | margin-left: 1px; 97 | color: ${props => props.theme.colors.tertiary}; 98 | &:hover { 99 | color: ${props => props.theme.colors.primary}; 100 | } 101 | `; 102 | 103 | export default function StartPage() { 104 | return ( 105 | 106 | 107 | TICKY 108 | 109 | 110 | Tickets› 111 | 112 | 113 | Orders› 114 | 115 | 116 | 117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /client/src/components/InputFields/RadioButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | 4 | const Input = styled.input` 5 | margin: 7px; 6 | opacity: 0; 7 | position: absolute; 8 | `; 9 | 10 | const Label = styled.label` 11 | width: 110px; 12 | height: 28px; 13 | display: flex; 14 | justify-content: center; 15 | align-items: center; 16 | margin: 1px; 17 | font-size: 14px; 18 | text-shadow: 1px 1px 1px #000000; 19 | background-color: ${props => { 20 | if (props.field === 'Active' && props.active) { 21 | return props.theme.colors.status_active_hover; 22 | } else if (props.field === 'In Progress' && props.active) { 23 | return props.theme.colors.status_in_progres_hover; 24 | } else if (props.field === 'Completed' && props.active) { 25 | return props.theme.colors.status_completed_hover; 26 | } else if (props.field === 'Normal' && props.active) { 27 | return props.theme.colors.priority_normal_hover; 28 | } else if (props.field === 'Medium' && props.active) { 29 | return props.theme.colors.priority_medium_hover; 30 | } else if (props.field === 'High' && props.active) { 31 | return props.theme.colors.priority_high_hover; 32 | } 33 | return props.field === 'Active' && props.active; 34 | }}; 35 | border-radius: 5px; 36 | border: 1px solid 37 | ${props => { 38 | if (props.field === 'Active') { 39 | return props.theme.colors.status_active; 40 | } else if (props.field === 'In Progress') { 41 | return props.theme.colors.status_in_progres; 42 | } else if (props.field === 'Completed') { 43 | return props.theme.colors.status_completed; 44 | } else if (props.field === 'Normal') { 45 | return props.theme.colors.priority_normal; 46 | } else if (props.field === 'Medium') { 47 | return props.theme.colors.priority_medium; 48 | } else if (props.field === 'High') { 49 | return props.theme.colors.priority_high; 50 | } 51 | return props.field === 'Active'; 52 | }}; 53 | &:hover, 54 | &:active { 55 | background-color: ${props => { 56 | if (props.field === 'Active') { 57 | return props.theme.colors.status_active_hover; 58 | } else if (props.field === 'In Progress') { 59 | return props.theme.colors.status_in_progres_hover; 60 | } else if (props.field === 'Completed') { 61 | return props.theme.colors.status_completed_hover; 62 | } else if (props.field === 'Normal') { 63 | return props.theme.colors.priority_normal_hover; 64 | } else if (props.field === 'Medium') { 65 | return props.theme.colors.priority_medium_hover; 66 | } else if (props.field === 'High') { 67 | return props.theme.colors.priority_high_hover; 68 | } 69 | return props.field === 'Active'; 70 | }}; 71 | } 72 | `; 73 | 74 | export default function RadioButton({ name, value, field, active }) { 75 | return ( 76 | <> 77 | 78 | {field} 79 | 80 | 81 | > 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /client/src/components/Filter Bar/PriorityFilter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import styled from '@emotion/styled'; 4 | 5 | const Wrapper = styled.div` 6 | display: grid; 7 | grid-template-columns: 1% 27% 26% 21% 19%; 8 | grid-template-rows: 1fr; 9 | width: auto; 10 | height: 40px; 11 | margin: 10px 0; 12 | `; 13 | 14 | const WrapperNormal = styled.div` 15 | grid-column: 2 / 3; 16 | `; 17 | 18 | const WrapperMedium = styled.div` 19 | grid-column: 3 / 4; 20 | `; 21 | 22 | const WrapperHigh = styled.div` 23 | grid-column: 4 / 5; 24 | `; 25 | 26 | const WrapperAll = styled.div` 27 | grid-column: 5 / 6; 28 | `; 29 | 30 | const Button = styled.button` 31 | width: 100%; 32 | height: 100%; 33 | background-color: transparent; 34 | color: ${props => props.theme.colors.primary}; 35 | outline: none; 36 | cursor: pointer; 37 | font-size: 12px; 38 | font-weight: 700; 39 | text-shadow: 1px 1px 1px 1px #000000; 40 | border: none; 41 | text-transform: uppercase; 42 | &:hover, 43 | &:active { 44 | background-color: ${props => props.theme.colors.background}; 45 | color: ${props => props.theme.colors.tertiary}; 46 | } 47 | `; 48 | 49 | const NormalCircle = styled.div` 50 | position: absolute; 51 | grid-column: 2 / 3; 52 | width: 13px; 53 | height: 13px; 54 | border-radius: 30%; 55 | background-color: ${props => props.theme.colors.priority_normal}; 56 | margin-top: 1px; 57 | margin-right: 12px; 58 | `; 59 | 60 | const MediumCircle = styled.div` 61 | position: absolute; 62 | grid-column: 2 / 3; 63 | width: 13px; 64 | height: 13px; 65 | border-radius: 30%; 66 | background-color: ${props => props.theme.colors.priority_medium}; 67 | margin-top: 1px; 68 | `; 69 | 70 | const HighCircle = styled.div` 71 | position: absolute; 72 | grid-column: 2 / 3; 73 | width: 13px; 74 | height: 13px; 75 | border-radius: 30%; 76 | background-color: ${props => props.theme.colors.priority_high}; 77 | margin-top: 1px; 78 | margin-right: -20px; 79 | `; 80 | 81 | const AllCircle = styled.div` 82 | position: absolute; 83 | grid-column: 2 / 3; 84 | width: 13px; 85 | height: 13px; 86 | border-radius: 30%; 87 | background-color: ${props => props.theme.colors.tertiary}; 88 | margin-top: 1px; 89 | margin-right: -20px; 90 | `; 91 | 92 | const LinkQuery = styled(Link)` 93 | text-transform: uppercase; 94 | text-decoration: none; 95 | font-size: 12px; 96 | font-weight: 700; 97 | text-shadow: 1px 1px 1px #000000; 98 | color: ${props => props.theme.colors.primary}; 99 | &:hover, 100 | &:active { 101 | color: ${props => props.theme.colors.tertiary}; 102 | } 103 | `; 104 | 105 | export default function PriorityFilter() { 106 | return ( 107 | 108 | 109 | 110 | 111 | Normal 112 | 113 | 114 | 115 | 116 | 117 | Medium 118 | 119 | 120 | 121 | 122 | 123 | High 124 | 125 | 126 | 127 | 128 | 129 | All 130 | 131 | 132 | 133 | ); 134 | } 135 | -------------------------------------------------------------------------------- /client/src/components/Filter Bar/StatusFilter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import styled from '@emotion/styled'; 4 | 5 | const Wrapper = styled.div` 6 | display: grid; 7 | grid-template-columns: 0.1% 23% 33% 31% 18%; 8 | grid-template-rows: 1fr; 9 | width: auto; 10 | height: 40px; 11 | margin: 10px 0; 12 | `; 13 | 14 | const WrapperActive = styled.div` 15 | grid-column: 2 / 3; 16 | `; 17 | 18 | const WrapperInProgress = styled.div` 19 | grid-column: 3 / 4; 20 | `; 21 | 22 | const WrapperCompleted = styled.div` 23 | grid-column: 4 / 5; 24 | `; 25 | 26 | const WrapperAll = styled.div` 27 | grid-column: 5 / 6; 28 | `; 29 | 30 | const Button = styled.button` 31 | width: 100%; 32 | height: 100%; 33 | background-color: transparent; 34 | color: ${props => props.theme.colors.primary}; 35 | outline: none; 36 | cursor: pointer; 37 | font-size: 12px; 38 | font-weight: 700; 39 | text-shadow: 1px 1px 1px 1px #000000; 40 | border: none; 41 | text-transform: uppercase; 42 | &:hover, 43 | &:active { 44 | background-color: ${props => props.theme.colors.background}; 45 | color: ${props => props.theme.colors.tertiary}; 46 | } 47 | `; 48 | 49 | const ActiveCircle = styled.div` 50 | position: absolute; 51 | grid-column: 2 / 3; 52 | width: 13px; 53 | height: 13px; 54 | border-radius: 30%; 55 | background-color: ${props => props.theme.colors.status_active}; 56 | margin-top: 1px; 57 | margin-right: 12px; 58 | `; 59 | 60 | const InProgressCircle = styled.div` 61 | position: absolute; 62 | grid-column: 2 / 3; 63 | width: 13px; 64 | height: 13px; 65 | border-radius: 30%; 66 | background-color: ${props => props.theme.colors.status_in_progres}; 67 | margin-top: 1px; 68 | `; 69 | 70 | const CompletedCircle = styled.div` 71 | position: absolute; 72 | grid-column: 2 / 3; 73 | width: 13px; 74 | height: 13px; 75 | border-radius: 30%; 76 | background-color: ${props => props.theme.colors.status_completed}; 77 | margin-top: 1px; 78 | margin-right: -20px; 79 | `; 80 | 81 | const AllCircle = styled.div` 82 | position: absolute; 83 | grid-column: 2 / 3; 84 | width: 13px; 85 | height: 13px; 86 | border-radius: 30%; 87 | background-color: ${props => props.theme.colors.tertiary}; 88 | margin-top: 1px; 89 | margin-right: -20px; 90 | `; 91 | 92 | const LinkQuery = styled(Link)` 93 | text-transform: uppercase; 94 | text-decoration: none; 95 | font-size: 12px; 96 | font-weight: 700; 97 | text-shadow: 1px 1px 1px #000000; 98 | color: ${props => props.theme.colors.primary}; 99 | &:hover, 100 | &:active { 101 | color: ${props => props.theme.colors.tertiary}; 102 | } 103 | `; 104 | 105 | export default function StatusFilter() { 106 | return ( 107 | 108 | 109 | 110 | 111 | Active 112 | 113 | 114 | 115 | 116 | 117 | In progress 118 | 119 | 120 | 121 | 122 | 123 | Completed 124 | 125 | 126 | 127 | 128 | 129 | All 130 | 131 | 132 | 133 | ); 134 | } 135 | -------------------------------------------------------------------------------- /client/src/components/Filter Bar/FilterBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import { Button } from './Button'; 4 | import CalendarIcon from '../../assets/icons/CalendarIcon'; 5 | import LocationIcon from '../../assets/icons/LocationIcon'; 6 | import FilterIcon from '../../assets/icons/FilterIcon'; 7 | import PriorityFilter from './PriorityFilter'; 8 | import StatusFilter from './StatusFilter'; 9 | import LocationFilter from './LocationFilter'; 10 | import DateFilter from './DateFilter'; 11 | 12 | const Wrapper = styled.div` 13 | display: grid; 14 | grid-template-columns: 10% 30% 30% 15% 15%; 15 | grid-template-rows: auto auto; 16 | justify-items: center; 17 | align-items: center; 18 | height: 30px; 19 | width: 100%; 20 | margin-bottom: 25px; 21 | border-top: 1px solid ${props => props.theme.colors.background}; 22 | background-color: ${props => props.theme.colors.elements_bg}; 23 | animation: fadeIn 1.5s; 24 | @keyframes fadeIn { 25 | from { 26 | opacity: 0; 27 | } 28 | to { 29 | opacity: 1; 30 | } 31 | } 32 | .fadeIn { 33 | animation-name: fadeIn; 34 | } 35 | `; 36 | 37 | const IconWrapper = styled.div` 38 | grid-column: 1 / 2; 39 | grid-row: 1 / 2; 40 | width: 100%; 41 | height: 100%; 42 | justify-self: center; 43 | align-self: center; 44 | box-shadow: 0px 4px 10px 1px rgba(0, 0, 0, 0.1); 45 | &:hover, 46 | &:active { 47 | color: ${props => props.theme.colors.tertiary}; 48 | } 49 | `; 50 | 51 | const PriorityWrapper = styled.div` 52 | grid-column: 2 / 3; 53 | grid-row: 1 / 2; 54 | justify-self: center; 55 | width: 100%; 56 | height: 100%; 57 | box-shadow: 0px 4px 10px 1px rgba(0, 0, 0, 0.1); 58 | &:hover { 59 | color: ${props => props.theme.colors.tertiary}; 60 | } 61 | `; 62 | 63 | const StatusWrapper = styled.div` 64 | grid-column: 3 / 4; 65 | grid-row: 1 / 2; 66 | justify-self: center; 67 | width: 100%; 68 | height: 100%; 69 | box-shadow: 0px 4px 10px 1px rgba(0, 0, 0, 0.1); 70 | `; 71 | 72 | const DateWrapper = styled.div` 73 | grid-column: 4 / 5; 74 | grid-row: 1 / 2; 75 | justify-self: center; 76 | width: 100%; 77 | height: 100%; 78 | box-shadow: 0px 4px 10px 1px rgba(0, 0, 0, 0.1); 79 | `; 80 | 81 | const LocationWrapper = styled.div` 82 | grid-column: 5 / 6; 83 | grid-row: 1 / 2; 84 | justify-self: center; 85 | width: 100%; 86 | height: 100%; 87 | box-shadow: 0px 4px 10px 1px rgba(0, 0, 0, 0.1); 88 | `; 89 | 90 | const Details = styled.div` 91 | display: ${props => (props.show ? 'block' : 'none')}; 92 | grid-column: 1 / 6; 93 | grid-row: 2 / 3; 94 | width: 100%; 95 | height: 30px; 96 | `; 97 | 98 | export default function FilterBar() { 99 | const [activeDetail, setActiveDetail] = React.useState(null); 100 | 101 | function handleClick(filter) { 102 | if (filter === activeDetail) { 103 | setActiveDetail(null); 104 | } else { 105 | setActiveDetail(filter); 106 | } 107 | } 108 | 109 | return ( 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | handleClick('priority')}>PRIORITY 118 | 119 | 120 | 121 | 122 | 123 | 124 | handleClick('status')}>STATUS 125 | 126 | 127 | 128 | 129 | 130 | handleClick('date')}> 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | handleClick('location')}> 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | ); 147 | } 148 | -------------------------------------------------------------------------------- /client/src/pages/NewOrder.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import { InputLabel } from '../components/InputFields/InputLabel'; 4 | import { InputField } from '../components/InputFields/InputField'; 5 | import { InputDescription } from '../components/InputFields/InputDescription'; 6 | import { SubmitButton } from '../components/InputFields/SubmitButton'; 7 | import { TextStyledH1 } from '../components/TextStyledH1'; 8 | import RadioButton from '../components/InputFields/RadioButton'; 9 | import Header from '../components/Header/Header'; 10 | import BackOrderButton from '../components/Header/BackOrdersButton'; 11 | 12 | const InputForm = styled.form` 13 | display: flex; 14 | flex-direction: column; 15 | margin: 15px 0 0 15px; 16 | `; 17 | 18 | const WrapperRadioButtons = styled.div` 19 | display: flex; 20 | justify-content: space-around; 21 | width: 98%; 22 | `; 23 | 24 | export default function AddOrder() { 25 | const [name, setName] = React.useState(''); 26 | const [serial, setSerial] = React.useState(''); 27 | const [manufacturer, setManufacturer] = React.useState(''); 28 | const [quantity, setQuantity] = React.useState(''); 29 | const [orderedby, setOrderedby] = React.useState(''); 30 | const [priority, setPriority] = React.useState(''); 31 | const [description, setDescription] = React.useState(''); 32 | 33 | async function handleSubmit(event) { 34 | event.preventDefault(); 35 | await fetch('/api/orders', { 36 | method: 'POST', 37 | headers: { 38 | 'Content-Type': 'application/json' 39 | }, 40 | body: JSON.stringify({ 41 | name, 42 | serial, 43 | manufacturer, 44 | quantity, 45 | orderedby, 46 | priority, 47 | description 48 | }) 49 | }); 50 | 51 | setName(''); 52 | setSerial(''); 53 | setManufacturer(''); 54 | setQuantity(''); 55 | setOrderedby(''); 56 | setPriority(''); 57 | setDescription(''); 58 | } 59 | 60 | return ( 61 | <> 62 | 63 | 64 | 65 | 66 | Add Order 67 | Name of the required part 68 | setName(event.target.value)} 72 | autoFocus 73 | required 74 | /> 75 | Serial Number 76 | setSerial(event.target.value)} 80 | required 81 | /> 82 | Manufacturer 83 | setManufacturer(event.target.value)} 87 | required 88 | /> 89 | Quantity 90 | setQuantity(event.target.value)} 94 | required 95 | /> 96 | Ordered by 97 | setOrderedby(event.target.value)} 101 | required 102 | /> 103 | Priority 104 | setPriority(event.target.value)}> 105 | 112 | Normal 113 | 114 | 121 | Medium 122 | 123 | 130 | High 131 | 132 | 133 | Description 134 | setDescription(event.target.value)} 139 | required 140 | /> 141 | Add Order 142 | 143 | > 144 | ); 145 | } 146 | -------------------------------------------------------------------------------- /db.json: -------------------------------------------------------------------------------- 1 | { 2 | "tickets": [ 3 | { 4 | "name": "Replace Hard Drive on #2 PC in West Building", 5 | "timestamp": 1578848254000, 6 | "assigned": "Panche Panevski", 7 | "location": "west", 8 | "status": "inprogress", 9 | "priority": "normal", 10 | "details": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.", 11 | "id": 1, 12 | "progress": "49" 13 | }, 14 | { 15 | "name": "Change network cable", 16 | "timestamp": 1578848254000, 17 | "assigned": "Panche Panevski", 18 | "location": "south", 19 | "status": "active", 20 | "priority": "normal", 21 | "details": "To ensure a robust fast LAN connection to easily transmit high data rates between IT system units, so they are ideally suited for Internet cable wiring in your home or office.", 22 | "id": 2, 23 | "progress": "0" 24 | }, 25 | { 26 | "name": "Replace the fuse in part 1 of machine 55 in section 3 ", 27 | "timestamp": 1578848254000, 28 | "assigned": "Panche", 29 | "location": "west", 30 | "status": "completed", 31 | "priority": "medium", 32 | "details": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s.", 33 | "id": 3, 34 | "progress": "100" 35 | }, 36 | { 37 | "name": "Fix WiFi signal ", 38 | "timestamp": 1579021864784, 39 | "assigned": "Panche Panevski", 40 | "location": "east-building", 41 | "status": "active", 42 | "priority": "high", 43 | "details": "While not as annoyingly frequent as app updates, router manufactures sometimes issue stability resolutions via driver updates. Go to the manufacturer’s website to download all new maintenance updates and changes.", 44 | "id": 4, 45 | "progress": "0" 46 | }, 47 | { 48 | "name": "Replace Printer Cable", 49 | "timestamp": 1579091868166, 50 | "assigned": "Panche Panevski", 51 | "location": "east-building", 52 | "status": "inprogress", 53 | "priority": "medium", 54 | "details": "Replace printer cable on a #102 printer in production station #13", 55 | "id": 10, 56 | "progress": "76" 57 | }, 58 | { 59 | "name": "Replace printer cable in building #30", 60 | "timestamp": 1579096245752, 61 | "assigned": "Panche", 62 | "location": "east-building", 63 | "status": "active", 64 | "priority": "high", 65 | "details": "Replace printer cable in building #30", 66 | "id": 21, 67 | "progress": "0" 68 | } 69 | ], 70 | "orders": [ 71 | { 72 | "name": "Network cable 1200 MHz", 73 | "serial": "017647681760", 74 | "manufacturer": "Delacon", 75 | "quantity": "3", 76 | "orderedby": "Panche Panevski", 77 | "priority": "normal", 78 | "description": "These LAN cables have twisted-pair copper wire, making generic cabling of customer premises according to ISO/IEC 11801 and/or EN 50173 their ideal field of application.", 79 | "id": 1 80 | }, 81 | { 82 | "name": "End product verification camera", 83 | "serial": "12344444776366 STR", 84 | "manufacturer": "Bosch", 85 | "quantity": "10", 86 | "orderedby": "Panche Panevski", 87 | "priority": "high", 88 | "description": "To ensure that errors are prevented as early in the automated system as possible, a verification must occur before a part enters the system. A verification step should occur after a part is marked or labeled with a barcode and before the part reaches the station where the barcode is first to read.", 89 | "id": 2 90 | }, 91 | { 92 | "name": "Router TP Link R1005", 93 | "serial": "RST 2126767", 94 | "manufacturer": "TP Link", 95 | "quantity": "1", 96 | "orderedby": "Panche Panevski", 97 | "priority": "medium", 98 | "description": "Need to be changed in Breiter Strasse, company SPACES", 99 | "id": 3 100 | }, 101 | { 102 | "name": "Sensor Bosch 1200 SMT", 103 | "serial": "SMT 12898966554", 104 | "manufacturer": "Bosch", 105 | "quantity": "5", 106 | "orderedby": "Panche Panevski", 107 | "priority": "normal", 108 | "description": "The sensor needs to be replaced in Neff Bretten, part Kleberei. ", 109 | "id": 4 110 | }, 111 | { 112 | "name": "Printer cable 1002", 113 | "serial": "3456637 SNGH ", 114 | "manufacturer": "Delacon", 115 | "quantity": "1", 116 | "orderedby": "Panche Panevski", 117 | "priority": "high", 118 | "description": "Replace printer cable on a #102 printer in production station #13", 119 | "id": 5 120 | }, 121 | { 122 | "name": "Replace printer cable in production station #13", 123 | "serial": "8273827382", 124 | "manufacturer": "Delacon", 125 | "quantity": "1", 126 | "orderedby": "Panche Panevski", 127 | "priority": "normal", 128 | "description": "Replace printer cable in production station #13", 129 | "id": 8 130 | } 131 | ] 132 | } -------------------------------------------------------------------------------- /client/src/components/Order/Order.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import PropTypes from 'prop-types'; 4 | import { Title } from '../Ticket/Title'; 5 | import Priority from './Priority'; 6 | import ManufacturerIcon from '../../assets/icons/ManufacturerIcon'; 7 | import QuantityIcon from '../../assets/icons/QuantityIcon'; 8 | import SerialIcon from '../../assets/icons/SerialIcon'; 9 | import DeleteIcon from '../../assets/icons/DeleteIcon'; 10 | 11 | const Container = styled.div` 12 | display: grid; 13 | grid-template-columns: 10% 10% 10% 10% 10% 10% 10% 10% 10% 10%; 14 | grid-template-rows: auto auto auto auto auto; 15 | justify-self: center; 16 | width: 98%; 17 | height: auto; 18 | background-color: ${props => props.theme.colors.elements_bg}; 19 | margin-top: 30px; 20 | padding: 10px; 21 | border-radius: 10px; 22 | box-shadow: 0px 10px 5px 0px rgba(0, 0, 0, 0.15); 23 | `; 24 | 25 | const NameWrapper = styled.div` 26 | grid-area: 1 / 1 / 2 / 10; 27 | margin: 5px 0; 28 | `; 29 | 30 | const OrderedByWrapper = styled.div` 31 | grid-area: 2 / 1 / 3 / 10; 32 | `; 33 | 34 | const PriorityWrapper = styled.div` 35 | grid-area: 3 / 1 / 4 / 11; 36 | margin: 5px 0; 37 | `; 38 | 39 | const DescriptionWrapper = styled.div` 40 | grid-area: 4 / 1 / 5 / 6; 41 | cursor: pointer; 42 | margin: 15px 0; 43 | &:hover { 44 | color: ${props => props.theme.colors.tertiary}; 45 | } 46 | `; 47 | 48 | const IconWrapper = styled.div` 49 | grid-row: 4 / 5; 50 | justify-self: center; 51 | align-self: center; 52 | cursor: pointer; 53 | `; 54 | 55 | const ManufacturerWrapper = styled(IconWrapper)` 56 | grid-column: 5 / 7; 57 | `; 58 | 59 | const SerialWrapper = styled(IconWrapper)` 60 | grid-column: 7 / 9; 61 | `; 62 | 63 | const QuantityWrapper = styled(IconWrapper)` 64 | grid-column: 9 / 11; 65 | `; 66 | 67 | const BorderLine = styled.div` 68 | grid-area: 4 / 1 / 5 / 11; 69 | border-bottom: 1px solid ${props => props.theme.colors.primary}; 70 | margin-top: 30px; 71 | margin-bottom: 5px; 72 | `; 73 | 74 | const DeleteButton = styled.button` 75 | grid-area: 1 / 10 / 2 / 11; 76 | justify-self: center; 77 | width: 24px; 78 | height: 24px; 79 | margin: 20px 0; 80 | background-color: transparent; 81 | border: none; 82 | outline: none; 83 | color: ${props => props.theme.colors.tertiary}; 84 | `; 85 | 86 | const IconsWrapper = styled.div` 87 | width: 24px; 88 | height: 24px; 89 | align-self: center; 90 | justify-self: center; 91 | `; 92 | 93 | const Details = styled.div` 94 | display: ${props => (props.show ? 'block' : 'none')}; 95 | grid-area: 5 / 1 / 6 / 11; 96 | margin: 10px 0; 97 | animation: fadeIn 1s; 98 | @keyframes fadeIn { 99 | from { 100 | opacity: 0; 101 | } 102 | to { 103 | opacity: 1; 104 | } 105 | } 106 | .fadeIn { 107 | animation-name: fadeIn; 108 | } 109 | `; 110 | 111 | export default function Order({ 112 | id, 113 | name, 114 | serial, 115 | manufacturer, 116 | quantity, 117 | orderedby, 118 | priority, 119 | description 120 | }) { 121 | const [activeDetail, setActiveDetail] = React.useState(null); 122 | 123 | function handleClick(filter) { 124 | if (filter === activeDetail) { 125 | setActiveDetail(null); 126 | } else { 127 | setActiveDetail(filter); 128 | } 129 | } 130 | 131 | return ( 132 | 133 | 134 | {name} 135 | 136 | 137 | 138 | beta 139 | 140 | 141 | 142 | Ordered by: {orderedby} 143 | 144 | 145 | 146 | Priority: 147 | 148 | 149 | 150 | handleClick('description')}> 151 | Description ▾ 152 | 153 | {description} 154 | 155 | handleClick('serial')}> 156 | 157 | 158 | 159 | 160 | Serial Number: {serial} 161 | 162 | handleClick('manufacturer')}> 163 | 164 | 165 | 166 | 167 | Manufacturer: {manufacturer} 168 | 169 | handleClick('quantity')}> 170 | 171 | 172 | 173 | 174 | Quanitiy: {quantity} 175 | 176 | 177 | ); 178 | } 179 | 180 | Order.propTypes = { 181 | id: PropTypes.number, 182 | name: PropTypes.string, 183 | serial: PropTypes.string, 184 | manufacturer: PropTypes.string, 185 | quantity: PropTypes.string, 186 | orderedby: PropTypes.string, 187 | priority: PropTypes.string, 188 | description: PropTypes.string 189 | }; 190 | -------------------------------------------------------------------------------- /client/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' } 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready.then(registration => { 134 | registration.unregister(); 135 | }); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /client/src/components/Ticket/Ticket.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import PropTypes from 'prop-types'; 4 | import { Title } from './Title'; 5 | import Status from './Status'; 6 | import Priority from './Priority'; 7 | import { IconsWrapper } from './IconsWrapper'; 8 | import Progress from './Progress'; 9 | import LocationIcon from '../../assets/icons/LocationIcon'; 10 | import UserIcon from '../../assets/icons/UserIcon'; 11 | import TimeDateIcon from '../../assets/icons/TimeDateIcon'; 12 | import EditIcon from '../../assets/icons/EditIcon'; 13 | 14 | const Container = styled.div` 15 | display: grid; 16 | grid-template-columns: 10% 10% 10% 10% 10% 10% 10% 10% 10% 10%; 17 | grid-template-rows: auto auto auto auto auto; 18 | row-gap: 10px; 19 | justify-self: center; 20 | width: 98%; 21 | height: auto; 22 | background-color: ${props => props.theme.colors.elements_bg}; 23 | margin-top: 30px; 24 | padding: 10px; 25 | border-radius: 10px; 26 | box-shadow: 0px 10px 5px 0px rgba(0, 0, 0, 0.15); 27 | `; 28 | 29 | const TitleWrapper = styled.div` 30 | grid-area: 1 / 1 / 2 / 10; 31 | margin: 5px 0; 32 | `; 33 | 34 | const StatusWrapper = styled.div` 35 | grid-area: 2 / 1 / 3 / 5; 36 | margin: 5px 0; 37 | `; 38 | 39 | const PriorityWrapper = styled.div` 40 | grid-area: 2 / 6 / 3 / 11; 41 | margin: 5px 0; 42 | `; 43 | 44 | const ProgressWrapper = styled.div` 45 | grid-area: 3 / 1 / 4 / 11; 46 | margin: 5px 0; 47 | `; 48 | 49 | const DescriptionWrapper = styled.div` 50 | grid-area: 4 / 1 / 5 / 6; 51 | cursor: pointer; 52 | &:hover { 53 | color: ${props => props.theme.colors.tertiary}; 54 | } 55 | `; 56 | 57 | const AssignedWrapper = styled.div` 58 | grid-area: 4 / 5 / 5 / 7; 59 | justify-self: center; 60 | cursor: pointer; 61 | `; 62 | 63 | const LocationWrapper = styled.div` 64 | grid-area: 4 / 7 / 5 / 9; 65 | justify-self: center; 66 | cursor: pointer; 67 | `; 68 | 69 | const TimeDateWrapper = styled.div` 70 | grid-area: 4 / 9 / 5 / 11; 71 | justify-self: center; 72 | cursor: pointer; 73 | border-bottom: 1px solid ${props => props.theme.colors.primary}; 74 | `; 75 | 76 | const BorderLine = styled.div` 77 | grid-area: 4 / 1 / 5 / 11; 78 | border-bottom: 1px solid ${props => props.theme.colors.primary}; 79 | margin-top: 30px; 80 | `; 81 | 82 | const Edit = styled.button` 83 | grid-area: 1 / 10 / 2 / 11; 84 | margin-top: 20px; 85 | width: 24px; 86 | height: 24px; 87 | background-color: transparent; 88 | border: none; 89 | outline: none; 90 | color: ${props => props.theme.colors.tertiary}; 91 | `; 92 | 93 | const ExtraDescription = styled.div` 94 | display: ${props => (props.show ? 'block' : 'none')}; 95 | grid-area: 5 / 1 / 6 / 11; 96 | margin: 10px 0; 97 | animation: fadeIn 1s; 98 | @keyframes fadeIn { 99 | from { 100 | opacity: 0; 101 | } 102 | to { 103 | opacity: 1; 104 | } 105 | } 106 | .fadeIn { 107 | animation-name: fadeIn; 108 | } 109 | `; 110 | 111 | export default function Ticket({ 112 | id, 113 | name, 114 | details, 115 | status, 116 | priority, 117 | assigned, 118 | location, 119 | timestamp, 120 | progress 121 | }) { 122 | const [progressValue, setProgressValue] = React.useState(progress); 123 | const date = new Date(timestamp); 124 | const [activeDetail, setActiveDetail] = React.useState(null); 125 | 126 | function handleClick(filter) { 127 | if (filter === activeDetail) { 128 | setActiveDetail(null); 129 | } else { 130 | setActiveDetail(filter); 131 | } 132 | } 133 | 134 | return ( 135 | 136 | 137 | {name} 138 | 139 | 140 | 141 | beta 142 | 143 | 144 | 145 | Status: 146 | 147 | 148 | 149 | 150 | Priority: 151 | 152 | 153 | 154 | 155 | Progress: 156 | setProgressValue(parseInt(event.target.value))} 159 | ticketId={id} 160 | /> 161 | 162 | 163 | handleClick('description')}> 164 | Description ▾ 165 | 166 | {details} 167 | 168 | handleClick('assignedUser')}> 169 | 170 | 171 | 172 | 173 | 174 | Assigned by: {assigned} 175 | 176 | 177 | handleClick('ticketLocation')}> 178 | 179 | 180 | 181 | 182 | 183 | Location: {location} 184 | 185 | 186 | handleClick('ticketTimedate')}> 187 | 188 | 189 | 190 | 191 | 192 | Date and Time: {date.toLocaleString()} 193 | 194 | 195 | 196 | ); 197 | } 198 | 199 | Ticket.propTypes = { 200 | name: PropTypes.string, 201 | status: PropTypes.string, 202 | priority: PropTypes.string, 203 | assigned: PropTypes.string, 204 | location: PropTypes.string, 205 | id: PropTypes.number, 206 | details: PropTypes.string, 207 | progress: PropTypes.string, 208 | timestamp: PropTypes.number 209 | }; 210 | -------------------------------------------------------------------------------- /client/src/pages/NewTicket.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import { InputLabel } from '../components/InputFields/InputLabel'; 4 | import { InputField } from '../components/InputFields/InputField'; 5 | import { InputSelectBox } from '../components/InputFields/InputSelectBox'; 6 | import { InputDescription } from '../components/InputFields/InputDescription'; 7 | import { SubmitButton } from '../components/InputFields/SubmitButton'; 8 | import { TextStyledH1 } from '../components/TextStyledH1'; 9 | import RadioButton from '../components/InputFields/RadioButton'; 10 | import Header from '../components/Header/Header'; 11 | import BackTicketsButton from '../components/Header/BackTicketsButton'; 12 | 13 | const InputForm = styled.form` 14 | display: flex; 15 | flex-direction: column; 16 | margin: 15px 0 0 15px; 17 | `; 18 | 19 | const WrapperRadioButtons = styled.div` 20 | display: flex; 21 | justify-content: space-around; 22 | width: 98%; 23 | `; 24 | 25 | export default function NewTicket() { 26 | const [name, setName] = React.useState(''); 27 | const [date, setDate] = React.useState(new Date()); 28 | const [assigned, setAssigned] = React.useState(''); 29 | const [location, setLocation] = React.useState(''); 30 | const [status, setStatus] = React.useState(''); 31 | const [priority, setPriority] = React.useState(''); 32 | const [details, setDetails] = React.useState(''); 33 | 34 | let isoDateString = date.toISOString(); 35 | isoDateString = isoDateString.substring(0, 19); 36 | 37 | async function handleSubmit(event) { 38 | event.preventDefault(); 39 | await fetch('/api/tickets', { 40 | method: 'POST', 41 | headers: { 42 | 'Content-Type': 'application/json' 43 | }, 44 | body: JSON.stringify({ 45 | name, 46 | timestamp: date.getTime(), 47 | assigned, 48 | location, 49 | status, 50 | priority, 51 | details 52 | }) 53 | }); 54 | 55 | setName(''); 56 | setDate(new Date()); 57 | setAssigned(''); 58 | setLocation(''); 59 | setStatus(''); 60 | setPriority(''); 61 | setDetails(''); 62 | } 63 | 64 | return ( 65 | <> 66 | 67 | 68 | 69 | 70 | Add Ticket 71 | Ticket name 72 | setName(event.target.value)} 76 | autoFocus 77 | required 78 | /> 79 | Date and Time 80 | setDate(new Date(event.target.value))} 84 | required 85 | /> 86 | Assigned by 87 | setAssigned(event.target.value)} 91 | required 92 | /> 93 | Location 94 | setLocation(event.target.value)} 97 | required 98 | > 99 | Select Location 100 | East Building 101 | West Building 102 | South Building 103 | North Building 104 | 105 | 106 | Status 107 | setStatus(event.target.value)}> 108 | 115 | Active 116 | 117 | 124 | In Progress 125 | 126 | 133 | Completed 134 | 135 | 136 | 137 | Priority 138 | setPriority(event.target.value)}> 139 | 146 | Normal 147 | 148 | 155 | Medium 156 | 157 | 164 | High 165 | 166 | 167 | Description 168 | setDetails(event.target.value)} 173 | required 174 | /> 175 | Add Ticket 176 | 177 | > 178 | ); 179 | } 180 | -------------------------------------------------------------------------------- /client/src/assets/logo/logoSmall.svg: -------------------------------------------------------------------------------- 1 | Ticky - Ticket App_Illu2 --------------------------------------------------------------------------------
142 | Ordered by: {orderedby} 143 |