Add basic todo behavior

This commit is contained in:
Kasper Juul Hermansen 2021-11-13 17:35:33 +01:00
parent 43a9187579
commit 62630d63f2
Signed by: kjuulh
GPG Key ID: DCD9397082D97069
14 changed files with 110 additions and 49 deletions

View File

@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace Todo.Api.Hubs.Models;
public record TodoResponse
{
[JsonPropertyName("id")]
public string Id { get; init; }
[JsonPropertyName("title")]
public string Title { get; init; }
[JsonPropertyName("status")]
public bool Status { get; init; }
}

View File

@ -1,5 +1,6 @@
using System.Text.Json; using System.Text.Json;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using Todo.Api.Hubs.Models;
using Todo.Core.Interfaces.Persistence; using Todo.Core.Interfaces.Persistence;
namespace Todo.Api.Hubs namespace Todo.Api.Hubs
@ -13,18 +14,40 @@ namespace Todo.Api.Hubs
_todoRepository = todoRepository; _todoRepository = todoRepository;
} }
public async Task GetInboxTodos() public async Task CreateTodo(string todoTitle)
{ {
await Clients.Caller.SendAsync("InboxTodos", "some data"); var _ = await _todoRepository.CreateTodoAsync(todoTitle);
var todos = await _todoRepository.GetTodosAsync();
var serializedTodos =
JsonSerializer.Serialize(todos
.Select(t => new TodoResponse { Id = t.Id, Title = t.Title })
.ToList());
await Clients.Caller.SendAsync("todos", serializedTodos);
} }
public async Task CreateTodo(string createTodoRequest) public async Task UpdateTodo(string todoId, bool todoStatus)
{ {
var todo = JsonSerializer.Deserialize<Core.Entities.Todo>(createTodoRequest); await _todoRepository.UpdateTodoStatus(todoId, todoStatus);
if (todo is null)
throw new InvalidOperationException("Failed to create todo because of invalid request");
await _todoRepository.CreateTodoAsync(todo.Title); var todos = await _todoRepository.GetTodosAsync();
var serializedTodos =
JsonSerializer.Serialize(todos
.Select(t => new TodoResponse { Id = t.Id, Title = t.Title, Status = t.Status })
.ToList());
await Clients.Caller.SendAsync("todos", serializedTodos);
}
public async Task GetTodos()
{
var todos = await _todoRepository.GetTodosAsync();
var serializedTodos = JsonSerializer.Serialize(todos
.Select(t => new TodoResponse { Id = t.Id, Title = t.Title, Status = t.Status})
.ToList());
await Clients.Caller.SendAsync("todos", serializedTodos);
} }
} }
} }

View File

@ -14,7 +14,7 @@
"dotnetRunMessages": "true", "dotnetRunMessages": "true",
"launchBrowser": false, "launchBrowser": false,
"launchUrl": "swagger", "launchUrl": "swagger",
"applicationUrl": "https://localhost:5001;http://localhost:5000", "applicationUrl": "http://localhost:5000",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }

View File

@ -53,8 +53,6 @@ namespace Todo.Api
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Todo.Api v1")); app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Todo.Api v1"));
} }
app.UseHttpsRedirection();
app.UseRouting(); app.UseRouting();
app.UseCors(); app.UseCors();

View File

@ -4,4 +4,5 @@ public record Todo
{ {
public string Id { get; init; } public string Id { get; init; }
public string Title { get; init; } public string Title { get; init; }
public bool Status { get; init; }
} }

View File

@ -4,4 +4,5 @@ public interface ITodoRepository
{ {
Task<Entities.Todo> CreateTodoAsync(string title); Task<Entities.Todo> CreateTodoAsync(string title);
Task<IEnumerable<Entities.Todo>> GetTodosAsync(); Task<IEnumerable<Entities.Todo>> GetTodosAsync();
Task UpdateTodoStatus(string todoId, bool todoStatus);
} }

View File

@ -10,4 +10,5 @@ public record MongoTodo
public string Id { get; init; } public string Id { get; init; }
[BsonRequired] public string Title { get; init; } [BsonRequired] public string Title { get; init; }
[BsonRequired] public bool Status { get; set; }
} }

View File

@ -29,6 +29,13 @@ public class TodoRepository : ITodoRepository
return todos return todos
.ToEnumerable() .ToEnumerable()
.Select(t => .Select(t =>
new Core.Entities.Todo() { Id = t.Id, Title = t.Title }); new Core.Entities.Todo() { Id = t.Id, Title = t.Title, Status = t.Status});
}
public async Task UpdateTodoStatus(string todoId, bool todoStatus)
{
await _todosCollection
.UpdateOneAsync(t => t.Id == todoId,
Builders<MongoTodo>.Update.Set(t => t.Status, todoStatus));
} }
} }

View File

@ -2,8 +2,9 @@ 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/collapsed/addTodoForm";
import { CollapsedState } from "@src/components/todos/collapsed/collapsedState"; import { CollapsedState } from "@src/components/todos/collapsed/collapsedState";
import {HubConnection} from "@microsoft/signalr";
export function AddTodo() { export function AddTodo(props: {conn: HubConnection}) {
const [collapsed, setCollapsed] = useState<CollapsedState>( const [collapsed, setCollapsed] = useState<CollapsedState>(
CollapsedState.collapsed CollapsedState.collapsed
); );
@ -18,7 +19,9 @@ export function AddTodo() {
return ( return (
<AddTodoForm <AddTodoForm
onAdd={(todoName) => {}} onAdd={(todoName) => {
props.conn.invoke("CreateTodo", todoName).catch(console.error)
}}
onClose={() => setCollapsed(CollapsedState.collapsed)} onClose={() => setCollapsed(CollapsedState.collapsed)}
/> />
); );

View File

@ -11,7 +11,7 @@ export const TodoItem: FC<TodoItemProps> = (props) => (
<div className="py-3 border-b border-gray-300 dark:border-gray-700"> <div className="py-3 border-b border-gray-300 dark:border-gray-700">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<TodoCheckmark {...props} /> <TodoCheckmark {...props} />
<span className="pb-1">{props.todo.name}</span> <span className="pb-1">{props.todo.title}</span>
</div> </div>
</div> </div>
); );

View File

@ -1,8 +1,9 @@
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 { HubConnection } from "@microsoft/signalr";
export const TodoList = (props: { todos: Todo[] }) => ( export const TodoList = (props: { todos: Todo[]; conn: HubConnection }) => (
<> <>
<ul id="inbox"> <ul id="inbox">
{props.todos.map((t, i) => ( {props.todos.map((t, i) => (
@ -10,12 +11,14 @@ export const TodoList = (props: { todos: Todo[] }) => (
<TodoItem <TodoItem
todo={t} todo={t}
updateTodo={(todo) => { updateTodo={(todo) => {
console.log(todo); props.conn
.invoke("UpdateTodo", todo.id, todo.status)
.catch(console.error);
}} }}
/> />
</li> </li>
))} ))}
</ul> </ul>
<AddTodo /> <AddTodo conn={props.conn} />
</> </>
); );

View File

@ -7,6 +7,7 @@ export const StatusState: { done: Done; notDone: NotDone } = {
}; };
export interface Todo { export interface Todo {
name: string; id: string;
title: string;
status: StatusState; status: StatusState;
} }

View File

@ -1,27 +1,34 @@
import { useEffect, useMemo } from "react"; import { useEffect, useState } from "react";
import { getInboxTodos } from "@src/core/actions/todos";
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 * as signalR from "@microsoft/signalr"; import * as signalR from "@microsoft/signalr";
import { HubConnection } from "@microsoft/signalr";
import { Todo } from "@src/core/entities/todo";
const HomePage = () => { const HomePage = () => {
const inboxTodos = useMemo(() => getInboxTodos(), []); const [conn, setConn] = useState<HubConnection>();
const [todos, setTodos] = useState<Todo[]>([]);
useEffect(() => { useEffect(() => {
const connection = new signalR.HubConnectionBuilder() const connection = new signalR.HubConnectionBuilder()
.withUrl("https://localhost:5001/hubs/todo") .withUrl("http://localhost:5000/hubs/todo")
.build(); .build();
connection.on("InboxTodos", (todos) => console.log(todos)); connection.on("todos", (todos) => {
const parsedTodos = JSON.parse(todos);
setTodos(parsedTodos);
});
connection.start().then(() => connection.invoke("getInboxTodos")); connection.start().then(() => connection.invoke("GetTodos"));
setConn(connection);
}, []); }, []);
return ( return (
<main className="py-8 px-14 space-y-6"> <main className="py-8 px-14 space-y-6">
<section className="space-y-2"> <section className="space-y-2">
<PageHeading title="Inbox" /> <PageHeading title="Inbox" />
<TodoList todos={inboxTodos} /> <TodoList todos={todos} conn={conn} />
</section> </section>
</main> </main>
); );

View File

@ -57,7 +57,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.10.2", "@babel/runtime@^7.16.3":
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==
@ -458,7 +458,7 @@ aria-query@^4.2.2:
"@babel/runtime" "^7.10.2" "@babel/runtime" "^7.10.2"
"@babel/runtime-corejs3" "^7.10.2" "@babel/runtime-corejs3" "^7.10.2"
array-includes@^3.1.1, array-includes@^3.1.3, array-includes@^3.1.4: array-includes@^3.1.3, array-includes@^3.1.4:
version "3.1.4" version "3.1.4"
resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.4.tgz#f5b493162c760f3539631f005ba2bb46acb45ba9" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.4.tgz#f5b493162c760f3539631f005ba2bb46acb45ba9"
integrity sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw== integrity sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==
@ -539,7 +539,7 @@ available-typed-arrays@^1.0.5:
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
axe-core@^4.0.2: axe-core@^4.3.5:
version "4.3.5" version "4.3.5"
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.3.5.tgz#78d6911ba317a8262bfee292aeafcc1e04b49cc5" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.3.5.tgz#78d6911ba317a8262bfee292aeafcc1e04b49cc5"
integrity sha512-WKTW1+xAzhMS5dJsxWkliixlO/PqC4VhmO9T4juNYcaTg9jzWiJsou6m5pxWYGfigWbwzJWeFY6z47a+4neRXA== integrity sha512-WKTW1+xAzhMS5dJsxWkliixlO/PqC4VhmO9T4juNYcaTg9jzWiJsou6m5pxWYGfigWbwzJWeFY6z47a+4neRXA==
@ -739,9 +739,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0" lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001202, caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001228, caniuse-lite@^1.0.30001272, caniuse-lite@^1.0.30001274: caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001202, caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001228, caniuse-lite@^1.0.30001272, caniuse-lite@^1.0.30001274:
version "1.0.30001279" version "1.0.30001280"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001279.tgz#eb06818da481ef5096a3b3760f43e5382ed6b0ce" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001280.tgz#066a506046ba4be34cde5f74a08db7a396718fb7"
integrity sha512-VfEHpzHEXj6/CxggTwSFoZBBYGQfQv9Cf42KPlO79sWXCD1QNKWKsKzFeWL7QpZHJQYAvocqV6Rty1yJMkqWLQ== integrity sha512-kFXwYvHe5rix25uwueBxC569o53J6TpnGu0BEEn+6Lhl2vsnAumRFWEBhDft1fwyo6m1r4i+RqA4+163FpeFcA==
chalk@2.4.2, chalk@^2.0.0: chalk@2.4.2, chalk@^2.0.0:
version "2.4.2" version "2.4.2"
@ -1098,7 +1098,7 @@ csstype@^3.0.2:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.9.tgz#6410af31b26bd0520933d02cbc64fce9ce3fbf0b" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.9.tgz#6410af31b26bd0520933d02cbc64fce9ce3fbf0b"
integrity sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw== integrity sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==
damerau-levenshtein@^1.0.6: damerau-levenshtein@^1.0.7:
version "1.0.7" version "1.0.7"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.7.tgz#64368003512a1a6992593741a09a9d31a836f55d" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.7.tgz#64368003512a1a6992593741a09a9d31a836f55d"
integrity sha512-VvdQIPGdWP0SqFXghj79Wf/5LArmreyMsGLa6FG6iC4t3j7j5s71TrwWmT/4akbDQIqjfACkLZmjXhA7g2oUZw== integrity sha512-VvdQIPGdWP0SqFXghj79Wf/5LArmreyMsGLa6FG6iC4t3j7j5s71TrwWmT/4akbDQIqjfACkLZmjXhA7g2oUZw==
@ -1244,9 +1244,9 @@ domutils@^2.6.0:
domhandler "^4.2.0" domhandler "^4.2.0"
electron-to-chromium@^1.3.723, electron-to-chromium@^1.3.886: electron-to-chromium@^1.3.723, electron-to-chromium@^1.3.886:
version "1.3.893" version "1.3.896"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.893.tgz#9d804c68953b05ede35409dba0d73dd54c077b4d" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.896.tgz#4a94efe4870b1687eafd5c378198a49da06e8a1b"
integrity sha512-ChtwF7qB03INq1SyMpue08wc6cve+ktj2UC/Y7se9vB+JryfzziJeYwsgb8jLaCA5GMkHCdn5M62PfSMWhifZg== integrity sha512-NcGkBVXePiuUrPLV8IxP43n1EOtdg+dudVjrfVEUd/bOqpQUFZ2diL5PPYzbgEhZFEltdXV3AcyKwGnEQ5lhMA==
elliptic@^6.5.3: elliptic@^6.5.3:
version "6.5.4" version "6.5.4"
@ -1266,7 +1266,7 @@ emoji-regex@^8.0.0:
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
emoji-regex@^9.0.0: emoji-regex@^9.2.2:
version "9.2.2" version "9.2.2"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
@ -1420,21 +1420,22 @@ eslint-plugin-import@^2.22.1:
tsconfig-paths "^3.11.0" tsconfig-paths "^3.11.0"
eslint-plugin-jsx-a11y@^6.4.1: eslint-plugin-jsx-a11y@^6.4.1:
version "6.4.1" version "6.5.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.4.1.tgz#a2d84caa49756942f42f1ffab9002436391718fd" resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.5.1.tgz#cdbf2df901040ca140b6ec14715c988889c2a6d8"
integrity sha512-0rGPJBbwHoGNPU73/QCLP/vveMlM1b1Z9PponxO87jfr6tuH5ligXbDT6nHSSzBC8ovX2Z+BQu7Bk5D/Xgq9zg== integrity sha512-sVCFKX9fllURnXT2JwLN5Qgo24Ug5NF6dxhkmxsMEUZhXRcGg+X3e1JbJ84YePQKBl5E0ZjAH5Q4rkdcGY99+g==
dependencies: dependencies:
"@babel/runtime" "^7.11.2" "@babel/runtime" "^7.16.3"
aria-query "^4.2.2" aria-query "^4.2.2"
array-includes "^3.1.1" array-includes "^3.1.4"
ast-types-flow "^0.0.7" ast-types-flow "^0.0.7"
axe-core "^4.0.2" axe-core "^4.3.5"
axobject-query "^2.2.0" axobject-query "^2.2.0"
damerau-levenshtein "^1.0.6" damerau-levenshtein "^1.0.7"
emoji-regex "^9.0.0" emoji-regex "^9.2.2"
has "^1.0.3" has "^1.0.3"
jsx-ast-utils "^3.1.0" jsx-ast-utils "^3.2.1"
language-tags "^1.0.5" language-tags "^1.0.5"
minimatch "^3.0.4"
eslint-plugin-react-hooks@^4.2.0: eslint-plugin-react-hooks@^4.2.0:
version "4.3.0" version "4.3.0"
@ -1702,9 +1703,9 @@ foreach@^2.0.5:
integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k= integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k=
fraction.js@^4.1.1: fraction.js@^4.1.1:
version "4.1.1" version "4.1.2"
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.1.1.tgz#ac4e520473dae67012d618aab91eda09bcb400ff" resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.1.2.tgz#13e420a92422b6cf244dff8690ed89401029fbe8"
integrity sha512-MHOhvvxHTfRFpF1geTK9czMIZ6xclsEor2wkIGYYq+PxcQqT7vStJqjhe6S1TenZrMZzo+wlqOufBDVepUEgPg== integrity sha512-o2RiJQ6DZaR/5+Si0qJUIy637QMRudSi9kU/FFzx9EZazrIdnBgpU+3sEWCxAVhH2RtxW2Oz+T4p2o8uOPVcgA==
fs-extra@^10.0.0: fs-extra@^10.0.0:
version "10.0.0" version "10.0.0"
@ -2250,7 +2251,7 @@ jsonfile@^6.0.1:
optionalDependencies: optionalDependencies:
graceful-fs "^4.1.6" graceful-fs "^4.1.6"
"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.1.0: "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.2.1:
version "3.2.1" version "3.2.1"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz#720b97bfe7d901b927d87c3773637ae8ea48781b" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz#720b97bfe7d901b927d87c3773637ae8ea48781b"
integrity sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA== integrity sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA==