Add create bookmark

This commit is contained in:
Neil Brommer 2021-12-13 16:27:13 -08:00
parent 64b893b778
commit 55625b1be4
19 changed files with 382 additions and 13 deletions

View file

@ -1,4 +1,4 @@
<li>
<li class="bookmark">
@if (!String.IsNullOrEmpty(this.Model.Notes))
{
<details>

View file

@ -2,6 +2,7 @@
@using Fluxor
@using Start.Client.Store.State
@using Start.Client.Store.Features.DeleteGroup
@using Start.Client.Store.Features.CreateBookmark
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
@ -33,7 +34,7 @@
</div>
<p class="empty-title h6">No Bookmarks</p>
<div class="empty-action">
<button type="button" class="btn btn-primary">
<button type="button" class="btn btn-primary" @onclick="this.OnCreateBookmarkClicked">
<i class="icon icon-plus"></i> Create Bookmark
</button>
</div>
@ -89,6 +90,7 @@
protected void OnCreateBookmarkClicked()
{
// Placeholder
dispatch.Dispatch(new ShowCreateBookmarkFormAction(this.Group.BookmarkGroupId,
this.Group.Title));
}
}

View file

@ -0,0 +1,91 @@
@using Start.Client.Store.Features.CreateBookmark
@using Fluxor
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
@inject IActionSubscriber actionSubscriber
@inject IDispatcher dispatch
@inject IState<CreateBookmarkState> state
<Dialog Title="Create Bookmark" Active="this.state.Value.ShowCreateBookmarkForm" OnClose="this.OnDialogClose">
<EditForm Model="this.Model" OnValidSubmit="this.OnSubmit">
<DataAnnotationsValidator />
@if (this.state.Value.CreateBookmarkErrorMessage != null)
{
<Alert Type="Alert.AlertType.Error">
@this.state.Value.CreateBookmarkErrorMessage
</Alert>
}
<ValidationSummary />
<div class="form-group">
<div class="container">
<div class="columns">
<div class="column col-12">
<label for="createBookmarkTitle">Title</label>
<InputText id="createBookmarkTitle" name="createBookmarkTitle"
class="form-input" @bind-Value="this.Model.Title" />
</div>
</div>
<div class="columns">
<div class="column col-12">
<label for="createBookmarkUrl">URL</label>
<input type="url" name="createBookmarkUrl" class="form-input"
@bind-value="this.Model.Url" />
</div>
</div>
<div class="columns">
<div class="column col-12">
<label for="createBookmarkNotes">Notes</label>
<InputTextArea name="createBookmarkNotes" class="form-input"
@bind-Value="this.Model.Notes" />
</div>
</div>
</div>
<div class="container">
<div class="columns">
<div class="column col-12 text-right">
@if (this.state.Value.IsLoadingCreateBookmark)
{
<button type="submit" disabled class="btn btn-primary loading">
<i class="icon icon-plus"></i> Create
</button>
}
else
{
<button type="submit" class="btn btn-primary">
<i class="icon icon-plus"></i> Create
</button>
}
</div>
</div>
</div>
</div>
</EditForm>
</Dialog>
@code {
protected BookmarkDto Model { get; set; } = new BookmarkDto("", "", null, 0);
protected override void OnInitialized()
{
base.OnInitialized();
this.Model = new BookmarkDto("", "", null, this.state.Value.GroupId);
actionSubscriber.SubscribeToAction<ShowCreateBookmarkFormAction>(this,
a => this.Model.BookmarkGroupId = a.GroupId);
}
protected void OnSubmit()
{
dispatch.Dispatch(new SubmitCreateBookmarkAction(this.Model));
}
protected void OnDialogClose()
{
dispatch.Dispatch(new HideCreateBookmarkFormAction());
}
}

View file

@ -80,6 +80,8 @@ else
<CreateGroup />
<DeleteGroup />
<CreateBookmark />
</Sidebar>
}

View file

@ -191,6 +191,32 @@
ul.bookmarks {
list-style: none;
margin: 0;
margin-block: 0;
.bookmark {
margin-top: 0;
a {
display: inline-block;
width: 100%;
padding: 0.5em 0.83em;
&:hover {
text-decoration: none;
background-color: $bg-color-dark;
}
}
&:last-child {
a {
border-radius: 0 0 0.4em 0.4em;
}
}
&:not(:last-child) {
border-bottom: solid 1px $border-color;
}
}
li.noBookmarksItem {
margin-top: 0;

View file

@ -74,6 +74,10 @@ $dark-border-color: darken($dark-color, 10%);
// Cards
.card {
border-color: $dark-border-color;
.card-body {
background-color: $dark-bg-color-dark;
}
}
@ -124,4 +128,20 @@ $dark-border-color: darken($dark-color, 10%);
#sidebar .accountActions {
border-top-color: $dark-border-color;
}
ul.bookmarks {
.bookmark {
&:not(:last-child) {
border-bottom: solid 1px $dark-border-color;
}
a, a:visited {
color: $dark-primary-color;
}
a:hover {
background-color: $dark-bg-color;
}
}
}
}

View file

@ -130,6 +130,7 @@
<None Remove="Sass\" />
<None Remove="Sass\Spectre\" />
<None Remove="Store\Features\DeleteGroup\" />
<None Remove="Store\Features\CreateBookmark\" />
</ItemGroup>
<ItemGroup>
<Content Remove="wwwroot\css\Spectre\" />
@ -147,5 +148,6 @@
<Folder Include="Sass\" />
<Folder Include="Sass\Spectre\" />
<Folder Include="Store\Features\DeleteGroup\" />
<Folder Include="Store\Features\CreateBookmark\" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,35 @@
using Start.Shared;
namespace Start.Client.Store.Features.CreateBookmark {
public class ShowCreateBookmarkFormAction {
public int GroupId { get; init; }
public string GroupTitle { get; init; }
public ShowCreateBookmarkFormAction(int groupId, string groupTitle) {
this.GroupId = groupId;
this.GroupTitle = groupTitle;
}
}
public class HideCreateBookmarkFormAction { }
public class FetchCreateBookmarkAction { }
public class ReceivedCreateBookmarkAction { }
public class ErrorFetchingCreateBookmarkAction {
public string ErrorMessage { get; init; }
public ErrorFetchingCreateBookmarkAction(string errorMessage) {
this.ErrorMessage = errorMessage;
}
}
public class SubmitCreateBookmarkAction {
public BookmarkDto NewBookmark { get; init; }
public SubmitCreateBookmarkAction(BookmarkDto newBookmark) {
this.NewBookmark = newBookmark;
}
}
}

View file

@ -0,0 +1,45 @@
using System.Threading.Tasks;
using Fluxor;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Start.Shared.Api;
using Start.Client.Store.Features.CurrentContainer;
namespace Start.Client.Store.Features.CreateBookmark {
public class CreateBookmarkEffects {
public IBookmarksApi BookmarksApi { get; init; }
public CreateBookmarkEffects(IBookmarksApi bookmarksApi) {
this.BookmarksApi = bookmarksApi;
}
[EffectMethod]
public async Task SubmitCreateBookmark(SubmitCreateBookmarkAction action,
IDispatcher dispatch) {
dispatch.Dispatch(new FetchCreateBookmarkAction());
try {
Refit.ApiResponse<Start.Shared.BookmarkDto?>? apiResponse = await this.BookmarksApi
.CreateBookmark(action.NewBookmark.Title, action.NewBookmark.Url,
action.NewBookmark.Notes, action.NewBookmark.BookmarkGroupId);
if (!apiResponse.IsSuccessStatusCode) {
dispatch.Dispatch(new ErrorFetchingCreateBookmarkAction(
"Error creating bookmark group: Status code " + apiResponse.StatusCode.ToString()));
return;
}
if (apiResponse.Content == null) {
dispatch.Dispatch(new ErrorFetchingCreateBookmarkAction(
"Error creating bookmark group"));
return;
}
dispatch.Dispatch(new AddBookmarkAction(apiResponse.Content));
dispatch.Dispatch(new ReceivedCreateBookmarkAction());
dispatch.Dispatch(new HideCreateBookmarkFormAction());
} catch (AccessTokenNotAvailableException e) {
e.Redirect();
}
}
}
}

View file

@ -0,0 +1,11 @@
using Fluxor;
namespace Start.Client.Store.Features.CreateBookmark {
public class CreateBookmarkFeature : Feature<CreateBookmarkState> {
public override string GetName() => "Create Bookmark";
protected override CreateBookmarkState GetInitialState() {
return new CreateBookmarkState();
}
}
}

View file

@ -0,0 +1,41 @@
using Fluxor;
namespace Start.Client.Store.Features.CreateBookmark {
public static class CreateBookmarkReducers {
[ReducerMethod]
public static CreateBookmarkState ShowCreateBookmarkForm(CreateBookmarkState state,
ShowCreateBookmarkFormAction action) {
return state with {
ShowCreateBookmarkForm = true,
GroupId = action.GroupId,
GroupTitle = action.GroupTitle,
IsLoadingCreateBookmark = false,
CreateBookmarkErrorMessage = null
};
}
[ReducerMethod(typeof(HideCreateBookmarkFormAction))]
public static CreateBookmarkState HideCreateBookmarkForm(CreateBookmarkState state) {
return state with {
ShowCreateBookmarkForm = false
};
}
[ReducerMethod(typeof(FetchCreateBookmarkAction))]
public static CreateBookmarkState FetchCreateBookmark(CreateBookmarkState state) {
return state with {
IsLoadingCreateBookmark = true,
CreateBookmarkErrorMessage = null
};
}
[ReducerMethod]
public static CreateBookmarkState ErrorFetchingCreateBookmark(CreateBookmarkState state,
ErrorFetchingCreateBookmarkAction action) {
return state with {
CreateBookmarkErrorMessage = action.ErrorMessage,
IsLoadingCreateBookmark = false
};
}
}
}

View file

@ -0,0 +1,15 @@
using Start.Client.Store.State;
namespace Start.Client.Store.Features.CreateBookmark {
public record CreateBookmarkState : RootState {
public bool ShowCreateBookmarkForm { get; init; }
public int GroupId { get; init; }
public string GroupTitle { get; init; }
public bool IsLoadingCreateBookmark { get; init; }
public string? CreateBookmarkErrorMessage { get; init; }
public CreateBookmarkState() {
this.GroupTitle = "";
}
}
}

View file

@ -33,7 +33,13 @@ namespace Start.Client.Store.Features.CreateGroup {
return;
}
dispatch.Dispatch(new AddBookmarkGroupAction(action.NewGroup));
if (apiResponse.Content == null) {
dispatch.Dispatch(new ErrorFetchingCreateGroupAction(
"Error creating bookmark group"));
return;
}
dispatch.Dispatch(new AddBookmarkGroupAction(apiResponse.Content));
dispatch.Dispatch(new RecievedCreateGroupAction());
dispatch.Dispatch(new HideCreateGroupFormAction());
} catch (AccessTokenNotAvailableException e) {

View file

@ -44,4 +44,20 @@ namespace Start.Client.Store.Features.CurrentContainer {
this.BookmarkGroupId = bookmarkGroupId;
}
}
public class AddBookmarkAction {
public BookmarkDto Bookmark { get; init; }
public AddBookmarkAction(BookmarkDto bookmark) {
this.Bookmark = bookmark;
}
}
public class RemoveBookmarkAction {
public int BookmarkId { get; init; }
public RemoveBookmarkAction(int bookmarkId) {
this.BookmarkId = bookmarkId;
}
}
}

View file

@ -78,5 +78,56 @@ namespace Start.Client.Store.Features.CurrentContainer {
}
};
}
[ReducerMethod]
public static RootState AddBookmark(RootState state, AddBookmarkAction action) {
BookmarkContainerDto? container = state.CurrentContainerState.Container;
if (container == null)
return state;
List<BookmarkGroupDto>? groups = container.BookmarkGroups
?.Select(bg => {
if (bg.BookmarkGroupId == action.Bookmark.BookmarkGroupId) {
return new BookmarkGroupDto(bg.BookmarkGroupId, bg.Title, bg.Color,
bg.BookmarkContainerId,
bg.Bookmarks?
.Concat(new List<BookmarkDto> { action.Bookmark })
.ToList());
}
return bg;
})
.ToList();
return state with {
CurrentContainerState = state.CurrentContainerState with {
Container = new BookmarkContainerDto(container.BookmarkContainerId,
container.Title, groups)
}
};
}
[ReducerMethod]
public static RootState RemoveBookmark(RootState state, RemoveBookmarkAction action) {
BookmarkContainerDto? container = state.CurrentContainerState.Container;
if (container == null)
return state;
List<BookmarkGroupDto>? groups = container.BookmarkGroups
?.Select(bg => new BookmarkGroupDto(bg.BookmarkGroupId, bg.Title, bg.Color,
bg.BookmarkContainerId, bg.Bookmarks
?.Where(b => b.BookmarkId != action.BookmarkId)
.ToList()))
.ToList();
return state with {
CurrentContainerState = state.CurrentContainerState with {
Container = new BookmarkContainerDto(container.BookmarkContainerId,
container.Title, groups)
}
};
}
}
}

View file

@ -1,6 +1,7 @@
using System;
using Start.Client.Store.State;
namespace Start.Client.Store.Features.DeleteGroup {
public record DeleteGroupState {
public record DeleteGroupState : RootState {
public bool ShowDeleteGroupForm { get; init; }
public int BookmarkGroupIdToDelete { get; init; }
public string BookmarkGroupTitleToDelete { get; init; }

View file

@ -6,7 +6,7 @@ namespace Start.Server.Extensions {
public static class BookmarkMaps {
public static BookmarkDto MapToDto(this Bookmark bookmark) {
return new BookmarkDto(bookmark.BookmarkId, bookmark.Title, bookmark.Url,
bookmark.Notes);
bookmark.Notes, bookmark.BookmarkGroupId);
}
public static BookmarkGroupDto MapToDto(this BookmarkGroup bookmarkGroup) {

View file

@ -4,7 +4,7 @@ using System.Net.Http;
namespace Start.Shared.Api {
public interface IBookmarksApi {
[Get("{bookmarkId}")]
[Get("/{bookmarkId}")]
Task<ApiResponse<BookmarkDto?>> GetBookmark(int bookmarkId);
[Post("/Create")]

View file

@ -1,5 +1,5 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
namespace Start.Shared {
public class BookmarkDto {
@ -11,14 +11,19 @@ namespace Start.Shared {
[StringLength(5000)]
public string? Notes { get; set; }
public BookmarkDto(string title, string url, string? notes) {
public int BookmarkGroupId { get; set; }
public BookmarkDto(string title, string url, string? notes, int bookmarkGroupId) {
this.Title = title;
this.Url = url;
this.Notes = notes;
this.BookmarkGroupId = bookmarkGroupId;
}
public BookmarkDto(int bookmarkId, string title, string url, string? notes)
: this(title, url, notes) {
[JsonConstructor]
public BookmarkDto(int bookmarkId, string title, string url, string? notes,
int bookmarkGroupId)
: this(title, url, notes, bookmarkGroupId) {
this.BookmarkId = bookmarkId;
}
}