Can now update title
This commit is contained in:
parent
6ed71eb8f8
commit
1865425d38
@ -0,0 +1,18 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Todo.Api.Hubs.Models;
|
||||||
|
|
||||||
|
public record UpdateTodoRequest
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string Id { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("title")]
|
||||||
|
public string Title { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("status")]
|
||||||
|
public bool Status { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("project")]
|
||||||
|
public string Project { get; set; }
|
||||||
|
}
|
@ -51,7 +51,6 @@ namespace Todo.Api.Hubs
|
|||||||
await Clients.Caller.SendAsync("todos", serializedTodos);
|
await Clients.Caller.SendAsync("todos", serializedTodos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task GetInboxTodos()
|
public async Task GetInboxTodos()
|
||||||
{
|
{
|
||||||
var todos = await _todoRepository.GetNotDoneTodos();
|
var todos = await _todoRepository.GetNotDoneTodos();
|
||||||
@ -61,5 +60,44 @@ namespace Todo.Api.Hubs
|
|||||||
|
|
||||||
await Clients.Caller.SendAsync("getInboxTodos", serializedTodos);
|
await Clients.Caller.SendAsync("getInboxTodos", serializedTodos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task GetTodo(string todoId)
|
||||||
|
{
|
||||||
|
var todo = await _todoRepository.GetTodoByIdAsync(todoId);
|
||||||
|
var serializedTodo = JsonSerializer.Serialize(new TodoResponse()
|
||||||
|
{
|
||||||
|
Id = todo.Id,
|
||||||
|
Project = todo.Project,
|
||||||
|
Status = todo.Status,
|
||||||
|
Title = todo.Title,
|
||||||
|
});
|
||||||
|
|
||||||
|
await Clients.Caller.SendAsync("getTodo", serializedTodo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ReplaceTodo(string updateTodoRequest)
|
||||||
|
{
|
||||||
|
var updateTodo = JsonSerializer.Deserialize<UpdateTodoRequest>(updateTodoRequest);
|
||||||
|
if (updateTodo is null)
|
||||||
|
throw new InvalidOperationException("Could not parse invalid updateTodo");
|
||||||
|
|
||||||
|
var updatedTodo = await _todoRepository.UpdateTodoAsync(new Core.Entities.Todo()
|
||||||
|
{
|
||||||
|
Id = updateTodo.Id,
|
||||||
|
Project = updateTodo.Project,
|
||||||
|
Status = updateTodo.Status,
|
||||||
|
Title = updateTodo.Title
|
||||||
|
});
|
||||||
|
|
||||||
|
var serializedTodo = JsonSerializer.Serialize(new TodoResponse()
|
||||||
|
{
|
||||||
|
Id = updatedTodo.Id,
|
||||||
|
Project = updatedTodo.Project,
|
||||||
|
Status = updatedTodo.Status,
|
||||||
|
Title = updatedTodo.Title,
|
||||||
|
});
|
||||||
|
|
||||||
|
await Clients.Caller.SendAsync("getTodo", serializedTodo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
namespace Todo.Core.Interfaces.Persistence;
|
namespace Todo.Core.Interfaces.Persistence;
|
||||||
|
|
||||||
public interface ITodoRepository
|
public interface ITodoRepository
|
||||||
@ -6,4 +7,6 @@ public interface ITodoRepository
|
|||||||
Task<IEnumerable<Entities.Todo>> GetTodosAsync();
|
Task<IEnumerable<Entities.Todo>> GetTodosAsync();
|
||||||
Task UpdateTodoStatus(string todoId, bool todoStatus);
|
Task UpdateTodoStatus(string todoId, bool todoStatus);
|
||||||
Task<IEnumerable<Entities.Todo>> GetNotDoneTodos();
|
Task<IEnumerable<Entities.Todo>> GetNotDoneTodos();
|
||||||
|
Task<Entities.Todo> GetTodoByIdAsync(string todoId);
|
||||||
|
Task<Entities.Todo> UpdateTodoAsync(Entities.Todo todo);
|
||||||
}
|
}
|
@ -44,4 +44,37 @@ public class TodoRepository : ITodoRepository
|
|||||||
var todos = await GetTodosAsync();
|
var todos = await GetTodosAsync();
|
||||||
return todos.Where(t => t.Status == false);
|
return todos.Where(t => t.Status == false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Core.Entities.Todo> GetTodoByIdAsync(string todoId)
|
||||||
|
{
|
||||||
|
var todoCursor = await _todosCollection.FindAsync(f => f.Id == todoId);
|
||||||
|
var todo = await todoCursor.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
return new Core.Entities.Todo()
|
||||||
|
{
|
||||||
|
Id = todo.Id,
|
||||||
|
Project = todo.ProjectName,
|
||||||
|
Status = todo.Status,
|
||||||
|
Title = todo.Title
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Core.Entities.Todo> UpdateTodoAsync(Core.Entities.Todo todo)
|
||||||
|
{
|
||||||
|
var updatedTodo = await _todosCollection.FindOneAndReplaceAsync<MongoTodo>(f => f.Id == todo.Id, new MongoTodo()
|
||||||
|
{
|
||||||
|
Id = todo.Id,
|
||||||
|
Status = todo.Status,
|
||||||
|
Title = todo.Title,
|
||||||
|
ProjectName = todo.Project
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Core.Entities.Todo()
|
||||||
|
{
|
||||||
|
Id = updatedTodo.Id,
|
||||||
|
Project = updatedTodo.ProjectName,
|
||||||
|
Status = updatedTodo.Status,
|
||||||
|
Title = updatedTodo.Title
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
@ -13,7 +13,7 @@ export const TodoCheckmark: FC<TodoCheckmarkProps> = (props) => (
|
|||||||
}
|
}
|
||||||
className={`todo-checkmark h-5 w-5 rounded-full border dark:border-gray-500 ${
|
className={`todo-checkmark h-5 w-5 rounded-full border dark:border-gray-500 ${
|
||||||
props.todo.status === StatusState.done
|
props.todo.status === StatusState.done
|
||||||
? "dark:bg-gray-700"
|
? "dark:bg-gray-500"
|
||||||
: "hover:dark:bg-gray-600"
|
: "hover:dark:bg-gray-600"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
|
@ -2,6 +2,7 @@ import { Todo } from "@src/core/entities/todo";
|
|||||||
import { FC, useState } from "react";
|
import { FC, useState } from "react";
|
||||||
import { TodoCheckmark } from "@src/components/todos/todoCheckmark";
|
import { TodoCheckmark } from "@src/components/todos/todoCheckmark";
|
||||||
import Tippy from "@tippyjs/react";
|
import Tippy from "@tippyjs/react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
interface TodoItemProps {
|
interface TodoItemProps {
|
||||||
todo: Todo;
|
todo: Todo;
|
||||||
@ -23,9 +24,11 @@ export const TodoItem: FC<TodoItemProps> = (props) => {
|
|||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<TodoCheckmark {...props} />
|
<TodoCheckmark {...props} />
|
||||||
<div className="flex flex-col md:flex-row flex-grow gap-0.5 md:gap-2 pr-6">
|
<div className="flex flex-col md:flex-row flex-grow gap-0.5 md:gap-2 pr-6">
|
||||||
<div className="flex-grow w-full break-all text-sm">
|
<Link href={`/todos/${props.todo.id}`} passHref>
|
||||||
{props.todo.title}
|
<a className="flex-grow w-full break-all text-sm">
|
||||||
</div>
|
{props.todo.title}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
<div>
|
<div>
|
||||||
{props.displayProject && props.todo.project && (
|
{props.displayProject && props.todo.project && (
|
||||||
<div className="text-gray-500 text-xs text-right whitespace-nowrap place-self-end">
|
<div className="text-gray-500 text-xs text-right whitespace-nowrap place-self-end">
|
||||||
@ -51,7 +54,9 @@ export const TodoItem: FC<TodoItemProps> = (props) => {
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<button className="hover:bg-accent-500">Delete</button>
|
<button className="hover:bg-accent-500">Delete</button>
|
||||||
<button>Edit</button>
|
<Link href={`/todos/${props.todo.id}`} passHref>
|
||||||
|
<a>Edit</a>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -22,7 +22,7 @@ export const asTodo = (item: Todo): Todo => {
|
|||||||
throw new Error("Validation failed: title is null");
|
throw new Error("Validation failed: title is null");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!!item.status) {
|
if (typeof item.status === "undefined") {
|
||||||
throw new Error("Validation failed: status is null");
|
throw new Error("Validation failed: status is null");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
105
src/client/src/pages/todos/[todoId]/index.tsx
Normal file
105
src/client/src/pages/todos/[todoId]/index.tsx
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
import {
|
||||||
|
useSelectTodo,
|
||||||
|
useUpdateTodo,
|
||||||
|
} from "@src/presentation/hooks/socketHooks";
|
||||||
|
import { Todo } from "@src/core/entities/todo";
|
||||||
|
import { FC, useEffect, useState } from "react";
|
||||||
|
import { PageHeading } from "@src/components/common/headings/pageHeading";
|
||||||
|
import { TodoCheckmark } from "@src/components/todos/todoCheckmark";
|
||||||
|
|
||||||
|
interface EditTodoProps {
|
||||||
|
todo: Todo;
|
||||||
|
onCancel: () => void;
|
||||||
|
onSave: (todo: Todo) => void;
|
||||||
|
}
|
||||||
|
const EditTodo: FC<EditTodoProps> = ({ todo, onCancel, onSave }) => {
|
||||||
|
const [todoTitle, setTodoTitle] = useState(todo.title);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4 flex-grow">
|
||||||
|
<div className="bg-gray-900 rounded-lg">
|
||||||
|
<input
|
||||||
|
className="py-2 px-4"
|
||||||
|
value={todoTitle}
|
||||||
|
onChange={(e) => setTodoTitle(e.target.value)}
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-x-4">
|
||||||
|
<button
|
||||||
|
className="base-button dark:bg-accent-500 disabled:bg-accent-800 active:bg-accent-400"
|
||||||
|
onClick={() => {
|
||||||
|
onSave({ ...todo, title: todoTitle });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button className="base-button" onClick={() => onCancel()}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TodoDetailsProps {
|
||||||
|
todo: Todo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TodoDetails: FC<TodoDetailsProps> = ({ todo }) => {
|
||||||
|
const [updatedTodo, setUpdatedTodo] = useState(todo);
|
||||||
|
const { updateTodo } = useUpdateTodo();
|
||||||
|
const [editMode, setEditMode] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateTodo(updatedTodo);
|
||||||
|
}, [updatedTodo]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-800 rounded-xl p-8 shadow-lg space-y-4">
|
||||||
|
<PageHeading title={updatedTodo.project || "Inbox"} />
|
||||||
|
<div className="flex flex-row items-center gap-4">
|
||||||
|
{editMode ? (
|
||||||
|
<EditTodo
|
||||||
|
todo={todo}
|
||||||
|
onCancel={() => setEditMode(false)}
|
||||||
|
onSave={(todo) => {
|
||||||
|
setUpdatedTodo(todo);
|
||||||
|
setEditMode(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<TodoCheckmark
|
||||||
|
updateTodo={(t) => {
|
||||||
|
setUpdatedTodo(t);
|
||||||
|
}}
|
||||||
|
todo={updatedTodo}
|
||||||
|
/>
|
||||||
|
<h4 className="flex-grow" onClick={() => setEditMode(true)}>
|
||||||
|
{updatedTodo.title}
|
||||||
|
</h4>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TodoDetailsPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { todoId } = router.query as { todoId: string };
|
||||||
|
|
||||||
|
const { todo } = useSelectTodo(todoId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="py-8 px-14 space-y-6">
|
||||||
|
<section className="space-y-8 mx-auto max-w-4xl">
|
||||||
|
{todo ? <TodoDetails todo={todo} /> : <div>Todo was not found</div>}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TodoDetailsPage;
|
@ -14,6 +14,8 @@ interface SocketContextProps {
|
|||||||
getInboxTodos: () => void;
|
getInboxTodos: () => void;
|
||||||
createTodo: (todoName: string, project: string) => void;
|
createTodo: (todoName: string, project: string) => void;
|
||||||
updateTodo: (todoId: string, todoStatus: StatusState) => void;
|
updateTodo: (todoId: string, todoStatus: StatusState) => void;
|
||||||
|
getTodoById(todoId: string): void;
|
||||||
|
replaceTodo(todo: Todo): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SocketContext = createContext<SocketContextProps>({
|
export const SocketContext = createContext<SocketContextProps>({
|
||||||
@ -24,6 +26,8 @@ export const SocketContext = createContext<SocketContextProps>({
|
|||||||
getInboxTodos: () => {},
|
getInboxTodos: () => {},
|
||||||
createTodo: (todoName, project) => {},
|
createTodo: (todoName, project) => {},
|
||||||
updateTodo: (todoId, todoStatus) => {},
|
updateTodo: (todoId, todoStatus) => {},
|
||||||
|
getTodoById(todoId: string) {},
|
||||||
|
replaceTodo(todo: Todo) {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const SocketProvider: FC = (props) => {
|
export const SocketProvider: FC = (props) => {
|
||||||
@ -55,6 +59,15 @@ export const SocketProvider: FC = (props) => {
|
|||||||
setInboxTodos(validatedTodos);
|
setInboxTodos(validatedTodos);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
connection.on("getTodo", (todo) => {
|
||||||
|
const newTodo = JSON.parse(todo) as Todo;
|
||||||
|
const validatedTodo = asTodo(newTodo);
|
||||||
|
setTodos([
|
||||||
|
...todos.filter((t) => t.id != validatedTodo.id),
|
||||||
|
validatedTodo,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
connection.start().then(() => {
|
connection.start().then(() => {
|
||||||
setConn(connection);
|
setConn(connection);
|
||||||
});
|
});
|
||||||
@ -78,6 +91,12 @@ export const SocketProvider: FC = (props) => {
|
|||||||
updateTodo: (todoId, todoStatus) => {
|
updateTodo: (todoId, todoStatus) => {
|
||||||
conn.invoke("UpdateTodo", todoId, todoStatus).catch(console.error);
|
conn.invoke("UpdateTodo", todoId, todoStatus).catch(console.error);
|
||||||
},
|
},
|
||||||
|
getTodoById(todoId: string) {
|
||||||
|
conn.invoke("GetTodo", todoId).catch(console.error);
|
||||||
|
},
|
||||||
|
replaceTodo: (todo) => {
|
||||||
|
conn.invoke("ReplaceTodo", JSON.stringify(todo)).catch(console.error);
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{conn ? props.children : "loading"}
|
{conn ? props.children : "loading"}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useContext, useEffect } from "react";
|
import { useContext, useEffect } from "react";
|
||||||
import { SocketContext } from "@src/presentation/contexts/SocketContext";
|
import { SocketContext } from "@src/presentation/contexts/SocketContext";
|
||||||
import { StatusState } from "@src/core/entities/todo";
|
import { StatusState, Todo } from "@src/core/entities/todo";
|
||||||
|
|
||||||
export const useSelectInboxTodos = () => {
|
export const useSelectInboxTodos = () => {
|
||||||
const socketContext = useContext(SocketContext);
|
const socketContext = useContext(SocketContext);
|
||||||
@ -34,3 +34,25 @@ export const useUpdateTodoState = () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useSelectTodo = (todoId: string): { todo: Todo | undefined } => {
|
||||||
|
const socketContext = useContext(SocketContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
socketContext.getTodoById(todoId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
todo: socketContext.todos.find((t) => t.id === todoId),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateTodo = () => {
|
||||||
|
const socketContext = useContext(SocketContext);
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateTodo: (todo: Todo) => {
|
||||||
|
socketContext.replaceTodo(todo);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user