Add redux
This commit is contained in:
parent
7db2ceca08
commit
7712f63999
@ -7,5 +7,5 @@ namespace Todo.Api.Controllers;
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
public class ApiController : ControllerBase
|
public class ApiController : ControllerBase
|
||||||
{
|
{
|
||||||
public IMediator Mediator => HttpContext.RequestServices.GetRequiredService<IMediator>();
|
protected IMediator Mediator => HttpContext.RequestServices.GetRequiredService<IMediator>();
|
||||||
}
|
}
|
@ -47,8 +47,33 @@ public class TodosController : ApiController
|
|||||||
public async Task<ActionResult<IEnumerable<Core.Entities.Todo>>> GetTodos()
|
public async Task<ActionResult<IEnumerable<Core.Entities.Todo>>> GetTodos()
|
||||||
=> Ok(await _todoRepository.GetTodosAsync());
|
=> Ok(await _todoRepository.GetTodosAsync());
|
||||||
|
|
||||||
[HttpGet("not-done")]
|
[HttpPut("{todoId}")]
|
||||||
public async Task<ActionResult<IEnumerable<Core.Entities.Todo>>>
|
public async Task<ActionResult> ReplaceTodo([FromRoute] string todoId, [FromBody] ReplaceTodoRequest request)
|
||||||
GetNotDoneTodos()
|
{
|
||||||
=> Ok(await _todoRepository.GetNotDoneTodos());
|
await Mediator.Send(request.To(todoId));
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ReplaceTodoRequest
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[JsonPropertyName("title")]
|
||||||
|
public string Title { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("description")]
|
||||||
|
public string? Description { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("project")]
|
||||||
|
public string? Project { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("status")]
|
||||||
|
public bool Status { get; init; }
|
||||||
|
|
||||||
|
internal ReplaceTodoCommand To(string id) => new(
|
||||||
|
id,
|
||||||
|
Title,
|
||||||
|
Project,
|
||||||
|
Description,
|
||||||
|
Status);
|
||||||
|
}
|
||||||
}
|
}
|
@ -27,8 +27,9 @@ public class TodoPublisher : ITodoPublisher
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Publish(
|
public async Task Publish<T>(
|
||||||
string todoId,
|
string eventType,
|
||||||
|
T message,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var userId = _currentUserService.GetUserId() ??
|
var userId = _currentUserService.GetUserId() ??
|
||||||
@ -36,14 +37,33 @@ public class TodoPublisher : ITodoPublisher
|
|||||||
var connections =
|
var connections =
|
||||||
await _userConnectionStore.GetConnectionsAsync(userId);
|
await _userConnectionStore.GetConnectionsAsync(userId);
|
||||||
|
|
||||||
|
switch (eventType)
|
||||||
|
{
|
||||||
|
case "todoCreated":
|
||||||
await _hubContext
|
await _hubContext
|
||||||
.Clients
|
.Clients
|
||||||
.Clients(connections)
|
.Clients(connections)
|
||||||
.SendAsync(
|
.SendAsync(
|
||||||
"todoCreated",
|
"todoCreated",
|
||||||
todoId,
|
message,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
break;
|
||||||
|
|
||||||
_logger.LogInformation("todo created {TodoId}", todoId);
|
case "todoUpdated":
|
||||||
|
await _hubContext
|
||||||
|
.Clients
|
||||||
|
.Clients(connections)
|
||||||
|
.SendAsync(
|
||||||
|
"todoUpdated",
|
||||||
|
message,
|
||||||
|
cancellationToken);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"todo event: {EventType} with message: {Message}",
|
||||||
|
eventType,
|
||||||
|
message);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using MediatR;
|
||||||
|
using Todo.Core.Application.Notifications.Todo;
|
||||||
|
using Todo.Core.Interfaces.Persistence;
|
||||||
|
using Todo.Core.Interfaces.User;
|
||||||
|
|
||||||
|
namespace Todo.Core.Application.Commands.Todo;
|
||||||
|
|
||||||
|
public record ReplaceTodoCommand(
|
||||||
|
string Id,
|
||||||
|
string Title,
|
||||||
|
string Project,
|
||||||
|
string Description,
|
||||||
|
bool Status) : IRequest<Unit>
|
||||||
|
{
|
||||||
|
internal class Handler : IRequestHandler<ReplaceTodoCommand, Unit>
|
||||||
|
{
|
||||||
|
private readonly ICurrentUserService _currentUserService;
|
||||||
|
private readonly ITodoRepository _todoRepository;
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
public Handler(
|
||||||
|
ICurrentUserService currentUserService,
|
||||||
|
ITodoRepository todoRepository,
|
||||||
|
IMediator mediator)
|
||||||
|
{
|
||||||
|
_currentUserService = currentUserService;
|
||||||
|
_todoRepository = todoRepository;
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Unit> Handle(ReplaceTodoCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var userId = _currentUserService.GetUserId();
|
||||||
|
if (userId is null)
|
||||||
|
throw new InvalidOperationException("User was not found");
|
||||||
|
//TODO: Make sure the author actually owns the todo
|
||||||
|
await _todoRepository.UpdateTodoAsync(request.To(userId));
|
||||||
|
|
||||||
|
await _mediator.Publish(new TodoUpdated(request.Id), cancellationToken);
|
||||||
|
|
||||||
|
return Unit.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Entities.Todo To(string authorId) => new()
|
||||||
|
{
|
||||||
|
Id = Id,
|
||||||
|
Description = Description,
|
||||||
|
Project = Project,
|
||||||
|
Status = Status,
|
||||||
|
Title = Title,
|
||||||
|
AuthorId = authorId
|
||||||
|
};
|
||||||
|
}
|
@ -20,6 +20,7 @@ public record TodoCreated(
|
|||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await _todoPublisher.Publish(
|
await _todoPublisher.Publish(
|
||||||
|
"todoCreated",
|
||||||
JsonSerializer.Serialize(notification),
|
JsonSerializer.Serialize(notification),
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading;
|
||||||
|
using MediatR;
|
||||||
|
using Todo.Core.Interfaces.Publisher;
|
||||||
|
|
||||||
|
namespace Todo.Core.Application.Notifications.Todo;
|
||||||
|
|
||||||
|
public record TodoUpdated(
|
||||||
|
[property: JsonPropertyName("todoId")] string TodoId) : INotification
|
||||||
|
{
|
||||||
|
internal class Handler : INotificationHandler<TodoUpdated>
|
||||||
|
{
|
||||||
|
private readonly ITodoPublisher _todoPublisher;
|
||||||
|
|
||||||
|
public Handler(ITodoPublisher todoPublisher) => _todoPublisher = todoPublisher;
|
||||||
|
|
||||||
|
public async Task Handle(
|
||||||
|
TodoUpdated notification,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _todoPublisher.Publish(
|
||||||
|
"todoUpdated",
|
||||||
|
JsonSerializer.Serialize(notification),
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,5 +4,5 @@ namespace Todo.Core.Interfaces.Publisher;
|
|||||||
|
|
||||||
public interface ITodoPublisher
|
public interface ITodoPublisher
|
||||||
{
|
{
|
||||||
Task Publish(string todoId, CancellationToken cancellationToken = new());
|
Task Publish<T>(string eventType, T message, CancellationToken cancellationToken = new());
|
||||||
}
|
}
|
@ -16,4 +16,4 @@ public record TodoViewModel(
|
|||||||
t.AuthorId,
|
t.AuthorId,
|
||||||
t.Project,
|
t.Project,
|
||||||
t.Description);
|
t.Description);
|
||||||
};
|
}
|
@ -39,7 +39,14 @@ internal class InMemoryUserConnectionStore : IUserConnectionStore
|
|||||||
ConnectedUsers.TryGetValue(userId, out var existingUserConnectionIds);
|
ConnectedUsers.TryGetValue(userId, out var existingUserConnectionIds);
|
||||||
|
|
||||||
// remove the connection id from the List
|
// remove the connection id from the List
|
||||||
|
try
|
||||||
|
{
|
||||||
existingUserConnectionIds?.Remove(connectionId);
|
existingUserConnectionIds?.Remove(connectionId);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e);
|
||||||
|
}
|
||||||
|
|
||||||
// If there are no connection ids in the List, delete the user from the global cache (ConnectedUsers).
|
// If there are no connection ids in the List, delete the user from the global cache (ConnectedUsers).
|
||||||
if (existingUserConnectionIds?.Count == 0)
|
if (existingUserConnectionIds?.Count == 0)
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
using MongoDB.Driver;
|
using MongoDB.Driver;
|
||||||
using Todo.Core.Interfaces.Persistence;
|
using Todo.Core.Interfaces.Persistence;
|
||||||
using Todo.Persistence.Mongo.Repositories.Dtos;
|
using Todo.Persistence.Mongo.Repositories.Dtos;
|
||||||
@ -35,7 +36,8 @@ public class TodoRepository : ITodoRepository
|
|||||||
Title = todo.Title,
|
Title = todo.Title,
|
||||||
Status = false,
|
Status = false,
|
||||||
Project = todo.ProjectName,
|
Project = todo.ProjectName,
|
||||||
Description = todo.Description
|
Description = todo.Description,
|
||||||
|
AuthorId = todo.AuthorId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +54,8 @@ public class TodoRepository : ITodoRepository
|
|||||||
Title = t.Title,
|
Title = t.Title,
|
||||||
Status = t.Status,
|
Status = t.Status,
|
||||||
Project = t.ProjectName,
|
Project = t.ProjectName,
|
||||||
Description = t.Description
|
Description = t.Description,
|
||||||
|
AuthorId = t.AuthorId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,7 +87,8 @@ public class TodoRepository : ITodoRepository
|
|||||||
Project = todo.ProjectName,
|
Project = todo.ProjectName,
|
||||||
Status = todo.Status,
|
Status = todo.Status,
|
||||||
Title = todo.Title,
|
Title = todo.Title,
|
||||||
Description = todo.Description
|
Description = todo.Description,
|
||||||
|
AuthorId = todo.AuthorId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,17 +9,20 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@microsoft/signalr": "^6.0.0",
|
"@microsoft/signalr": "^6.0.0",
|
||||||
|
"@reduxjs/toolkit": "^1.6.2",
|
||||||
"@tippyjs/react": "^4.2.6",
|
"@tippyjs/react": "^4.2.6",
|
||||||
"axios": "^0.24.0",
|
"axios": "^0.24.0",
|
||||||
"next": "12.0.3",
|
"next": "12.0.3",
|
||||||
"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-redux": "^7.2.6",
|
||||||
"react-textarea-autosize": "^8.3.3",
|
"react-textarea-autosize": "^8.3.3",
|
||||||
"tailwindcss": "^2.2.19"
|
"tailwindcss": "^2.2.19"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^17.0.34",
|
"@types/react": "^17.0.34",
|
||||||
|
"@types/react-redux": "^7.1.20",
|
||||||
"autoprefixer": "^10.4.0",
|
"autoprefixer": "^10.4.0",
|
||||||
"cssnano": "^5.0.10",
|
"cssnano": "^5.0.10",
|
||||||
"eslint": "7",
|
"eslint": "7",
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
import axios from "axios";
|
|
||||||
import { Todo } from "@src/core/entities/todo";
|
|
||||||
|
|
||||||
const getBaseUrl = () =>
|
|
||||||
process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:5000";
|
|
||||||
|
|
||||||
const createBaseClient = () =>
|
|
||||||
axios.create({
|
|
||||||
baseURL: getBaseUrl(),
|
|
||||||
withCredentials: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
interface CreateTodoRequest {
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
project?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createTodoAsync = async (
|
|
||||||
createTodo: CreateTodoRequest
|
|
||||||
): Promise<string> =>
|
|
||||||
await createBaseClient().post<string, string, CreateTodoRequest>(
|
|
||||||
"/api/todos",
|
|
||||||
createTodo
|
|
||||||
);
|
|
||||||
|
|
||||||
interface GetTodoByIdResponse extends Todo {}
|
|
||||||
export const getTodoByIdAsync = async (todoId: string) =>
|
|
||||||
await createBaseClient().get<any, GetTodoByIdResponse>(
|
|
||||||
`/api/todos/${todoId}`
|
|
||||||
);
|
|
@ -10,7 +10,7 @@ export const PrimaryButton: FC<ButtonHTMLAttributes<HTMLButtonElement>> = (
|
|||||||
tabIndex={2}
|
tabIndex={2}
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
className={
|
className={
|
||||||
"base-button bg-accent-500 disabled:bg-accent-800 active:bg-accent-400 border-none text-white " +
|
"base-button bg-accent-500 disabled:bg-accent-800 dark:bg-accent-500 active:bg-accent-400 border-none text-white " +
|
||||||
props.className
|
props.className
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -3,6 +3,7 @@ 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";
|
import Link from "next/link";
|
||||||
|
import { todosApi } from "@src/infrastructure/apis/todosApi";
|
||||||
|
|
||||||
interface TodoItemProps {
|
interface TodoItemProps {
|
||||||
todo: Todo;
|
todo: Todo;
|
||||||
@ -14,10 +15,15 @@ export const TodoItem: FC<TodoItemProps> = (props) => {
|
|||||||
const [isHovering, setIsHovering] = useState(false);
|
const [isHovering, setIsHovering] = useState(false);
|
||||||
const [menuIsOpen, setMenuIsOpen] = useState(false);
|
const [menuIsOpen, setMenuIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const prefetchTodo = todosApi.usePrefetch("getTodoById");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="py-3 border-b border-gray-300 dark:border-gray-600 relative"
|
className="py-3 border-b border-gray-300 dark:border-gray-600 relative"
|
||||||
onMouseEnter={() => setIsHovering(true)}
|
onMouseEnter={() => {
|
||||||
|
prefetchTodo(props.todo.id);
|
||||||
|
setIsHovering(true);
|
||||||
|
}}
|
||||||
onMouseLeave={() => setIsHovering(false)}
|
onMouseLeave={() => setIsHovering(false)}
|
||||||
onFocus={() => setIsHovering(true)}
|
onFocus={() => setIsHovering(true)}
|
||||||
>
|
>
|
||||||
@ -32,7 +38,7 @@ export const TodoItem: FC<TodoItemProps> = (props) => {
|
|||||||
<div className="flex flex-row justify-between items-end">
|
<div className="flex flex-row justify-between items-end">
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
{props.todo.description && (
|
{props.todo.description && (
|
||||||
<div className="h-3 w-3 bg-gray-100 dark:border-black border border-gray-300 dark:bg-gray-900 rounded-sm"/>
|
<div className="h-3 w-3 bg-gray-100 dark:border-black border border-gray-300 dark:bg-gray-900 rounded-sm" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{props.displayProject && props.todo.project && (
|
{props.displayProject && props.todo.project && (
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Todo } from "@src/core/entities/todo";
|
import { Todo } from "@src/core/entities/todo";
|
||||||
import { TodoItem } from "@src/components/todos/todoItem";
|
import { TodoItem } from "@src/components/todos/todoItem";
|
||||||
import { AddTodo } from "@src/components/todos/addTodo";
|
import { AddTodo } from "@src/components/todos/addTodo";
|
||||||
import { useUpdateTodoState } from "@src/presentation/hooks/socketHooks";
|
import { useUpdateTodo } from "@src/presentation/hooks/socketHooks";
|
||||||
|
|
||||||
export const TodoList = (props: {
|
export const TodoList = (props: {
|
||||||
todos: Todo[];
|
todos: Todo[];
|
||||||
@ -9,19 +9,23 @@ export const TodoList = (props: {
|
|||||||
hideProject: boolean;
|
hideProject: boolean;
|
||||||
project: string;
|
project: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { updateTodoState } = useUpdateTodoState();
|
const { updateTodo } = useUpdateTodo();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ul id="inbox">
|
<ul id="inbox">
|
||||||
{props.todos
|
{props.todos
|
||||||
.filter((t) => t.status == false)
|
.filter((t) => {
|
||||||
|
if (!props.hideDone) return true;
|
||||||
|
|
||||||
|
return t.status == false;
|
||||||
|
})
|
||||||
.map((t, i) => (
|
.map((t, i) => (
|
||||||
<li key={i}>
|
<li key={i}>
|
||||||
<TodoItem
|
<TodoItem
|
||||||
todo={t}
|
todo={t}
|
||||||
updateTodo={(todo) => {
|
updateTodo={(todo) => {
|
||||||
updateTodoState(todo.id, todo.status);
|
updateTodo(todo);
|
||||||
}}
|
}}
|
||||||
displayProject={!props.hideProject}
|
displayProject={!props.hideProject}
|
||||||
/>
|
/>
|
||||||
|
16
src/client/src/core/actions/utility/groupByProjects.ts
Normal file
16
src/client/src/core/actions/utility/groupByProjects.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export const groupByProjects = (arr) => {
|
||||||
|
if (!arr) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const criteria = "project";
|
||||||
|
|
||||||
|
return arr.reduce(function (acc, currentValue) {
|
||||||
|
const currentValueElement = currentValue[criteria] || "inbox";
|
||||||
|
|
||||||
|
if (!acc[currentValueElement]) {
|
||||||
|
acc[currentValueElement] = [];
|
||||||
|
}
|
||||||
|
acc[currentValueElement].push(currentValue);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
};
|
63
src/client/src/infrastructure/apis/todosApi.ts
Normal file
63
src/client/src/infrastructure/apis/todosApi.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { createApi } from "@reduxjs/toolkit/query/react";
|
||||||
|
import { baseQueryWithReauth } from "@src/infrastructure/apis/utilities";
|
||||||
|
import { Todo } from "@src/core/entities/todo";
|
||||||
|
|
||||||
|
export const todosApi = createApi({
|
||||||
|
reducerPath: "api",
|
||||||
|
tagTypes: ["todo"],
|
||||||
|
baseQuery: baseQueryWithReauth,
|
||||||
|
endpoints: (build) => ({
|
||||||
|
getTodoById: build.query<Todo, string>({
|
||||||
|
query: (id) => `api/todos/${id}`,
|
||||||
|
providesTags: (result, error, id) => [{ type: "todo", id }],
|
||||||
|
}),
|
||||||
|
|
||||||
|
getAllTodos: build.query<Todo[], void>({
|
||||||
|
query: () => ({ url: `api/todos` }),
|
||||||
|
providesTags: (result) =>
|
||||||
|
result
|
||||||
|
? [
|
||||||
|
...result.map(({ id }) => ({ type: "todo" as const, id })),
|
||||||
|
{ type: "todo", id: "LIST" },
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
}),
|
||||||
|
|
||||||
|
createTodo: build.mutation<string, Partial<Todo>>({
|
||||||
|
query(body) {
|
||||||
|
return {
|
||||||
|
url: "/api/todos",
|
||||||
|
method: "POST",
|
||||||
|
body,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
invalidatesTags: (result, error, id) => [{ type: "todo", id: "LIST" }],
|
||||||
|
}),
|
||||||
|
|
||||||
|
replaceTodo: build.mutation<string, Partial<Todo>>({
|
||||||
|
query(data) {
|
||||||
|
const { id, ...body } = data;
|
||||||
|
return {
|
||||||
|
url: `/api/todos/${id}`,
|
||||||
|
method: "PUT",
|
||||||
|
body,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
invalidatesTags: (result, error, { id }) => [{ type: "todo", id }],
|
||||||
|
async onQueryStarted({ id, ...body }, { dispatch, queryFulfilled }) {
|
||||||
|
const replaceResult = dispatch(
|
||||||
|
todosApi.util.updateQueryData("getTodoById", id, (draft) => {
|
||||||
|
Object.assign(draft, body);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await queryFulfilled;
|
||||||
|
} catch {
|
||||||
|
replaceResult.undo();
|
||||||
|
dispatch(todosApi.util.invalidateTags([{ type: "todo", id }]));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
27
src/client/src/infrastructure/apis/utilities.ts
Normal file
27
src/client/src/infrastructure/apis/utilities.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import {
|
||||||
|
BaseQueryFn,
|
||||||
|
FetchArgs,
|
||||||
|
fetchBaseQuery,
|
||||||
|
FetchBaseQueryError,
|
||||||
|
} from "@reduxjs/toolkit/query/react";
|
||||||
|
|
||||||
|
export const getApiBaseUrl = () =>
|
||||||
|
process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:5000";
|
||||||
|
|
||||||
|
const baseQuery = fetchBaseQuery({
|
||||||
|
baseUrl: getApiBaseUrl(),
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
export const baseQueryWithReauth: BaseQueryFn<
|
||||||
|
string | FetchArgs,
|
||||||
|
unknown,
|
||||||
|
FetchBaseQueryError
|
||||||
|
> = async (args, api, extraOptions) => {
|
||||||
|
let result = await baseQuery(args, api, extraOptions);
|
||||||
|
if (result.error && result.error.status === 302) {
|
||||||
|
window.location.href = `${getApiBaseUrl()}/api/auth/login?returnUrl=${
|
||||||
|
window.location.href
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
23
src/client/src/infrastructure/state/todo.ts
Normal file
23
src/client/src/infrastructure/state/todo.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { createEntityAdapter, createSlice } from "@reduxjs/toolkit";
|
||||||
|
import { Todo } from "@src/core/entities/todo";
|
||||||
|
import { AppState } from "@src/infrastructure/store";
|
||||||
|
|
||||||
|
export const todosAdapter = createEntityAdapter<Todo>({
|
||||||
|
selectId: (todo) => todo.id,
|
||||||
|
sortComparer: (a, b) => a.title.localeCompare(b.title),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const todosSlice = createSlice({
|
||||||
|
name: "todo",
|
||||||
|
initialState: todosAdapter.getInitialState(),
|
||||||
|
reducers: {
|
||||||
|
todoAdded: todosAdapter.addOne,
|
||||||
|
todosReceived(state, action) {
|
||||||
|
todosAdapter.setAll(state, action.payload.todos);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const todosSelectors = todosAdapter.getSelectors<AppState>(
|
||||||
|
(state) => state.todos
|
||||||
|
);
|
6
src/client/src/infrastructure/store/hooks.ts
Normal file
6
src/client/src/infrastructure/store/hooks.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { AppDispatch, AppState } from "@src/infrastructure/store/store";
|
||||||
|
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
|
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||||
|
|
||||||
|
export const useAppSelector: TypedUseSelectorHook<AppState> = useSelector;
|
4
src/client/src/infrastructure/store/index.ts
Normal file
4
src/client/src/infrastructure/store/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from "./hooks";
|
||||||
|
export * from "./store";
|
||||||
|
|
||||||
|
export { default } from "./store";
|
30
src/client/src/infrastructure/store/store.ts
Normal file
30
src/client/src/infrastructure/store/store.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { Action, configureStore, ThunkAction } from "@reduxjs/toolkit";
|
||||||
|
import { todosSlice } from "@src/infrastructure/state/todo";
|
||||||
|
import { todosApi } from "@src/infrastructure/apis/todosApi";
|
||||||
|
import { setupListeners } from "@reduxjs/toolkit/query";
|
||||||
|
|
||||||
|
export function makeStore() {
|
||||||
|
return configureStore({
|
||||||
|
reducer: {
|
||||||
|
todos: todosSlice.reducer,
|
||||||
|
[todosApi.reducerPath]: todosApi.reducer,
|
||||||
|
},
|
||||||
|
middleware: (getDefaultMiddleware) =>
|
||||||
|
getDefaultMiddleware().concat(todosApi.middleware),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = makeStore();
|
||||||
|
|
||||||
|
setupListeners(store.dispatch);
|
||||||
|
|
||||||
|
export type AppState = ReturnType<typeof store.getState>;
|
||||||
|
export type AppDispatch = typeof store.dispatch;
|
||||||
|
export type AppThunk<ReturnType = void> = ThunkAction<
|
||||||
|
ReturnType,
|
||||||
|
AppState,
|
||||||
|
unknown,
|
||||||
|
Action<string>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export default store;
|
@ -1,8 +1,10 @@
|
|||||||
import { AppProps } from "next/app";
|
import type { AppProps } from "next/app";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
|
||||||
import "@src/styles/tailwind.css";
|
import "@src/styles/tailwind.css";
|
||||||
import SocketProvider from "@src/presentation/contexts/SocketContext";
|
import SocketProvider from "@src/presentation/contexts/SocketContext";
|
||||||
|
import store from "@src/infrastructure/store";
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
|
||||||
const MyApp = ({ Component, pageProps }: AppProps) => (
|
const MyApp = ({ Component, pageProps }: AppProps) => (
|
||||||
<>
|
<>
|
||||||
@ -34,9 +36,11 @@ const MyApp = ({ Component, pageProps }: AppProps) => (
|
|||||||
<meta name="theme-color" content="#317EFB" />
|
<meta name="theme-color" content="#317EFB" />
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
|
<Provider store={store}>
|
||||||
<SocketProvider>
|
<SocketProvider>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</SocketProvider>
|
</SocketProvider>
|
||||||
|
</Provider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
export default MyApp;
|
export default MyApp;
|
||||||
|
@ -1,40 +1,33 @@
|
|||||||
import { PageHeading } from "@src/components/common/headings/pageHeading";
|
import { PageHeading } from "@src/components/common/headings/pageHeading";
|
||||||
import { TodoList } from "@src/components/todos";
|
import { TodoList } from "@src/components/todos";
|
||||||
import { useSelectInboxTodos } from "@src/presentation/hooks/socketHooks";
|
|
||||||
import { ProjectsHeading } from "@src/components/common/headings/projectsHeading";
|
import { ProjectsHeading } from "@src/components/common/headings/projectsHeading";
|
||||||
|
import { groupByProjects } from "@src/core/actions/utility/groupByProjects";
|
||||||
|
import { todosApi } from "@src/infrastructure/apis/todosApi";
|
||||||
import { AddTodo } from "@src/components/todos/addTodo";
|
import { AddTodo } from "@src/components/todos/addTodo";
|
||||||
|
|
||||||
const groupByProjects = (arr) => {
|
|
||||||
if (!arr) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
const criteria = "project";
|
|
||||||
|
|
||||||
return arr.reduce(function (acc, currentValue) {
|
|
||||||
const currentValueElement = currentValue[criteria] || "inbox";
|
|
||||||
|
|
||||||
if (!acc[currentValueElement]) {
|
|
||||||
acc[currentValueElement] = [];
|
|
||||||
}
|
|
||||||
acc[currentValueElement].push(currentValue);
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
};
|
|
||||||
|
|
||||||
const HomePage = () => {
|
const HomePage = () => {
|
||||||
const { todos, loading } = useSelectInboxTodos();
|
const { data, isLoading } = todosApi.useGetAllTodosQuery();
|
||||||
const groupedTodos = groupByProjects(todos);
|
|
||||||
|
if (isLoading) {
|
||||||
|
return "loading...";
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupedTodos = groupByProjects(data);
|
||||||
const sections = Object.keys(groupedTodos)
|
const sections = Object.keys(groupedTodos)
|
||||||
.filter((s) => s !== "inbox")
|
.filter((s) => s !== "inbox")
|
||||||
.sort();
|
.sort();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="py-8 px-14 space-y-6">
|
<main className="py-8 px-14 space-y-6">
|
||||||
<section className="space-y-8 mx-auto max-w-4xl">
|
<section className="space-y-8 mx-auto max-w-4xl">
|
||||||
{groupedTodos["inbox"] && (
|
{groupedTodos["inbox"] && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<PageHeading title="Inbox" />
|
<PageHeading title="Inbox" />
|
||||||
<TodoList todos={groupedTodos["inbox"]} hideDone project={""} hideProject={false} />
|
<TodoList
|
||||||
|
todos={groupedTodos["inbox"]}
|
||||||
|
hideDone
|
||||||
|
project={""}
|
||||||
|
hideProject={false}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{sections &&
|
{sections &&
|
||||||
@ -49,7 +42,7 @@ const HomePage = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{!loading && todos && todos.length === 0 && (
|
{data && data.length === 0 && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<PageHeading title="You're done!" />
|
<PageHeading title="You're done!" />
|
||||||
<AddTodo project={""} />
|
<AddTodo project={""} />
|
||||||
|
@ -98,7 +98,11 @@ const TodoDetailsPage = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { todoId } = router.query as { todoId: string };
|
const { todoId } = router.query as { todoId: string };
|
||||||
|
|
||||||
const { todo } = useSelectTodo(todoId);
|
const { todo, isLoading } = useSelectTodo(todoId);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return "loading...";
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="py-8 px-14 space-y-6">
|
<main className="py-8 px-14 space-y-6">
|
||||||
|
@ -1,86 +1,40 @@
|
|||||||
import { createContext, FC, useEffect, useState } from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
HubConnection,
|
HubConnection,
|
||||||
HubConnectionBuilder,
|
HubConnectionBuilder,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
} from "@microsoft/signalr";
|
} from "@microsoft/signalr";
|
||||||
import { asTodo, StatusState, Todo } from "@src/core/entities/todo";
|
import { getApiBaseUrl } from "@src/infrastructure/apis/utilities";
|
||||||
import { getTodoByIdAsync } from "@src/boundary/todo/todoApi";
|
import { useAppDispatch } from "@src/infrastructure/store";
|
||||||
|
import { todosApi } from "@src/infrastructure/apis/todosApi";
|
||||||
interface SocketContextProps {
|
|
||||||
conn: HubConnection;
|
|
||||||
todos: Todo[];
|
|
||||||
inboxTodos: Todo[];
|
|
||||||
getTodos: () => void;
|
|
||||||
getInboxTodos: () => void;
|
|
||||||
createTodo: (todoName: string, project: string, description: string) => void;
|
|
||||||
updateTodo: (todoId: string, todoStatus: StatusState) => void;
|
|
||||||
getTodoById(todoId: string): void;
|
|
||||||
replaceTodo(todo: Todo): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SocketContext = createContext<SocketContextProps>({
|
|
||||||
conn: void 0,
|
|
||||||
todos: [],
|
|
||||||
inboxTodos: [],
|
|
||||||
getTodos: () => {},
|
|
||||||
getInboxTodos: () => {},
|
|
||||||
createTodo: (todoName, project, description) => {},
|
|
||||||
updateTodo: (todoId, todoStatus) => {},
|
|
||||||
getTodoById(todoId: string) {},
|
|
||||||
replaceTodo(todo: Todo) {},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const SocketProvider: FC = (props) => {
|
export const SocketProvider: FC = (props) => {
|
||||||
const [conn, setConn] = useState<HubConnection>();
|
const [conn, setConn] = useState<HubConnection>();
|
||||||
const [todos, setTodos] = useState<Todo[]>([]);
|
const dispatch = useAppDispatch();
|
||||||
const [inboxTodos, setInboxTodos] = useState<Todo[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const serverUrl =
|
|
||||||
process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:5000";
|
|
||||||
|
|
||||||
const connection = new HubConnectionBuilder()
|
const connection = new HubConnectionBuilder()
|
||||||
.withUrl(`${serverUrl}/hubs/todo`, {
|
.withUrl(`${getApiBaseUrl()}/hubs/todo`, {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
})
|
})
|
||||||
.withAutomaticReconnect()
|
.withAutomaticReconnect()
|
||||||
.configureLogging(LogLevel.Information)
|
.configureLogging(LogLevel.Information)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
connection.on("getTodos", (rawTodos) => {
|
|
||||||
const newTodos = JSON.parse(rawTodos) as Todo[];
|
|
||||||
const validatedTodos = newTodos.map(asTodo);
|
|
||||||
setTodos(validatedTodos);
|
|
||||||
});
|
|
||||||
|
|
||||||
connection.on("todoCreated", (todoCreated) => {
|
connection.on("todoCreated", (todoCreated) => {
|
||||||
const { todoId } = JSON.parse(todoCreated) as { todoId: string };
|
const { todoId } = JSON.parse(todoCreated) as { todoId: string };
|
||||||
|
if (todoId) {
|
||||||
getTodoByIdAsync(todoId).then((response) => {
|
dispatch(todosApi.endpoints.getTodoById.initiate(todoId));
|
||||||
setTodos([...todos.filter((t) => t.id !== response.id), response]);
|
dispatch(todosApi.util.invalidateTags([{ type: "todo", id: "LIST" }]));
|
||||||
setInboxTodos([
|
}
|
||||||
...inboxTodos.filter((t) => t.id !== response.id),
|
|
||||||
response,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
connection.on("getInboxTodos", (rawTodos) => {
|
connection.on("todoUpdated", (todoUpdated) => {
|
||||||
const newTodos = JSON.parse(rawTodos) as Todo[];
|
const { todoId } = JSON.parse(todoUpdated) as { todoId: string };
|
||||||
const validatedTodos = newTodos
|
if (todoId) {
|
||||||
.map(asTodo)
|
dispatch(todosApi.util.invalidateTags([{ type: "todo", id: todoId }]));
|
||||||
.filter((t) => t.status == false);
|
dispatch(todosApi.util.invalidateTags([{ type: "todo", id: "LIST" }]));
|
||||||
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
|
connection
|
||||||
@ -89,41 +43,13 @@ export const SocketProvider: FC = (props) => {
|
|||||||
setConn(connection);
|
setConn(connection);
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
window.location.href = `${serverUrl}/api/auth/login?returnUrl=${window.location.href}`;
|
window.location.href = `${getApiBaseUrl()}/api/auth/login?returnUrl=${
|
||||||
|
window.location.href
|
||||||
|
}`;
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return <>{props.children}</>;
|
||||||
<SocketContext.Provider
|
|
||||||
value={{
|
|
||||||
conn,
|
|
||||||
todos,
|
|
||||||
inboxTodos,
|
|
||||||
getTodos: () => {
|
|
||||||
conn.invoke("GetTodos").catch(console.error);
|
|
||||||
},
|
|
||||||
getInboxTodos: () => {
|
|
||||||
conn.invoke("GetInboxTodos").catch(console.error);
|
|
||||||
},
|
|
||||||
createTodo: (todoName, project, description) => {
|
|
||||||
conn
|
|
||||||
.invoke("CreateTodo", todoName, project, description)
|
|
||||||
.catch(console.error);
|
|
||||||
},
|
|
||||||
updateTodo: (todoId, todoStatus) => {
|
|
||||||
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"}
|
|
||||||
</SocketContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SocketProvider;
|
export default SocketProvider;
|
||||||
|
@ -1,64 +1,51 @@
|
|||||||
import { useContext, useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { SocketContext } from "@src/presentation/contexts/SocketContext";
|
import { useAppSelector } from "@src/infrastructure/store";
|
||||||
import { StatusState, Todo } from "@src/core/entities/todo";
|
import { todosSelectors } from "@src/infrastructure/state/todo";
|
||||||
import { createTodoAsync } from "@src/boundary/todo/todoApi";
|
import { todosApi } from "@src/infrastructure/apis/todosApi";
|
||||||
|
import { Todo } from "@src/core/entities/todo";
|
||||||
|
|
||||||
export const useSelectInboxTodos = () => {
|
export const useSelectInboxTodos = () => {
|
||||||
const socketContext = useContext(SocketContext);
|
const todos = useAppSelector(todosSelectors.selectAll);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
socketContext.getInboxTodos();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
todos: socketContext.inboxTodos,
|
todos,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCreateTodo = () => {
|
export const useCreateTodo = () => {
|
||||||
const socketContext = useContext(SocketContext);
|
const [createTodo, { isLoading }] = todosApi.useCreateTodoMutation();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createTodo: (todoName: string, project: string, description: string) => {
|
createTodo: (todoName: string, project: string, description: string) => {
|
||||||
//socketContext.createTodo(todoName, project, description);
|
createTodo({
|
||||||
createTodoAsync({
|
title: todoName,
|
||||||
project,
|
project,
|
||||||
description,
|
description,
|
||||||
title: todoName,
|
});
|
||||||
}).catch(console.error);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdateTodoState = () => {
|
export const useSelectTodo = (
|
||||||
const socketContext = useContext(SocketContext);
|
todoId: string
|
||||||
|
): { todo: Todo | undefined; isLoading: boolean } => {
|
||||||
|
const { data, isLoading } = todosApi.useGetTodoByIdQuery(todoId);
|
||||||
|
|
||||||
|
useEffect(() => {}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
updateTodoState: (todoId: string, todoState: StatusState) => {
|
todo: data,
|
||||||
socketContext.updateTodo(todoId, todoState);
|
isLoading: isLoading,
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
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 = () => {
|
export const useUpdateTodo = () => {
|
||||||
const socketContext = useContext(SocketContext);
|
const [replaceTodo, { isLoading }] = todosApi.useReplaceTodoMutation();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
updateTodo: (todo: Todo) => {
|
updateTodo: (todo: Todo) => {
|
||||||
socketContext.replaceTodo(todo);
|
replaceTodo(todo);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -875,7 +875,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.4"
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.16.3", "@babel/runtime@^7.8.4":
|
"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
|
||||||
version "7.16.3"
|
version "7.16.3"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5"
|
||||||
integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==
|
integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==
|
||||||
@ -1107,6 +1107,16 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.2.tgz#0798c03351f0dea1a5a4cabddf26a55a7cbee590"
|
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.2.tgz#0798c03351f0dea1a5a4cabddf26a55a7cbee590"
|
||||||
integrity sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==
|
integrity sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==
|
||||||
|
|
||||||
|
"@reduxjs/toolkit@^1.6.2":
|
||||||
|
version "1.6.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.6.2.tgz#2f2b5365df77dd6697da28fdf44f33501ed9ba37"
|
||||||
|
integrity sha512-HbfI/hOVrAcMGAYsMWxw3UJyIoAS9JTdwddsjlr5w3S50tXhWb+EMyhIw+IAvCVCLETkzdjgH91RjDSYZekVBA==
|
||||||
|
dependencies:
|
||||||
|
immer "^9.0.6"
|
||||||
|
redux "^4.1.0"
|
||||||
|
redux-thunk "^2.3.0"
|
||||||
|
reselect "^4.0.0"
|
||||||
|
|
||||||
"@rollup/plugin-babel@^5.2.0":
|
"@rollup/plugin-babel@^5.2.0":
|
||||||
version "5.3.0"
|
version "5.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz#9cb1c5146ddd6a4968ad96f209c50c62f92f9879"
|
resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz#9cb1c5146ddd6a4968ad96f209c50c62f92f9879"
|
||||||
@ -1182,6 +1192,14 @@
|
|||||||
"@types/minimatch" "*"
|
"@types/minimatch" "*"
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/hoist-non-react-statics@^3.3.0":
|
||||||
|
version "3.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
|
||||||
|
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
|
||||||
|
dependencies:
|
||||||
|
"@types/react" "*"
|
||||||
|
hoist-non-react-statics "^3.3.0"
|
||||||
|
|
||||||
"@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8":
|
"@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8":
|
||||||
version "7.0.9"
|
version "7.0.9"
|
||||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
|
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
|
||||||
@ -1212,6 +1230,25 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11"
|
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11"
|
||||||
integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==
|
integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==
|
||||||
|
|
||||||
|
"@types/react-redux@^7.1.20":
|
||||||
|
version "7.1.20"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.20.tgz#42f0e61ababb621e12c66c96dda94c58423bd7df"
|
||||||
|
integrity sha512-q42es4c8iIeTgcnB+yJgRTTzftv3eYYvCZOh1Ckn2eX/3o5TdsQYKUWpLoLuGlcY/p+VAhV9IOEZJcWk/vfkXw==
|
||||||
|
dependencies:
|
||||||
|
"@types/hoist-non-react-statics" "^3.3.0"
|
||||||
|
"@types/react" "*"
|
||||||
|
hoist-non-react-statics "^3.3.0"
|
||||||
|
redux "^4.0.0"
|
||||||
|
|
||||||
|
"@types/react@*":
|
||||||
|
version "17.0.35"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.35.tgz#217164cf830267d56cd1aec09dcf25a541eedd4c"
|
||||||
|
integrity sha512-r3C8/TJuri/SLZiiwwxQoLAoavaczARfT9up9b4Jr65+ErAUX3MIkU0oMOQnrpfgHme8zIqZLX7O5nnjm5Wayw==
|
||||||
|
dependencies:
|
||||||
|
"@types/prop-types" "*"
|
||||||
|
"@types/scheduler" "*"
|
||||||
|
csstype "^3.0.2"
|
||||||
|
|
||||||
"@types/react@^17.0.34":
|
"@types/react@^17.0.34":
|
||||||
version "17.0.34"
|
version "17.0.34"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.34.tgz#797b66d359b692e3f19991b6b07e4b0c706c0102"
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.34.tgz#797b66d359b692e3f19991b6b07e4b0c706c0102"
|
||||||
@ -3060,6 +3097,13 @@ hmac-drbg@^1.0.1:
|
|||||||
minimalistic-assert "^1.0.0"
|
minimalistic-assert "^1.0.0"
|
||||||
minimalistic-crypto-utils "^1.0.1"
|
minimalistic-crypto-utils "^1.0.1"
|
||||||
|
|
||||||
|
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
|
||||||
|
version "3.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||||
|
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
|
||||||
|
dependencies:
|
||||||
|
react-is "^16.7.0"
|
||||||
|
|
||||||
hsl-regex@^1.0.0:
|
hsl-regex@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e"
|
resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e"
|
||||||
@ -3132,6 +3176,11 @@ image-size@1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
queue "6.0.2"
|
queue "6.0.2"
|
||||||
|
|
||||||
|
immer@^9.0.6:
|
||||||
|
version "9.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.6.tgz#7a96bf2674d06c8143e327cbf73539388ddf1a73"
|
||||||
|
integrity sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ==
|
||||||
|
|
||||||
import-cwd@^3.0.0:
|
import-cwd@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-3.0.0.tgz#20845547718015126ea9b3676b7592fb8bd4cf92"
|
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-3.0.0.tgz#20845547718015126ea9b3676b7592fb8bd4cf92"
|
||||||
@ -4574,16 +4623,28 @@ react-dom@17.0.2:
|
|||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
scheduler "^0.20.2"
|
scheduler "^0.20.2"
|
||||||
|
|
||||||
react-is@17.0.2:
|
react-is@17.0.2, react-is@^17.0.2:
|
||||||
version "17.0.2"
|
version "17.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||||
|
|
||||||
react-is@^16.8.1:
|
react-is@^16.7.0, react-is@^16.8.1:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||||
|
|
||||||
|
react-redux@^7.2.6:
|
||||||
|
version "7.2.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.6.tgz#49633a24fe552b5f9caf58feb8a138936ddfe9aa"
|
||||||
|
integrity sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.15.4"
|
||||||
|
"@types/react-redux" "^7.1.20"
|
||||||
|
hoist-non-react-statics "^3.3.2"
|
||||||
|
loose-envify "^1.4.0"
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
react-is "^17.0.2"
|
||||||
|
|
||||||
react-refresh@0.8.3:
|
react-refresh@0.8.3:
|
||||||
version "0.8.3"
|
version "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"
|
||||||
@ -4644,6 +4705,18 @@ reduce-css-calc@^2.1.8:
|
|||||||
css-unit-converter "^1.1.1"
|
css-unit-converter "^1.1.1"
|
||||||
postcss-value-parser "^3.3.0"
|
postcss-value-parser "^3.3.0"
|
||||||
|
|
||||||
|
redux-thunk@^2.3.0:
|
||||||
|
version "2.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.0.tgz#ac89e1d6b9bdb9ee49ce69a69071be41bbd82d67"
|
||||||
|
integrity sha512-/y6ZKQNU/0u8Bm7ROLq9Pt/7lU93cT0IucYMrubo89ENjxPa7i8pqLKu6V4X7/TvYovQ6x01unTeyeZ9lgXiTA==
|
||||||
|
|
||||||
|
redux@^4.0.0, redux@^4.1.0:
|
||||||
|
version "4.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.2.tgz#140f35426d99bb4729af760afcf79eaaac407104"
|
||||||
|
integrity sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.9.2"
|
||||||
|
|
||||||
regenerate-unicode-properties@^9.0.0:
|
regenerate-unicode-properties@^9.0.0:
|
||||||
version "9.0.0"
|
version "9.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz#54d09c7115e1f53dc2314a974b32c1c344efe326"
|
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz#54d09c7115e1f53dc2314a974b32c1c344efe326"
|
||||||
@ -4720,6 +4793,11 @@ requires-port@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
||||||
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
|
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
|
||||||
|
|
||||||
|
reselect@^4.0.0:
|
||||||
|
version "4.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.4.tgz#66df0aff41b6ee0f51e2cc17cfaf2c1995916f32"
|
||||||
|
integrity sha512-i1LgXw8DKSU5qz1EV0ZIKz4yIUHJ7L3bODh+Da6HmVSm9vdL/hG7IpbgzQ3k2XSirzf8/eI7OMEs81gb1VV2fQ==
|
||||||
|
|
||||||
resolve-from@^4.0.0:
|
resolve-from@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
|
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
|
||||||
|
Loading…
Reference in New Issue
Block a user