Fixed projects
This commit is contained in:
parent
56711f5b59
commit
0ad741463d
@ -16,6 +16,8 @@ namespace Todo.Api.Hubs
|
|||||||
|
|
||||||
public async Task CreateTodo(string todoTitle, string projectName)
|
public async Task CreateTodo(string todoTitle, string projectName)
|
||||||
{
|
{
|
||||||
|
if (todoTitle is null)
|
||||||
|
throw new ArgumentException("title cannot be null");
|
||||||
var _ = await _todoRepository.CreateTodoAsync(todoTitle, projectName);
|
var _ = await _todoRepository.CreateTodoAsync(todoTitle, projectName);
|
||||||
|
|
||||||
var todos = await _todoRepository.GetNotDoneTodos();
|
var todos = await _todoRepository.GetNotDoneTodos();
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
"next-pwa": "^5.4.0",
|
"next-pwa": "^5.4.0",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
|
"react-textarea-autosize": "^8.3.3",
|
||||||
"tailwindcss": "^2.2.19"
|
"tailwindcss": "^2.2.19"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
15
src/client/src/components/common/buttons/outlinedButton.tsx
Normal file
15
src/client/src/components/common/buttons/outlinedButton.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { ButtonHTMLAttributes, FC, forwardRef } from "react";
|
||||||
|
|
||||||
|
export const OutlinedButton: FC<ButtonHTMLAttributes<HTMLButtonElement>> = (
|
||||||
|
props
|
||||||
|
) => (
|
||||||
|
<button
|
||||||
|
{...props}
|
||||||
|
tabIndex={3}
|
||||||
|
type="button"
|
||||||
|
className={"base-button " + props.className}
|
||||||
|
onClick={props.onClick}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</button>
|
||||||
|
);
|
19
src/client/src/components/common/buttons/primaryButton.tsx
Normal file
19
src/client/src/components/common/buttons/primaryButton.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { ButtonHTMLAttributes, FC } from "react";
|
||||||
|
|
||||||
|
export const PrimaryButton: FC<ButtonHTMLAttributes<HTMLButtonElement>> = (
|
||||||
|
props
|
||||||
|
) => (
|
||||||
|
<button
|
||||||
|
{...props}
|
||||||
|
type="submit"
|
||||||
|
onClick={props.onClick}
|
||||||
|
tabIndex={2}
|
||||||
|
disabled={props.disabled}
|
||||||
|
className={
|
||||||
|
"base-button bg-accent-500 disabled:bg-accent-800 active:bg-accent-400 border-none text-white " +
|
||||||
|
props.className
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</button>
|
||||||
|
);
|
62
src/client/src/components/todos/add/addProjectButton.tsx
Normal file
62
src/client/src/components/todos/add/addProjectButton.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { ButtonHTMLAttributes, FC, useState } from "react";
|
||||||
|
import { OutlinedButton } from "@src/components/common/buttons/outlinedButton";
|
||||||
|
import Tippy from "@tippyjs/react";
|
||||||
|
|
||||||
|
interface AddProjectButtonProps
|
||||||
|
extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
initialProject: string;
|
||||||
|
onProjectChanged: (project: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddProjectButton: FC<AddProjectButtonProps> = (props) => {
|
||||||
|
const [menuIsOpen, setMenuIsOpen] = useState(false);
|
||||||
|
const [project, setProject] = useState(props.initialProject);
|
||||||
|
return (
|
||||||
|
<Tippy
|
||||||
|
placement="bottom"
|
||||||
|
visible={menuIsOpen}
|
||||||
|
onClickOutside={() => {
|
||||||
|
setMenuIsOpen(false);
|
||||||
|
}}
|
||||||
|
delay={0}
|
||||||
|
interactive={true}
|
||||||
|
duration={0}
|
||||||
|
content={
|
||||||
|
<div
|
||||||
|
className="py-2 px-4 bg-white dark:bg-gray-700 border border-accent-500 rounded-md flex flex-col gap-4"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
tabIndex={0}
|
||||||
|
autoFocus
|
||||||
|
value={project}
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
onChange={(e) => setProject(e.target.value)}
|
||||||
|
placeholder="Project"
|
||||||
|
onKeyDown={(k) => {
|
||||||
|
if (k.key === "Enter") {
|
||||||
|
props.onProjectChanged(project);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
props.onProjectChanged(project);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<OutlinedButton
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
{...props}
|
||||||
|
onClick={() => setMenuIsOpen(true)}
|
||||||
|
>
|
||||||
|
{props.children || "Add project"}
|
||||||
|
</OutlinedButton>
|
||||||
|
</span>
|
||||||
|
</Tippy>
|
||||||
|
);
|
||||||
|
};
|
42
src/client/src/components/todos/add/addTodoForm.tsx
Normal file
42
src/client/src/components/todos/add/addTodoForm.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { FC, useState } from "react";
|
||||||
|
import { OutlinedButton } from "@src/components/common/buttons/outlinedButton";
|
||||||
|
import { PrimaryButton } from "@src/components/common/buttons/primaryButton";
|
||||||
|
import { TodoShortForm } from "@src/components/todos/collapsed/todoShortForm";
|
||||||
|
|
||||||
|
export const AddTodoForm: FC<{
|
||||||
|
onAdd: (todoName: string, project: string) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
project: string;
|
||||||
|
}> = ({ onAdd, onClose, ...props }) => {
|
||||||
|
const [todoName, setTodoName] = useState("");
|
||||||
|
const [todoDescription, setTodoDescription] = useState("");
|
||||||
|
const [project, setProject] = useState(props.project);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onAdd(todoName, project);
|
||||||
|
setTodoName("");
|
||||||
|
setTodoDescription("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="py-2 space-y-3">
|
||||||
|
<TodoShortForm
|
||||||
|
project={project}
|
||||||
|
onProjectChanged={(p) => setProject(p)}
|
||||||
|
name={todoName}
|
||||||
|
onNameChange={(e) => setTodoName(e.target.value)}
|
||||||
|
description={todoDescription}
|
||||||
|
onDescriptionChange={(e) => setTodoDescription(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="space-x-2">
|
||||||
|
<PrimaryButton disabled={!todoName} type="submit">
|
||||||
|
Add todo
|
||||||
|
</PrimaryButton>
|
||||||
|
<OutlinedButton onClick={onClose}>Cancel</OutlinedButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { CollapsedAddTodo } from "@src/components/todos/collapsed/collapsedAddTodo";
|
import { CollapsedAddTodo } from "@src/components/todos/collapsed/collapsedAddTodo";
|
||||||
import { AddTodoForm } from "@src/components/todos/collapsed/addTodoForm";
|
import { AddTodoForm } from "@src/components/todos/add/addTodoForm";
|
||||||
import { CollapsedState } from "@src/components/todos/collapsed/collapsedState";
|
import { CollapsedState } from "@src/components/todos/collapsed/collapsedState";
|
||||||
import { useCreateTodo } from "@src/presentation/hooks/socketHooks";
|
import { useCreateTodo } from "@src/presentation/hooks/socketHooks";
|
||||||
|
|
||||||
|
@ -1,64 +0,0 @@
|
|||||||
import { FC, useState } from "react";
|
|
||||||
|
|
||||||
export const AddTodoForm: FC<{
|
|
||||||
onAdd: (todoName: string, project: string) => void;
|
|
||||||
onClose: () => void;
|
|
||||||
project: string;
|
|
||||||
}> = ({ onAdd, onClose, ...props }) => {
|
|
||||||
const [todoName, setTodoName] = useState("");
|
|
||||||
const [project, setProject] = useState(props.project);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
onAdd(todoName, project);
|
|
||||||
setTodoName("");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="py-2 space-y-3">
|
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
|
||||||
<div className="todo-input-form md:flex-grow py-2 px-4 bg-gray-800 border border-gray-500 rounded-lg">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Todo name"
|
|
||||||
className="text-sm w-full"
|
|
||||||
autoFocus
|
|
||||||
value={todoName}
|
|
||||||
tabIndex={1}
|
|
||||||
onChange={(e) => setTodoName(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="todo-project py-2 px-4 bg-gray-700 border border-gray-500 rounded-lg ">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Project name"
|
|
||||||
className="text-sm w-full placeholder-gray-400"
|
|
||||||
value={project}
|
|
||||||
tabIndex={2}
|
|
||||||
onChange={(e) => setProject(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-x-2">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
tabIndex={2}
|
|
||||||
disabled={!todoName}
|
|
||||||
className="base-button dark:bg-accent-500 disabled:bg-accent-800 active:bg-accent-400"
|
|
||||||
>
|
|
||||||
Add todo
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
tabIndex={3}
|
|
||||||
type="button"
|
|
||||||
className="base-button"
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
48
src/client/src/components/todos/collapsed/todoShortForm.tsx
Normal file
48
src/client/src/components/todos/collapsed/todoShortForm.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import TextareaAutosize from "react-textarea-autosize";
|
||||||
|
import { AddProjectButton } from "@src/components/todos/add/addProjectButton";
|
||||||
|
|
||||||
|
export const TodoShortForm = (props: {
|
||||||
|
project: string;
|
||||||
|
onProjectChanged: (project: string) => void;
|
||||||
|
name: string;
|
||||||
|
onNameChange: (e) => void;
|
||||||
|
description: string;
|
||||||
|
onDescriptionChange: (e) => void;
|
||||||
|
}) => (
|
||||||
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
|
<div className="todo-input-form md:flex-grow py-2 px-4 dark:bg-gray-800 border border-gray-200 rounded-lg space-y-2">
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="Todo name"
|
||||||
|
className="text-sm w-full"
|
||||||
|
autoFocus
|
||||||
|
value={props.name}
|
||||||
|
tabIndex={1}
|
||||||
|
onChange={props.onNameChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="h-px bg-gray-200 dark:bg-gray-500" />
|
||||||
|
<div>
|
||||||
|
<TextareaAutosize
|
||||||
|
placeholder="Description"
|
||||||
|
className="text-sm w-full h-full resize-none overflow-auto"
|
||||||
|
value={props.description}
|
||||||
|
tabIndex={2}
|
||||||
|
onChange={props.onDescriptionChange}
|
||||||
|
minRows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row pb-1">
|
||||||
|
<AddProjectButton
|
||||||
|
onProjectChanged={props.onProjectChanged}
|
||||||
|
initialProject={props.project}
|
||||||
|
>
|
||||||
|
{props.project}
|
||||||
|
</AddProjectButton>
|
||||||
|
<div className="flex-grow w-1/2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
@ -19,7 +19,7 @@ const EditTodo: FC<EditTodoProps> = ({ todo, onCancel, onSave }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 flex-grow">
|
<div className="space-y-4 flex-grow">
|
||||||
<div className="bg-gray-900 rounded-lg">
|
<div className="dark:bg-gray-900 rounded-lg">
|
||||||
<input
|
<input
|
||||||
className="py-2 px-4"
|
className="py-2 px-4"
|
||||||
value={todoTitle}
|
value={todoTitle}
|
||||||
@ -28,7 +28,7 @@ const EditTodo: FC<EditTodoProps> = ({ todo, onCancel, onSave }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="todo-project py-2 px-4 bg-gray-700 border border-gray-500 rounded-lg ">
|
<div className="todo-project py-2 px-4 dark:bg-gray-700 border border-gray-500 rounded-lg ">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Project name"
|
placeholder="Project name"
|
||||||
@ -64,7 +64,7 @@ const TodoDetails: FC<TodoDetailsProps> = ({ todo }) => {
|
|||||||
const [editMode, setEditMode] = useState(false);
|
const [editMode, setEditMode] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-gray-800 rounded-xl p-8 shadow-lg space-y-4">
|
<div className="dark:bg-gray-800 rounded-xl p-8 shadow-lg space-y-4">
|
||||||
<PageHeading title={todo.project || "Inbox"} />
|
<PageHeading title={todo.project || "Inbox"} />
|
||||||
<div className="flex flex-row items-center gap-4">
|
<div className="flex flex-row items-center gap-4">
|
||||||
{editMode ? (
|
{editMode ? (
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
|
|
||||||
input {
|
input, textarea {
|
||||||
@apply shadow-none placeholder-gray-50 dark:placeholder-gray-500 border-none outline-none;
|
@apply shadow-none placeholder-gray-50 placeholder-gray-500 border-none outline-none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,12 +17,14 @@ input {
|
|||||||
px-4
|
px-4
|
||||||
rounded-lg
|
rounded-lg
|
||||||
transition
|
transition
|
||||||
bg-gray-700
|
dark:bg-gray-700
|
||||||
disabled:bg-gray-800
|
disabled:bg-gray-200
|
||||||
active:bg-gray-600
|
active:bg-gray-300
|
||||||
|
disabled:dark:bg-gray-800
|
||||||
|
active:dark:bg-gray-600
|
||||||
border
|
border
|
||||||
border-gray-500
|
border-gray-300
|
||||||
disabled:border-gray-600
|
disabled:border-gray-400
|
||||||
text-sm
|
text-sm
|
||||||
font-semibold
|
font-semibold
|
||||||
disabled:text-gray-300
|
disabled:text-gray-300
|
||||||
|
@ -4577,6 +4577,15 @@ react-refresh@0.8.3:
|
|||||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
|
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
|
||||||
integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==
|
integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==
|
||||||
|
|
||||||
|
react-textarea-autosize@^8.3.3:
|
||||||
|
version "8.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.3.3.tgz#f70913945369da453fd554c168f6baacd1fa04d8"
|
||||||
|
integrity sha512-2XlHXK2TDxS6vbQaoPbMOfQ8GK7+irc2fVK6QFIcC8GOnH3zI/v481n+j1L0WaPVvKxwesnY93fEfH++sus2rQ==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.10.2"
|
||||||
|
use-composed-ref "^1.0.0"
|
||||||
|
use-latest "^1.0.0"
|
||||||
|
|
||||||
react@17.0.2:
|
react@17.0.2:
|
||||||
version "17.0.2"
|
version "17.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
||||||
@ -5321,6 +5330,11 @@ tr46@~0.0.3:
|
|||||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||||
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
|
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
|
||||||
|
|
||||||
|
ts-essentials@^2.0.3:
|
||||||
|
version "2.0.12"
|
||||||
|
resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-2.0.12.tgz#c9303f3d74f75fa7528c3d49b80e089ab09d8745"
|
||||||
|
integrity sha512-3IVX4nI6B5cc31/GFFE+i8ey/N2eA0CZDbo6n0yrz0zDX8ZJ8djmU1p+XRz7G3is0F3bB3pu2pAroFdAWQKU3w==
|
||||||
|
|
||||||
tsconfig-paths@^3.11.0, tsconfig-paths@^3.9.0:
|
tsconfig-paths@^3.11.0, tsconfig-paths@^3.9.0:
|
||||||
version "3.11.0"
|
version "3.11.0"
|
||||||
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz#954c1fe973da6339c78e06b03ce2e48810b65f36"
|
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz#954c1fe973da6339c78e06b03ce2e48810b65f36"
|
||||||
@ -5455,6 +5469,25 @@ url-parse@^1.4.3:
|
|||||||
querystringify "^2.1.1"
|
querystringify "^2.1.1"
|
||||||
requires-port "^1.0.0"
|
requires-port "^1.0.0"
|
||||||
|
|
||||||
|
use-composed-ref@^1.0.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.1.0.tgz#9220e4e94a97b7b02d7d27eaeab0b37034438bbc"
|
||||||
|
integrity sha512-my1lNHGWsSDAhhVAT4MKs6IjBUtG6ZG11uUqexPH9PptiIZDQOzaF4f5tEbJ2+7qvNbtXNBbU3SfmN+fXlWDhg==
|
||||||
|
dependencies:
|
||||||
|
ts-essentials "^2.0.3"
|
||||||
|
|
||||||
|
use-isomorphic-layout-effect@^1.0.0:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.1.tgz#7bb6589170cd2987a152042f9084f9effb75c225"
|
||||||
|
integrity sha512-L7Evj8FGcwo/wpbv/qvSfrkHFtOpCzvM5yl2KVyDJoylVuSvzphiiasmjgQPttIGBAy2WKiBNR98q8w7PiNgKQ==
|
||||||
|
|
||||||
|
use-latest@^1.0.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/use-latest/-/use-latest-1.2.0.tgz#a44f6572b8288e0972ec411bdd0840ada366f232"
|
||||||
|
integrity sha512-d2TEuG6nSLKQLAfW3By8mKr8HurOlTkul0sOpxbClIv4SQ4iOd7BYr7VIzdbktUCnv7dua/60xzd8igMU6jmyw==
|
||||||
|
dependencies:
|
||||||
|
use-isomorphic-layout-effect "^1.0.0"
|
||||||
|
|
||||||
use-subscription@1.5.1:
|
use-subscription@1.5.1:
|
||||||
version "1.5.1"
|
version "1.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.5.1.tgz#73501107f02fad84c6dd57965beb0b75c68c42d1"
|
resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.5.1.tgz#73501107f02fad84c6dd57965beb0b75c68c42d1"
|
||||||
|
Loading…
Reference in New Issue
Block a user