├── .netlify
└── state.json
├── assets
└── preview.png
├── public
├── favicon.ico
├── logo192.png
├── logo512.png
├── manifest.json
└── index.html
├── src
├── Utils
│ ├── utils.js
│ ├── index.js
│ ├── moveElement.js
│ ├── getColumnFromParam.js
│ ├── extractParams.js
│ └── randomColor.js
├── hooks
│ ├── hooks.js
│ ├── useInput.js
│ ├── useTheme.js
│ └── useCalendar.js
├── index.js
├── components
│ ├── Calender
│ │ ├── style.js
│ │ └── Calender.js
│ ├── Dock
│ │ ├── style.js
│ │ └── Dock.js
│ ├── App
│ │ ├── style.js
│ │ └── App.js
│ ├── EditableDescription
│ │ ├── style.js
│ │ └── EditableDescription.js
│ ├── ToolBar
│ │ ├── style.js
│ │ └── ToolBar.js
│ ├── Tags
│ │ ├── style.js
│ │ └── Tags.js
│ ├── NewTicketForm
│ │ ├── NewTicketForm.js
│ │ └── style.js
│ ├── Day
│ │ ├── style.js
│ │ └── Day.js
│ └── Ticket
│ │ ├── style.js
│ │ └── Ticket.js
├── context
│ └── CalendarContext.js
├── styles
│ ├── base.css
│ └── setting.css
├── initalData.js
├── service-worker.js
└── serviceWorkerRegistration.js
├── README.md
├── .gitignore
├── package.json
└── LICENSE
/.netlify/state.json:
--------------------------------------------------------------------------------
1 | {
2 | "siteId": "d1bba6bc-7bc6-406d-80d5-d9077bef6a9c"
3 | }
--------------------------------------------------------------------------------
/assets/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guyariely/scalendar/HEAD/assets/preview.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guyariely/scalendar/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guyariely/scalendar/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guyariely/scalendar/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/src/Utils/utils.js:
--------------------------------------------------------------------------------
1 | export { randomColor } from "./randomColor";
2 | export { moveElement } from "./moveElement";
3 |
--------------------------------------------------------------------------------
/src/hooks/hooks.js:
--------------------------------------------------------------------------------
1 | export { useCalendar } from "./useCalendar";
2 | export { useTheme } from "./useTheme";
3 | export { useInput } from "./useInput";
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Scalendar
2 |
3 | A simple, elegant interface to organise your week, with drag-and-drop capabilties.
4 |
5 | [Link to the website](https://scalendar.netlify.app/)
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/Utils/index.js:
--------------------------------------------------------------------------------
1 | export { extractParams } from "./extractParams";
2 | export { getColumnFromParam } from "./getColumnFromParam";
3 | export { moveElement } from "./moveElement";
4 | export { randomColor } from "./randomColor";
5 |
--------------------------------------------------------------------------------
/src/Utils/moveElement.js:
--------------------------------------------------------------------------------
1 | export const moveElement = (arr, fromIndex, toIndex) => {
2 | const newArr = Array.from(arr);
3 | const [element] = newArr.splice(fromIndex, 1);
4 | newArr.splice(toIndex, 0, element);
5 | return newArr;
6 | };
7 |
--------------------------------------------------------------------------------
/src/Utils/getColumnFromParam.js:
--------------------------------------------------------------------------------
1 | export function getColumnFromParam(day) {
2 | const dayRef = {
3 | sun: 1,
4 | mon: 2,
5 | tue: 3,
6 | wed: 4,
7 | thu: 5,
8 | fri: 6,
9 | sat: 7,
10 | };
11 |
12 | return dayRef[day] || 0;
13 | }
14 |
--------------------------------------------------------------------------------
/src/Utils/extractParams.js:
--------------------------------------------------------------------------------
1 | export function extractParams(str) {
2 | const parsedStr = str.replace(/[^: ]*:[^ ]*/g, "").trim();
3 | const params = Object.fromEntries(
4 | (str.match(/[^: ]*:[^ ]*/g) || []).map(param => param.split(/:(.*)/))
5 | );
6 |
7 | return [parsedStr, params];
8 | }
9 |
--------------------------------------------------------------------------------
/src/Utils/randomColor.js:
--------------------------------------------------------------------------------
1 | export const randomColor = () => {
2 | const colors = [
3 | "red",
4 | "green",
5 | "orange",
6 | "blue",
7 | "purple",
8 | "pink",
9 | "turquoise",
10 | "maroon",
11 | "grey",
12 | "yellow",
13 | ];
14 | const randomIndex = Math.floor(Math.random() * colors.length);
15 | return colors[randomIndex];
16 | };
17 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./components/App/App";
4 | import "./styles/setting.css";
5 | import "./styles/base.css";
6 | import * as serviceWorkerRegistration from "./serviceWorkerRegistration";
7 |
8 | ReactDOM.render(, document.getElementById("root"));
9 |
10 | serviceWorkerRegistration.register();
11 |
--------------------------------------------------------------------------------
/.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 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/src/components/Calender/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const StyledCalendar = styled.div`
4 | display: grid;
5 | // calender's right padding hides the .day element,
6 | // so ghost-element-padding acts as a 8th invisible element.
7 | // 7 days (270px in width) and the ghost-element-padding (25px in width)
8 | grid-template-columns: repeat(7, 270px) 15px;
9 | overflow: auto;
10 | `;
11 |
--------------------------------------------------------------------------------
/src/components/Dock/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const StyledDock = styled.aside`
4 | background-color: var(--color-background-secondary);
5 | overflow: auto;
6 | `;
7 |
8 | export const Logo = styled.h1`
9 | font-size: 38px;
10 | display: flex;
11 | justify-content: center;
12 | padding-top: 15px;
13 | `;
14 |
15 | export const Container = styled.div`
16 | height: 100%;
17 | `;
18 |
--------------------------------------------------------------------------------
/src/context/CalendarContext.js:
--------------------------------------------------------------------------------
1 | import React, { createContext } from "react";
2 | import { useCalendar } from "../hooks/hooks";
3 |
4 | const CalendarContext = createContext();
5 |
6 | export function CalendarProvider({ children }) {
7 | const calendar = useCalendar();
8 |
9 | return (
10 |
11 | {children}
12 |
13 | );
14 | }
15 |
16 | export default CalendarContext;
17 |
--------------------------------------------------------------------------------
/src/components/App/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const StyledApp = styled.div`
4 | overflow: hidden;
5 | height: 100vh;
6 | background: var(--color-background);
7 | `;
8 |
9 | export const Container = styled.div`
10 | display: grid;
11 | grid-template-columns: 240px auto;
12 | height: 100%;
13 | overflow: hidden;
14 | `;
15 |
16 | export const Main = styled.div`
17 | display: grid;
18 | grid-template-rows: 80px auto;
19 | `;
20 |
--------------------------------------------------------------------------------
/src/components/EditableDescription/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const Input = styled.input`
4 | font-weight: bold;
5 | border-radius: 5px;
6 | font-family: inherit;
7 | font-size: 16px;
8 | width: 150px;
9 | color: ${({ theme }) => `var(--color-${theme}-text)`};
10 | border: ${({ theme }) => `2px solid var(--color-${theme}-text)`};
11 | `;
12 |
13 | export const Description = styled.p`
14 | font-size: 16px;
15 | font-weight: bold;
16 | `;
17 |
--------------------------------------------------------------------------------
/src/hooks/useInput.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | function useInput(initialState) {
4 | const [input, setInput] = useState(initialState);
5 |
6 | const onChangeInput = e => setInput(e.target.value);
7 |
8 | function onSubmitInput(e, submitCallback) {
9 | e.preventDefault();
10 | if (input) {
11 | submitCallback(input);
12 | setInput("");
13 | }
14 | }
15 |
16 | return { input, onChangeInput, onSubmitInput };
17 | }
18 |
19 | export { useInput };
20 |
--------------------------------------------------------------------------------
/src/styles/base.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: "Nunito", "Segoe UI", Helvetica, Arial, sans-serif;
3 | color: var(--color-text);
4 | margin: 0;
5 | }
6 |
7 | h1,
8 | h2,
9 | h3,
10 | h4,
11 | h5,
12 | p {
13 | margin: 0;
14 | cursor: default;
15 | }
16 |
17 | *,
18 | input,
19 | button {
20 | outline: none !important;
21 | }
22 |
23 | button {
24 | cursor: pointer;
25 | }
26 |
27 | button:disabled {
28 | cursor: default;
29 | }
30 |
31 | ::-webkit-scrollbar {
32 | display: none;
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/ToolBar/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const StyledToolbar = styled.div`
4 | display: flex;
5 | padding: 15px;
6 | `;
7 |
8 | export const Button = styled.button`
9 | border: none;
10 | height: 50px;
11 | width: 50px;
12 | border-radius: 20px;
13 | font-size: 30px;
14 | background: none;
15 |
16 | &:hover {
17 | opacity: 0.5;
18 | }
19 | `;
20 |
21 | export const ClearTicketsButton = styled(Button)``;
22 |
23 | export const ToggleThemeButton = styled(Button)`
24 | margin-left: auto;
25 | `;
26 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "scalendar",
3 | "name": "scalendar",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#23274f",
24 | "background_color": "#ffffff"
25 | }
--------------------------------------------------------------------------------
/src/components/Tags/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const StyledTags = styled.div`
4 | display: flex;
5 | flex-wrap: wrap;
6 | margin-top: 8px;
7 | padding: 0 10px;
8 | `;
9 |
10 | export const StyledTag = styled.div`
11 | background: ${({ theme }) => `var(--color-${theme}-text)`};
12 | color: ${({ theme }) => `var(--color-${theme}-background)`};
13 | padding: 5px 10px;
14 | border-radius: 15px;
15 | margin: 2px;
16 | font-size: 11px;
17 | font-weight: bold;
18 | `;
19 |
20 | export const Link = styled.a`
21 | color: ${({ theme }) => `var(--color-${theme}-background)`};
22 | `;
23 |
24 | export const TagContent = styled.p``;
25 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Scalendar
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/components/NewTicketForm/NewTicketForm.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useInput } from "../../hooks/hooks";
3 | import { Form, Input, SubmitButton } from "./style";
4 |
5 | function NewTicketForm({ addNewTicket }) {
6 | const { input, onChangeInput, onSubmitInput } = useInput("");
7 |
8 | return (
9 |
23 | );
24 | }
25 |
26 | export default NewTicketForm;
27 |
--------------------------------------------------------------------------------
/src/components/Day/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const StyledDay = styled.div`
4 | margin: 0 15px;
5 | display: grid;
6 | grid-template-rows: 40px auto;
7 | `;
8 |
9 | export const Title = styled.h1`
10 | color: ${({ active }) =>
11 | active ? "var(--color-text)" : "var(--color-light-text)"};
12 | `;
13 |
14 | export const Container = styled.div`
15 | padding-top: 33px;
16 | border-radius: 20px;
17 | background: linear-gradient(
18 | to bottom,
19 | ${({ isOver }) =>
20 | isOver
21 | ? "var(--color-background-secondary)"
22 | : "var(--color-background)"}
23 | 96%,
24 | var(--color-stripes) 5%
25 | );
26 | background-size: 100% 35px;
27 | `;
28 |
--------------------------------------------------------------------------------
/src/components/EditableDescription/EditableDescription.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Input, Description } from "./style";
3 |
4 | function EditableDescription(props) {
5 | const { description, onChange, onSubmit, isEditMode, theme } = props;
6 |
7 | if (isEditMode) {
8 | return (
9 |
22 | );
23 | }
24 |
25 | return {description};
26 | }
27 |
28 | export default EditableDescription;
29 |
--------------------------------------------------------------------------------
/src/hooks/useTheme.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | function useTheme() {
4 | const [theme, setTheme] = useState(null);
5 |
6 | useEffect(() => {
7 | const lastTheme = localStorage.getItem("theme");
8 | if (!lastTheme) localStorage.setItem("theme", "light");
9 | setTheme(lastTheme || "light");
10 | document.documentElement.setAttribute("data-theme", lastTheme || "light");
11 | }, []);
12 |
13 | function toggleTheme() {
14 | const newTheme = theme === "light" ? "dark" : "light";
15 | setTheme(newTheme);
16 | document.documentElement.setAttribute("data-theme", newTheme);
17 | localStorage.setItem("theme", newTheme);
18 | }
19 |
20 | return toggleTheme;
21 | }
22 |
23 | export { useTheme };
24 |
--------------------------------------------------------------------------------
/src/components/Calender/Calender.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import CalendarContext from "../../context/CalendarContext";
3 | import Day from "../Day/Day";
4 | import { StyledCalendar } from "./style";
5 |
6 | function Calender() {
7 | const { tickets, columns } = useContext(CalendarContext);
8 |
9 | const days = new Array(7).fill(0).map((_, index) => index + 1);
10 |
11 | return (
12 |
13 | {days.map(columnId => {
14 | const { id, name, ticketIds } = columns[columnId];
15 | const columnTickets = ticketIds.map(id => tickets[id]);
16 |
17 | return (
18 |
19 | );
20 | })}
21 |
22 |
23 | );
24 | }
25 |
26 | export default Calender;
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "scalendar",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "node-sass": "^4.12.0",
7 | "react": "^16.14.0",
8 | "react-beautiful-dnd": "^13.1.0",
9 | "react-dom": "^16.14.0",
10 | "react-scripts": "3.0.1",
11 | "styled-components": "^5.3.0",
12 | "uniqid": "^5.3.0"
13 | },
14 | "scripts": {
15 | "start": "react-scripts start",
16 | "build": "react-scripts build",
17 | "test": "react-scripts test",
18 | "eject": "react-scripts eject"
19 | },
20 | "eslintConfig": {
21 | "extends": "react-app"
22 | },
23 | "browserslist": {
24 | "production": [
25 | ">0.2%",
26 | "not dead",
27 | "not op_mini all"
28 | ],
29 | "development": [
30 | "last 1 chrome version",
31 | "last 1 firefox version",
32 | "last 1 safari version"
33 | ]
34 | }
35 | }
--------------------------------------------------------------------------------
/src/components/NewTicketForm/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const Form = styled.form`
4 | display: flex;
5 | `;
6 |
7 | export const Input = styled.input`
8 | font-size: 18px;
9 | border: none;
10 | background-color: var(--color-background-secondary);
11 | height: 50px;
12 | border-radius: 20px;
13 | width: 400px;
14 | padding: 0 20px;
15 | font-weight: bold;
16 | color: var(--color-text);
17 |
18 | &::placeholder {
19 | color: var(--color-light-text);
20 | }
21 | `;
22 |
23 | export const SubmitButton = styled.button`
24 | background: var(--color-background-secondary);
25 | border: none;
26 | margin: 0 10px;
27 | height: 50px;
28 | width: 50px;
29 | border-radius: 20px;
30 | font-size: 30px;
31 | color: var(--color-light-text);
32 |
33 | &:hover {
34 | color: var(--color-text);
35 | }
36 |
37 | &:disabled {
38 | color: var(--color-light-text);
39 | }
40 | `;
41 |
--------------------------------------------------------------------------------
/src/components/Ticket/style.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const StyledTicket = styled.div`
4 | margin: 10px;
5 | `;
6 |
7 | export const MainContainer = styled.div`
8 | display: flex;
9 | justify-content: space-between;
10 | padding: 0 20px;
11 | `;
12 |
13 | export const Container = styled.div`
14 | padding: 10px 0;
15 | border-radius: 20px;
16 | word-break: break-word;
17 | background-color: ${({ theme }) => `var(--color-${theme}-background)`};
18 | color: ${({ theme }) => `var(--color-${theme}-text)`};
19 | transform: ${({ isDragging }) => (isDragging ? "scale(0.9)" : "")};
20 | transition: transform 60ms ease-in-out;
21 | `;
22 |
23 | export const DeleteButton = styled.button`
24 | background: none;
25 | border: none;
26 | margin: 0;
27 | padding: 0;
28 | font-size: 18px;
29 | font-weight: bold;
30 | display: flex;
31 | margin-top: 2px;
32 | padding-left: 10px;
33 | color: ${({ theme }) => `var(--color-${theme}-text)`};
34 | `;
35 |
--------------------------------------------------------------------------------
/src/components/ToolBar/ToolBar.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import CalendarContext from "../../context/CalendarContext";
3 | import { useTheme } from "../../hooks/hooks";
4 | import NewTicketForm from "../NewTicketForm/NewTicketForm";
5 | import { StyledToolbar, ToggleThemeButton, ClearTicketsButton } from "./style";
6 |
7 | function ToolBar() {
8 | const { addNewTicket, clearTickets } = useContext(CalendarContext);
9 | const toggleTheme = useTheme();
10 |
11 | return (
12 |
13 |
14 | toggleTheme()}>
15 |
16 | 🌓
17 |
18 |
19 | clearTickets()}>
20 |
21 | 🗑
22 |
23 |
24 |
25 | );
26 | }
27 |
28 | export default ToolBar;
29 |
--------------------------------------------------------------------------------
/src/components/Dock/Dock.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import Ticket from "../Ticket/Ticket";
3 | import { StyledDock, Logo, Container } from "./style";
4 | import { Droppable } from "react-beautiful-dnd";
5 | import CalendarContext from "../../context/CalendarContext";
6 |
7 | function Dock() {
8 | const { columns, tickets } = useContext(CalendarContext);
9 | const columnTickets = columns["0"].ticketIds.map(id => tickets[id]);
10 |
11 | return (
12 |
13 | Scalendar
14 |
15 | {provided => (
16 |
17 | {columnTickets.map((ticket, index) => (
18 |
24 | ))}
25 | {provided.placeholder}
26 |
27 | )}
28 |
29 |
30 | );
31 | }
32 |
33 | export default Dock;
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Guy Arieli
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/components/App/App.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import Dock from "../Dock/Dock";
3 | import Calender from "../Calender/Calender";
4 | import ToolBar from "../ToolBar/ToolBar";
5 | import { DragDropContext } from "react-beautiful-dnd";
6 | import { StyledApp, Container, Main } from "./style";
7 | import CalendarContext, {
8 | CalendarProvider,
9 | } from "../../context/CalendarContext";
10 |
11 | function AppContainer() {
12 | const { moveTicket } = useContext(CalendarContext);
13 |
14 | function handleDragEnd({ destination, source, draggableId }) {
15 | moveTicket(draggableId, source, destination);
16 | }
17 |
18 | return (
19 |
20 | handleDragEnd(e)}>
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | );
31 | }
32 |
33 | function App() {
34 | return (
35 |
36 |
37 |
38 | );
39 | }
40 |
41 | export default App;
42 |
--------------------------------------------------------------------------------
/src/components/Day/Day.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Ticket from "../Ticket/Ticket";
3 | import { StyledDay, Title, Container } from "./style";
4 | import { Droppable } from "react-beautiful-dnd";
5 |
6 | function Day(props) {
7 | const { name, dayIndex, tickets } = props;
8 |
9 | const isActiveDay = new Date().getDay() === Number(dayIndex - 1);
10 |
11 | return (
12 |
13 | {name}
14 |
15 | {(provided, snapshot) => (
16 |
21 | {tickets.map((ticket, index) => (
22 |
28 | ))}
29 | {provided.placeholder}
30 |
31 | )}
32 |
33 |
34 | );
35 | }
36 |
37 | export default Day;
38 |
--------------------------------------------------------------------------------
/src/styles/setting.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --color-background: #fff;
3 | --color-background-secondary: #f7f8fb;
4 | --color-light-text: #bfcbd2;
5 | --color-text: #23274f;
6 | --color-stripes: #ebebee;
7 |
8 | --color-orange-background: #efc9b3;
9 | --color-orange-text: #bf5b2c;
10 | --color-green-background: #b4e5c9;
11 | --color-green-text: #509f6e;
12 | --color-red-background: #ff7878;
13 | --color-red-text: #ea2b2b;
14 | --color-blue-background: #1cb0f6;
15 | --color-blue-text: #2b70c9;
16 | --color-purple-background: #ce82ff;
17 | --color-purple-text: #634590;
18 | --color-pink-background: #ffb2b2;
19 | --color-pink-text: #ff4b4b;
20 | --color-turquoise-background: #d2e4e8;
21 | --color-turquoise-text: #425863;
22 | --color-maroon-background: #b7657b;
23 | --color-maroon-text: #502e2e;
24 | --color-grey-background: #afafaf;
25 | --color-grey-text: #4b4b4b;
26 | --color-yellow-background: #ffe06e;
27 | --color-yellow-text: #ca7702;
28 | }
29 |
30 | [data-theme="dark"] {
31 | --color-background: #202124;
32 | --color-background-secondary: #292a2d;
33 | --color-light-text: #b3b3b3;
34 | --color-text: #fff;
35 | --color-stripes: #585858;
36 | }
37 |
--------------------------------------------------------------------------------
/src/initalData.js:
--------------------------------------------------------------------------------
1 | const initialData = {
2 | tickets: {
3 | "task-1": {
4 | id: "task-1",
5 | description: "Create your first task!",
6 | theme: "orange",
7 | tags: {},
8 | },
9 | "task-2": {
10 | id: "task-2",
11 | description: "Organize your tasks throughout the week",
12 | theme: "blue",
13 | tags: {},
14 | },
15 | "task-3": {
16 | id: "task-3",
17 | description: "Press the moon button to toggle dark mode",
18 | theme: "red",
19 | tags: {},
20 | },
21 | },
22 | columns: {
23 | 0: {
24 | id: "0",
25 | name: "dock",
26 | ticketIds: ["task-1", "task-2", "task-3"],
27 | },
28 | 1: {
29 | id: "1",
30 | name: "SUN",
31 | ticketIds: [],
32 | },
33 | 2: {
34 | id: "2",
35 | name: "MON",
36 | ticketIds: [],
37 | },
38 | 3: {
39 | id: "3",
40 | name: "TUE",
41 | ticketIds: [],
42 | },
43 | 4: {
44 | id: "4",
45 | name: "WED",
46 | ticketIds: [],
47 | },
48 | 5: {
49 | id: "5",
50 | name: "THU",
51 | ticketIds: [],
52 | },
53 | 6: {
54 | id: "6",
55 | name: "FRI",
56 | ticketIds: [],
57 | },
58 | 7: {
59 | id: "7",
60 | name: "SAT",
61 | ticketIds: [],
62 | },
63 | },
64 | };
65 |
66 | export default initialData;
67 |
--------------------------------------------------------------------------------
/src/components/Tags/Tags.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { StyledTags, StyledTag, TagContent, Link } from "./style";
3 |
4 | function TimeTag({ theme, content }) {
5 | return {`⏰ ${content}`};
6 | }
7 |
8 | function UrgentTag({ theme, content }) {
9 | const level = Number(content);
10 | if (Number.isNaN(level) || level < 1 || level > 3) return null;
11 |
12 | return (
13 |
14 | {Array(level).fill("🚨").join("")}
15 |
16 | );
17 | }
18 |
19 | function LinkTag({ theme, content }) {
20 | return (
21 |
22 | {"🔗 "}
23 |
29 | {content}
30 |
31 |
32 | );
33 | }
34 |
35 | function Tag({ theme, type, content }) {
36 | switch (type) {
37 | case "time":
38 | return ;
39 | case "link":
40 | return ;
41 | case "urgent":
42 | return ;
43 | default:
44 | return null;
45 | }
46 | }
47 |
48 | function TagContainer({ theme, children }) {
49 | return (
50 |
51 | {children}
52 |
53 | );
54 | }
55 |
56 | const tagTypes = ["time", "link", "urgent"];
57 |
58 | function Tags({ theme, tags }) {
59 | if (tagTypes.every(tag => !tags[tag])) return null;
60 |
61 | return (
62 |
63 | {Object.entries(tags).map(([type, content], i) => (
64 |
65 | ))}
66 |
67 | );
68 | }
69 |
70 | export default Tags;
71 |
--------------------------------------------------------------------------------
/src/components/Ticket/Ticket.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext } from "react";
2 | import ClickOutsideWrapper from "react-click-outside-wrapper";
3 | import EditableDescription from "../EditableDescription/EditableDescription";
4 | import { StyledTicket, Container, MainContainer, DeleteButton } from "./style";
5 | import { Draggable } from "react-beautiful-dnd";
6 | import CalendarContext from "../../context/CalendarContext";
7 | import Tags from "../Tags/Tags";
8 |
9 | function Ticket(props) {
10 | const { id, theme, tags } = props.ticket;
11 |
12 | const [isEditMode, setIsEditMode] = useState(false);
13 | const [description, setDescription] = useState(props.ticket.description);
14 |
15 | const { updateDescription, deleteTicket } = useContext(CalendarContext);
16 |
17 | function onSubmitEditableDescription() {
18 | setIsEditMode(false);
19 | updateDescription(id, description);
20 | }
21 |
22 | return (
23 | isEditMode && onSubmitEditableDescription()}
25 | >
26 |
27 | {(provided, snapshot) => (
28 | setIsEditMode(true)}
33 | >
34 |
35 |
36 | setDescription(e.target.value)}
39 | onSubmit={onSubmitEditableDescription}
40 | isEditMode={isEditMode}
41 | theme={theme}
42 | />
43 | deleteTicket(props.column, id)}
45 | theme={theme}
46 | >
47 | ✕
48 |
49 |
50 | {Object.keys(tags).length > 0 && (
51 |
52 | )}
53 |
54 | {provided.placeholder}
55 |
56 | )}
57 |
58 |
59 | );
60 | }
61 |
62 | export default Ticket;
63 |
--------------------------------------------------------------------------------
/src/service-worker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 |
3 | // This service worker can be customized!
4 | // See https://developers.google.com/web/tools/workbox/modules
5 | // for the list of available Workbox modules, or add any other
6 | // code you'd like.
7 | // You can also remove this file if you'd prefer not to use a
8 | // service worker, and the Workbox build step will be skipped.
9 |
10 | import { clientsClaim } from 'workbox-core';
11 | import { ExpirationPlugin } from 'workbox-expiration';
12 | import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
13 | import { registerRoute } from 'workbox-routing';
14 | import { StaleWhileRevalidate } from 'workbox-strategies';
15 |
16 | clientsClaim();
17 |
18 | // Precache all of the assets generated by your build process.
19 | // Their URLs are injected into the manifest variable below.
20 | // This variable must be present somewhere in your service worker file,
21 | // even if you decide not to use precaching. See https://cra.link/PWA
22 | precacheAndRoute(self.__WB_MANIFEST);
23 |
24 | // Set up App Shell-style routing, so that all navigation requests
25 | // are fulfilled with your index.html shell. Learn more at
26 | // https://developers.google.com/web/fundamentals/architecture/app-shell
27 | const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');
28 | registerRoute(
29 | // Return false to exempt requests from being fulfilled by index.html.
30 | ({ request, url }) => {
31 | // If this isn't a navigation, skip.
32 | if (request.mode !== 'navigate') {
33 | return false;
34 | } // If this is a URL that starts with /_, skip.
35 |
36 | if (url.pathname.startsWith('/_')) {
37 | return false;
38 | } // If this looks like a URL for a resource, because it contains // a file extension, skip.
39 |
40 | if (url.pathname.match(fileExtensionRegexp)) {
41 | return false;
42 | } // Return true to signal that we want to use the handler.
43 |
44 | return true;
45 | },
46 | createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
47 | );
48 |
49 | // An example runtime caching route for requests that aren't handled by the
50 | // precache, in this case same-origin .png requests like those from in public/
51 | registerRoute(
52 | // Add in any other file extensions or routing criteria as needed.
53 | ({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'), // Customize this strategy as needed, e.g., by changing to CacheFirst.
54 | new StaleWhileRevalidate({
55 | cacheName: 'images',
56 | plugins: [
57 | // Ensure that once this runtime cache reaches a maximum size the
58 | // least-recently used images are removed.
59 | new ExpirationPlugin({ maxEntries: 50 }),
60 | ],
61 | })
62 | );
63 |
64 | // This allows the web app to trigger skipWaiting via
65 | // registration.waiting.postMessage({type: 'SKIP_WAITING'})
66 | self.addEventListener('message', (event) => {
67 | if (event.data && event.data.type === 'SKIP_WAITING') {
68 | self.skipWaiting();
69 | }
70 | });
71 |
72 | // Any other custom service worker logic can go here.
73 |
--------------------------------------------------------------------------------
/src/hooks/useCalendar.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import initialData from "../initalData";
3 | import {
4 | randomColor,
5 | moveElement,
6 | extractParams,
7 | getColumnFromParam,
8 | } from "../Utils";
9 | import uniqid from "uniqid";
10 |
11 | function useCalendar() {
12 | const [tickets, setTickets] = useState(initialData.tickets);
13 | const [columns, setColumns] = useState(initialData.columns);
14 |
15 | useEffect(() => {
16 | const storedTickets = localStorage.getItem("tickets");
17 | const storedColumns = localStorage.getItem("columns");
18 |
19 | if (storedTickets && storedColumns) {
20 | setTickets(JSON.parse(storedTickets));
21 | setColumns(JSON.parse(storedColumns));
22 | } else {
23 | localStorage.setItem("tickets", JSON.stringify(initialData.tickets));
24 | localStorage.setItem("columns", JSON.stringify(initialData.columns));
25 | }
26 | }, []);
27 |
28 | useEffect(() => {
29 | persistTicketsToStorage(tickets);
30 | }, [tickets]);
31 |
32 | useEffect(() => {
33 | persistColumnsToStorage(columns);
34 | }, [columns]);
35 |
36 | const addNewTicket = newTicketInput => {
37 | const newId = uniqid();
38 | const [parsedInput, params] = extractParams(newTicketInput);
39 |
40 | setTickets(tickets => ({
41 | ...tickets,
42 | [newId]: {
43 | id: newId,
44 | description: parsedInput,
45 | theme: randomColor(),
46 | tags: params,
47 | },
48 | }));
49 |
50 | const column = getColumnFromParam(params["day"]);
51 |
52 | setColumns(columns => {
53 | const updatedColumn = {
54 | [column]: {
55 | ...columns[column],
56 | ticketIds: [...columns[column].ticketIds, newId],
57 | },
58 | };
59 | return Object.assign({}, columns, updatedColumn);
60 | });
61 | };
62 |
63 | const deleteTicket = (columnId, ticketId) => {
64 | setTickets(tickets => {
65 | const newTickets = Object.assign({}, tickets);
66 | delete newTickets[ticketId];
67 | return newTickets;
68 | });
69 |
70 | setColumns(columns => {
71 | const column = columns[columnId];
72 | return {
73 | ...columns,
74 | [columnId]: {
75 | ...column,
76 | ticketIds: column.ticketIds.filter(id => id !== ticketId),
77 | },
78 | };
79 | });
80 | };
81 |
82 | const moveTicket = (ticketId, source, destination) => {
83 | if (!destination) return;
84 |
85 | const fromColumnId = source.droppableId;
86 | const toColumnId = destination.droppableId;
87 | const fromIndex = source.index;
88 | const toIndex = destination.index;
89 |
90 | if (fromColumnId === toColumnId && fromIndex === toIndex) {
91 | return;
92 | }
93 |
94 | setColumns(columns => {
95 | const fromColumn = columns[fromColumnId];
96 | const toColumn = columns[toColumnId];
97 |
98 | if (fromColumnId === toColumnId) {
99 | const updatedTicketIds = moveElement(
100 | fromColumn.ticketIds,
101 | fromIndex,
102 | toIndex
103 | );
104 |
105 | return {
106 | ...columns,
107 | [toColumnId]: {
108 | ...toColumn,
109 | ticketIds: updatedTicketIds,
110 | },
111 | };
112 | }
113 |
114 | const fromColumnsTicketIds = Array.from(fromColumn.ticketIds);
115 | fromColumnsTicketIds.splice(fromIndex, 1);
116 | const toColumnsTicketIds = Array.from(toColumn.ticketIds);
117 | toColumnsTicketIds.splice(toIndex, 0, ticketId);
118 |
119 | return {
120 | ...columns,
121 | [fromColumnId]: {
122 | ...fromColumn,
123 | ticketIds: fromColumnsTicketIds,
124 | },
125 | [toColumnId]: {
126 | ...toColumn,
127 | ticketIds: toColumnsTicketIds,
128 | },
129 | };
130 | });
131 | };
132 |
133 | const updateDescription = (id, description) => {
134 | setTickets(tickets => ({
135 | ...tickets,
136 | [id]: {
137 | ...tickets[id],
138 | description,
139 | },
140 | }));
141 | };
142 |
143 | const clearTickets = () => {
144 | setTickets({});
145 | setColumns(columns => {
146 | const clearedColumns = {};
147 | Object.entries(columns).forEach(([columnId, column]) => {
148 | clearedColumns[columnId] = {
149 | ...column,
150 | ticketIds: [],
151 | };
152 | });
153 | return clearedColumns;
154 | });
155 | };
156 |
157 | const persistTicketsToStorage = tickets => {
158 | localStorage.setItem("tickets", JSON.stringify(tickets));
159 | };
160 |
161 | const persistColumnsToStorage = columns => {
162 | localStorage.setItem("columns", JSON.stringify(columns));
163 | };
164 |
165 | return {
166 | tickets,
167 | columns,
168 | deleteTicket,
169 | addNewTicket,
170 | moveTicket,
171 | clearTickets,
172 | updateDescription,
173 | };
174 | }
175 |
176 | export { useCalendar };
177 |
--------------------------------------------------------------------------------
/src/serviceWorkerRegistration.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://cra.link/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(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
19 | );
20 |
21 | export function register(config) {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Let's check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl, config);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://cra.link/PWA'
45 | );
46 | });
47 | } else {
48 | // Is not localhost. Just register service worker
49 | registerValidSW(swUrl, config);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl, config) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then((registration) => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | if (installingWorker == null) {
62 | return;
63 | }
64 | installingWorker.onstatechange = () => {
65 | if (installingWorker.state === 'installed') {
66 | if (navigator.serviceWorker.controller) {
67 | // At this point, the updated precached content has been fetched,
68 | // but the previous service worker will still serve the older
69 | // content until all client tabs are closed.
70 | console.log(
71 | 'New content is available and will be used when all ' +
72 | 'tabs for this page are closed. See https://cra.link/PWA.'
73 | );
74 |
75 | // Execute callback
76 | if (config && config.onUpdate) {
77 | config.onUpdate(registration);
78 | }
79 | } else {
80 | // At this point, everything has been precached.
81 | // It's the perfect time to display a
82 | // "Content is cached for offline use." message.
83 | console.log('Content is cached for offline use.');
84 |
85 | // Execute callback
86 | if (config && config.onSuccess) {
87 | config.onSuccess(registration);
88 | }
89 | }
90 | }
91 | };
92 | };
93 | })
94 | .catch((error) => {
95 | console.error('Error during service worker registration:', error);
96 | });
97 | }
98 |
99 | function checkValidServiceWorker(swUrl, config) {
100 | // Check if the service worker can be found. If it can't reload the page.
101 | fetch(swUrl, {
102 | headers: { 'Service-Worker': 'script' },
103 | })
104 | .then((response) => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then((registration) => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log('No internet connection found. App is running in offline mode.');
124 | });
125 | }
126 |
127 | export function unregister() {
128 | if ('serviceWorker' in navigator) {
129 | navigator.serviceWorker.ready
130 | .then((registration) => {
131 | registration.unregister();
132 | })
133 | .catch((error) => {
134 | console.error(error.message);
135 | });
136 | }
137 | }
138 |
--------------------------------------------------------------------------------