Add Spectre, add bookmark container creation, save selected container to localStorage

This commit is contained in:
Neil Brommer 2021-11-20 22:47:10 -08:00
parent c5403ca206
commit 3eb2b2ae98
21 changed files with 6014 additions and 57 deletions

View file

@ -11,7 +11,8 @@ trim_trailing_whitespace = true
[*.razor] [*.razor]
indent_style = space indent_style = space
indent_width = 4 indent_size = 4
tab_width = 4
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.cs] [*.cs]

View file

@ -0,0 +1,31 @@
<div class="toast @AlertTypeToClass(this.Type)">
@ChildContent
</div>
@code {
[Parameter]
public AlertType Type { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; } = null!;
private string AlertTypeToClass(AlertType type)
{
switch (type)
{
case AlertType.Error: return "toast-error";
case AlertType.Warning: return "toast-warning";
case AlertType.Success: return "toast-success";
case AlertType.Info: return "toast-primary";
default: return "";
}
}
public enum AlertType
{
Error,
Warning,
Success,
Info,
None
}
}

View file

@ -0,0 +1,26 @@
@using Start.Shared
<h2 class="bookmarkGroupTitle" style="background-color: #@this.Group.Color">
@this.Group.Title
</h2>
<ul class="bookmarkGroupList">
@if (this.Group.Bookmarks == null || !this.Group.Bookmarks.Any())
{
<li class="bookmarkListItem noBookmarksItem"><i>No Bookmarks</i></li>
}
else
{
foreach (BookmarkDto bookmark in this.Group.Bookmarks!)
{
<li class="bookmarkListItem">
<a href="@bookmark.Url" class="bookmarkLink">@bookmark.Title</a>
</li>
}
}
</ul>
@code
{
[Parameter]
public BookmarkGroupDto Group { get; set; } = null!; // [Required] is a .net 6 feature
}

View file

@ -0,0 +1,79 @@
@using Start.Shared
@using System.IO
@inject HttpClient Http
<Dialog Title="Create Container" Active="this.IsOpen" OnClose="this.OnDialogClose">
<EditForm Model="this.model" OnValidSubmit="this.OnSubmit">
@if (displayError)
{
<Alert Type="Alert.AlertType.Error">
There was an error creating the container
</Alert>
}
<div class="form-group">
<div class="container">
<div class="columns">
<div class="column col-12">
<div>
<label for="createBookmarkContainerTitle" class="form-label">Title</label>
<InputText id="createBookmarkContainerTitle" name="createBookmarkContainerTitle"
class="form-input" @bind-Value="this.model.Title" />
</div>
</div>
</div>
</div>
<div class="container">
<div class="columns">
<div class="column col-12 text-right">
<div>
<button type="submit" class="btn btn-primary">
<i class="icon icon-plus"></i> Create
</button>
</div>
</div>
</div>
</div>
</div>
</EditForm>
</Dialog>
@code {
[Parameter]
public EventCallback<BookmarkContainerDto> OnCreated { get; set; }
[Parameter]
public bool IsOpen { get; set; }
[Parameter]
public EventCallback OnClose { get; set; }
private BookmarkContainerDto model = new("");
private bool displayError = false;
protected async void OnSubmit()
{
HttpResponseMessage response = await Http
.PostAsJsonAsync("Bookmarks/CreateBookmarkContainer", model.Title);
Stream stream = response.RequestMessage!.Content!.ReadAsStream();
StreamReader reader = new StreamReader(stream);
Console.WriteLine(reader.ReadToEnd());
BookmarkContainerDto? container = await response
!.Content
!.ReadFromJsonAsync<BookmarkContainerDto>();
if (container == null)
{
this.displayError = true;
}
else
{
await this.OnCreated.InvokeAsync(container);
}
}
protected async void OnDialogClose()
{
this.IsOpen = false;
await this.OnClose.InvokeAsync();
}
}

View file

@ -0,0 +1,31 @@
<div class="modal @(this.Active ? "active" : "")">
<a class="modal-overlay" @onclick="this.OnDialogClose" aria-label="Close"></a>
<div class="modal-container">
<div class="modal-header">
<a class="btn btn-clear float-right" @onclick="this.OnClose" aria-label="Close"></a>
<div class="modal-title h5">@this.Title</div>
</div>
<div class="modal-body">
<div class="content">
@this.ChildContent
</div>
</div>
</div>
</div>
@code {
[Parameter]
public string Title { get; set; } = null!;
[Parameter]
public RenderFragment ChildContent { get; set; } = null!;
[Parameter]
public bool Active { get; set; }
[Parameter]
public EventCallback OnClose { get; set; }
public void OnDialogClose()
{
this.Active = false;
this.OnClose.InvokeAsync();
}
}

View file

@ -0,0 +1,175 @@
@page "/Start"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using Start.Client.Components
@using Start.Shared
@attribute [Authorize]
@inject HttpClient Http
@inject Blazored.LocalStorage.ILocalStorageService localStorage
@if (bookmarkContainers == null)
{
<div class="empty">
<div class="empty-icon">
<div class="loading loading-lg"></div>
</div>
<p class="empty-title h5">Loading Containers</p>
</div>
}
else
{
<ul class="containerList tab">
@foreach (BookmarkContainerDto container in this.bookmarkContainers)
{
string itemClasses = "tab-item";
if (container.BookmarkContainerId == this.selectedBookmarkContainer?.BookmarkContainerId)
itemClasses += " active";
<li class="@itemClasses">
<a @onclick="() => OnContainerSelected(container.BookmarkContainerId)">
@container.Title
</a>
<button class="btn btn-clear"></button>
</li>
}
<li class="tab-item tab-action">
<button @onclick="OnCreateContainerClicked" class="btn btn-link"
title="Create New Container" aria-label="Create New Container">
+
</button>
</li>
</ul>
<div class="activeBookmarkContainer">
@if (this.selectedBookmarkContainer == null)
{
<div class="empty">
<div class="empty-icon">
<div class="loading loading-icon"></div>
</div>
<p class="empty-title h5">Loading Bookmarks</p>
</div>
<p class="text-center">Loading Bookmarks</p>
}
else if (!this.selectedBookmarkContainer.BookmarkGroups?.Any() ?? true)
{
<div class="empty">
<div class="empty-icon">
<i class="icon icon-3x icon-bookmark"></i>
</div>
<p class="empty-title h5">No Bookmark Groups</p>
</div>
}
else
{
@foreach (BookmarkGroupDto group in this.selectedBookmarkContainer.BookmarkGroups!)
{
<BookmarkGroup Group="group" />
}
}
</div>
<CreateContainer IsOpen="showCreateContainerForm" OnClose="this.OnCloseCreateContainer"
OnCreated="this.OnContainerCreated" />
}
@code
{
private IList<BookmarkContainerDto>? bookmarkContainers;
private BookmarkContainerDto? selectedBookmarkContainer;
private bool showCreateContainerForm = false;
private bool showCreateGroupForm = false;
private bool showCreateBookmarkForm = false;
protected override async Task OnInitializedAsync()
{
await LoadContainers();
}
protected async Task LoadContainers()
{
try {
this.bookmarkContainers = await Http
.GetFromJsonAsync<IList<BookmarkContainerDto>>(
"Bookmarks/GetAllBookmarkContainers");
if (this.bookmarkContainers == null || !this.bookmarkContainers.Any()) {
HttpResponseMessage response = await Http
.PostAsJsonAsync("Bookmarks/CreateBookmarkContainer", "Default");
BookmarkContainerDto? container = await response
.RequestMessage
!.Content
!.ReadFromJsonAsync<BookmarkContainerDto?>();
}
await this.OnContainerSelected(await this.GetSelectedContainerId());
}
catch (AccessTokenNotAvailableException e) {
e.Redirect();
}
}
protected async Task OnContainerSelected(int bookmarkContainerId)
{
try
{
BookmarkContainerDto? bookmarkContainer = await Http
.GetFromJsonAsync<BookmarkContainerDto?>(
$"Bookmarks/GetBookmarkContainer/{bookmarkContainerId}");
await this.SetSelectedContainer(bookmarkContainerId);
this.selectedBookmarkContainer = bookmarkContainer;
}
catch (AccessTokenNotAvailableException e)
{
e.Redirect();
}
}
protected async Task OnDeleteContainerClicked()
{
}
protected void OnCreateContainerClicked()
{
this.showCreateContainerForm = true;
}
protected void OnCloseCreateContainer()
{
this.showCreateContainerForm = false;
}
protected async Task OnContainerCreated(BookmarkContainerDto newContainer)
{
if (this.bookmarkContainers == null)
return;
this.bookmarkContainers.Add(newContainer);
this.showCreateContainerForm = false;
await SetSelectedContainer(newContainer.BookmarkContainerId);
}
// Save the currently selected container in LocalStorage so that the same container remains
// selected between new tabs
protected async Task<int> GetSelectedContainerId()
{
bool hasValue = await localStorage.ContainKeyAsync("SelectedContainer");
if (hasValue)
return await localStorage.GetItemAsync<int>("SelectedContainer");
// Default to the first container
int firstContainer = this.bookmarkContainers!.First().BookmarkContainerId;
await this.SetSelectedContainer(firstContainer);
return firstContainer;
}
protected async Task SetSelectedContainer(int selectedContainerId)
{
await localStorage.SetItemAsync<int>("SelectedContainer", selectedContainerId);
}
}

View file

@ -1,32 +1,30 @@
using System; using System;
using System.Net.Http; using System.Net.Http;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Text;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication; using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Blazored.LocalStorage;
namespace Start.Client namespace Start.Client {
{ public class Program {
public class Program public static async Task Main(string[] args) {
{ var builder = WebAssemblyHostBuilder.CreateDefault(args);
public static async Task Main(string[] args) builder.RootComponents.Add<App>("#app");
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddHttpClient("Start.ServerAPI", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)) builder.Services.AddHttpClient("Start.ServerAPI",
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>(); client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
// Supply HttpClient instances that include access tokens when making requests to the server project // Supply HttpClient instances that include access tokens when making requests to the server project
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("Start.ServerAPI")); builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
.CreateClient("Start.ServerAPI"));
builder.Services.AddApiAuthorization(); builder.Services.AddApiAuthorization();
await builder.Build().RunAsync(); builder.Services.AddBlazoredLocalStorage();
}
} await builder.Build().RunAsync();
}
}
} }

View file

@ -22,6 +22,11 @@
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
</NavLink> </NavLink>
</li> </li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="Start">
<span class="oi oi-star"></span> Start
</NavLink>
</li>
</ul> </ul>
</div> </div>

View file

@ -16,6 +16,7 @@
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="5.0.11" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="5.0.11" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="System.Net.Http.Json" Version="5.0.0" /> <PackageReference Include="System.Net.Http.Json" Version="5.0.0" />
<PackageReference Include="Blazored.LocalStorage" Version="4.1.5" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -26,4 +27,10 @@
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" /> <ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Remove="Blazored.LocalStorage" />
</ItemGroup>
<ItemGroup>
<Content Remove="wwwroot\css\Spectre\" />
</ItemGroup>
</Project> </Project>

View file

@ -1,50 +1,54 @@
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); @import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
html, body { html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
} }
a, .btn-link { a, .btn-link {
color: #0366d6; color: #0366d6;
} }
.btn-primary { .btn-primary {
color: #fff; color: #fff;
background-color: #1b6ec2; background-color: #1b6ec2;
border-color: #1861ac; border-color: #1861ac;
} }
.content { .content {
padding-top: 1.1rem; padding-top: 1.1rem;
} }
.valid.modified:not([type=checkbox]) { .valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050; outline: 1px solid #26b050;
} }
.invalid { .invalid {
outline: 1px solid red; outline: 1px solid red;
} }
.validation-message { .validation-message {
color: red; color: red;
} }
#blazor-error-ui { #blazor-error-ui {
background: lightyellow; background: lightyellow;
bottom: 0; bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none; display: none;
left: 0; left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem; padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed; position: fixed;
width: 100%; width: 100%;
z-index: 1000; z-index: 1000;
} }
#blazor-error-ui .dismiss { #blazor-error-ui .dismiss {
cursor: pointer; cursor: pointer;
position: absolute; position: absolute;
right: 0.75rem; right: 0.75rem;
top: 0.5rem; top: 0.5rem;
} }
.container {
padding: 0.4rem;
}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,597 @@
/*! Spectre.css Icons v0.5.9 | MIT License | github.com/picturepan2/spectre */
.icon {
box-sizing: border-box;
display: inline-block;
font-size: inherit;
font-style: normal;
height: 1em;
position: relative;
text-indent: -9999px;
vertical-align: middle;
width: 1em;
}
.icon::before,
.icon::after {
content: "";
display: block;
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
}
.icon.icon-2x {
font-size: 1.6rem;
}
.icon.icon-3x {
font-size: 2.4rem;
}
.icon.icon-4x {
font-size: 3.2rem;
}
.accordion .icon,
.btn .icon,
.toast .icon,
.menu .icon {
vertical-align: -10%;
}
.btn-lg .icon {
vertical-align: -15%;
}
.icon-arrow-down::before,
.icon-arrow-left::before,
.icon-arrow-right::before,
.icon-arrow-up::before,
.icon-downward::before,
.icon-back::before,
.icon-forward::before,
.icon-upward::before {
border: .1rem solid currentColor;
border-bottom: 0;
border-right: 0;
height: .65em;
width: .65em;
}
.icon-arrow-down::before {
transform: translate(-50%, -75%) rotate(225deg);
}
.icon-arrow-left::before {
transform: translate(-25%, -50%) rotate(-45deg);
}
.icon-arrow-right::before {
transform: translate(-75%, -50%) rotate(135deg);
}
.icon-arrow-up::before {
transform: translate(-50%, -25%) rotate(45deg);
}
.icon-back::after,
.icon-forward::after {
background: currentColor;
height: .1rem;
width: .8em;
}
.icon-downward::after,
.icon-upward::after {
background: currentColor;
height: .8em;
width: .1rem;
}
.icon-back::after {
left: 55%;
}
.icon-back::before {
transform: translate(-50%, -50%) rotate(-45deg);
}
.icon-downward::after {
top: 45%;
}
.icon-downward::before {
transform: translate(-50%, -50%) rotate(-135deg);
}
.icon-forward::after {
left: 45%;
}
.icon-forward::before {
transform: translate(-50%, -50%) rotate(135deg);
}
.icon-upward::after {
top: 55%;
}
.icon-upward::before {
transform: translate(-50%, -50%) rotate(45deg);
}
.icon-caret::before {
border-left: .3em solid transparent;
border-right: .3em solid transparent;
border-top: .3em solid currentColor;
height: 0;
transform: translate(-50%, -25%);
width: 0;
}
.icon-menu::before {
background: currentColor;
box-shadow: 0 -.35em, 0 .35em;
height: .1rem;
width: 100%;
}
.icon-apps::before {
background: currentColor;
box-shadow: -.35em -.35em, -.35em 0, -.35em .35em, 0 -.35em, 0 .35em, .35em -.35em, .35em 0, .35em .35em;
height: 3px;
width: 3px;
}
.icon-resize-horiz::before,
.icon-resize-horiz::after,
.icon-resize-vert::before,
.icon-resize-vert::after {
border: .1rem solid currentColor;
border-bottom: 0;
border-right: 0;
height: .45em;
width: .45em;
}
.icon-resize-horiz::before,
.icon-resize-vert::before {
transform: translate(-50%, -90%) rotate(45deg);
}
.icon-resize-horiz::after,
.icon-resize-vert::after {
transform: translate(-50%, -10%) rotate(225deg);
}
.icon-resize-horiz::before {
transform: translate(-90%, -50%) rotate(-45deg);
}
.icon-resize-horiz::after {
transform: translate(-10%, -50%) rotate(135deg);
}
.icon-more-horiz::before,
.icon-more-vert::before {
background: currentColor;
border-radius: 50%;
box-shadow: -.4em 0, .4em 0;
height: 3px;
width: 3px;
}
.icon-more-vert::before {
box-shadow: 0 -.4em, 0 .4em;
}
.icon-plus::before,
.icon-minus::before,
.icon-cross::before {
background: currentColor;
height: .1rem;
width: 100%;
}
.icon-plus::after,
.icon-cross::after {
background: currentColor;
height: 100%;
width: .1rem;
}
.icon-cross::before {
width: 100%;
}
.icon-cross::after {
height: 100%;
}
.icon-cross::before,
.icon-cross::after {
transform: translate(-50%, -50%) rotate(45deg);
}
.icon-check::before {
border: .1rem solid currentColor;
border-right: 0;
border-top: 0;
height: .5em;
transform: translate(-50%, -75%) rotate(-45deg);
width: .9em;
}
.icon-stop {
border: .1rem solid currentColor;
border-radius: 50%;
}
.icon-stop::before {
background: currentColor;
height: .1rem;
transform: translate(-50%, -50%) rotate(45deg);
width: 1em;
}
.icon-shutdown {
border: .1rem solid currentColor;
border-radius: 50%;
border-top-color: transparent;
}
.icon-shutdown::before {
background: currentColor;
content: "";
height: .5em;
top: .1em;
width: .1rem;
}
.icon-refresh::before {
border: .1rem solid currentColor;
border-radius: 50%;
border-right-color: transparent;
height: 1em;
width: 1em;
}
.icon-refresh::after {
border: .2em solid currentColor;
border-left-color: transparent;
border-top-color: transparent;
height: 0;
left: 80%;
top: 20%;
width: 0;
}
.icon-search::before {
border: .1rem solid currentColor;
border-radius: 50%;
height: .75em;
left: 5%;
top: 5%;
transform: translate(0, 0) rotate(45deg);
width: .75em;
}
.icon-search::after {
background: currentColor;
height: .1rem;
left: 80%;
top: 80%;
transform: translate(-50%, -50%) rotate(45deg);
width: .4em;
}
.icon-edit::before {
border: .1rem solid currentColor;
height: .4em;
transform: translate(-40%, -60%) rotate(-45deg);
width: .85em;
}
.icon-edit::after {
border: .15em solid currentColor;
border-right-color: transparent;
border-top-color: transparent;
height: 0;
left: 5%;
top: 95%;
transform: translate(0, -100%);
width: 0;
}
.icon-delete::before {
border: .1rem solid currentColor;
border-bottom-left-radius: .1rem;
border-bottom-right-radius: .1rem;
border-top: 0;
height: .75em;
top: 60%;
width: .75em;
}
.icon-delete::after {
background: currentColor;
box-shadow: -.25em .2em, .25em .2em;
height: .1rem;
top: .05rem;
width: .5em;
}
.icon-share {
border: .1rem solid currentColor;
border-radius: .1rem;
border-right: 0;
border-top: 0;
}
.icon-share::before {
border: .1rem solid currentColor;
border-left: 0;
border-top: 0;
height: .4em;
left: 100%;
top: .25em;
transform: translate(-125%, -50%) rotate(-45deg);
width: .4em;
}
.icon-share::after {
border: .1rem solid currentColor;
border-bottom: 0;
border-radius: 75% 0;
border-right: 0;
height: .5em;
width: .6em;
}
.icon-flag::before {
background: currentColor;
height: 1em;
left: 15%;
width: .1rem;
}
.icon-flag::after {
border: .1rem solid currentColor;
border-bottom-right-radius: .1rem;
border-left: 0;
border-top-right-radius: .1rem;
height: .65em;
left: 60%;
top: 35%;
width: .8em;
}
.icon-bookmark::before {
border: .1rem solid currentColor;
border-bottom: 0;
border-top-left-radius: .1rem;
border-top-right-radius: .1rem;
height: .9em;
width: .8em;
}
.icon-bookmark::after {
border: .1rem solid currentColor;
border-bottom: 0;
border-left: 0;
border-radius: .1rem;
height: .5em;
transform: translate(-50%, 35%) rotate(-45deg) skew(15deg, 15deg);
width: .5em;
}
.icon-download,
.icon-upload {
border-bottom: .1rem solid currentColor;
}
.icon-download::before,
.icon-upload::before {
border: .1rem solid currentColor;
border-bottom: 0;
border-right: 0;
height: .5em;
transform: translate(-50%, -60%) rotate(-135deg);
width: .5em;
}
.icon-download::after,
.icon-upload::after {
background: currentColor;
height: .6em;
top: 40%;
width: .1rem;
}
.icon-upload::before {
transform: translate(-50%, -60%) rotate(45deg);
}
.icon-upload::after {
top: 50%;
}
.icon-copy::before {
border: .1rem solid currentColor;
border-bottom: 0;
border-radius: .1rem;
border-right: 0;
height: .8em;
left: 40%;
top: 35%;
width: .8em;
}
.icon-copy::after {
border: .1rem solid currentColor;
border-radius: .1rem;
height: .8em;
left: 60%;
top: 60%;
width: .8em;
}
.icon-time {
border: .1rem solid currentColor;
border-radius: 50%;
}
.icon-time::before {
background: currentColor;
height: .4em;
transform: translate(-50%, -75%);
width: .1rem;
}
.icon-time::after {
background: currentColor;
height: .3em;
transform: translate(-50%, -75%) rotate(90deg);
transform-origin: 50% 90%;
width: .1rem;
}
.icon-mail::before {
border: .1rem solid currentColor;
border-radius: .1rem;
height: .8em;
width: 1em;
}
.icon-mail::after {
border: .1rem solid currentColor;
border-right: 0;
border-top: 0;
height: .5em;
transform: translate(-50%, -90%) rotate(-45deg) skew(10deg, 10deg);
width: .5em;
}
.icon-people::before {
border: .1rem solid currentColor;
border-radius: 50%;
height: .45em;
top: 25%;
width: .45em;
}
.icon-people::after {
border: .1rem solid currentColor;
border-radius: 50% 50% 0 0;
height: .4em;
top: 75%;
width: .9em;
}
.icon-message {
border: .1rem solid currentColor;
border-bottom: 0;
border-radius: .1rem;
border-right: 0;
}
.icon-message::before {
border: .1rem solid currentColor;
border-bottom-right-radius: .1rem;
border-left: 0;
border-top: 0;
height: .8em;
left: 65%;
top: 40%;
width: .7em;
}
.icon-message::after {
background: currentColor;
border-radius: .1rem;
height: .3em;
left: 10%;
top: 100%;
transform: translate(0, -90%) rotate(45deg);
width: .1rem;
}
.icon-photo {
border: .1rem solid currentColor;
border-radius: .1rem;
}
.icon-photo::before {
border: .1rem solid currentColor;
border-radius: 50%;
height: .25em;
left: 35%;
top: 35%;
width: .25em;
}
.icon-photo::after {
border: .1rem solid currentColor;
border-bottom: 0;
border-left: 0;
height: .5em;
left: 60%;
transform: translate(-50%, 25%) rotate(-45deg);
width: .5em;
}
.icon-link::before,
.icon-link::after {
border: .1rem solid currentColor;
border-radius: 5em 0 0 5em;
border-right: 0;
height: .5em;
width: .75em;
}
.icon-link::before {
transform: translate(-70%, -45%) rotate(-45deg);
}
.icon-link::after {
transform: translate(-30%, -55%) rotate(135deg);
}
.icon-location::before {
border: .1rem solid currentColor;
border-radius: 50% 50% 50% 0;
height: .8em;
transform: translate(-50%, -60%) rotate(-45deg);
width: .8em;
}
.icon-location::after {
border: .1rem solid currentColor;
border-radius: 50%;
height: .2em;
transform: translate(-50%, -80%);
width: .2em;
}
.icon-emoji {
border: .1rem solid currentColor;
border-radius: 50%;
}
.icon-emoji::before {
border-radius: 50%;
box-shadow: -.17em -.1em, .17em -.1em;
height: .15em;
width: .15em;
}
.icon-emoji::after {
border: .1rem solid currentColor;
border-bottom-color: transparent;
border-radius: 50%;
border-right-color: transparent;
height: .5em;
transform: translate(-50%, -40%) rotate(-135deg);
width: .5em;
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -6,7 +6,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>Start</title> <title>Start</title>
<base href="/" /> <base href="/" />
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" /> <!--<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />-->
<link href="css/spectre/spectre.min.css" rel="stylesheet" />
<link href="css/spectre/spectre-icons.css" rel="stylesheet" />
<link href="css/app.css" rel="stylesheet" /> <link href="css/app.css" rel="stylesheet" />
<link href="Start.Client.styles.css" rel="stylesheet" /> <link href="Start.Client.styles.css" rel="stylesheet" />
<link href="manifest.json" rel="manifest" /> <link href="manifest.json" rel="manifest" />

View file

@ -9,41 +9,39 @@ using Start.Shared;
namespace Start.Server.Controllers { namespace Start.Server.Controllers {
[Authorize] [Authorize]
[ApiController] [ApiController]
[Route("[controller]")] [Route("[controller]/[action]")]
public class BookmarksController : ControllerBase { public class BookmarksController : ControllerBase {
private readonly IBookmarkContainerService bookmarkContainerService; private readonly IBookmarkContainerService bookmarkContainerService;
private readonly IBookmarkGroupService bookmarkGroupService; private readonly IBookmarkGroupService bookmarkGroupService;
private readonly IBookmarkService bookmarkService; private readonly IBookmarkService bookmarkService;
private readonly string userId;
public BookmarksController(IBookmarkContainerService bookmarkContainerService, public BookmarksController(IBookmarkContainerService bookmarkContainerService,
IBookmarkGroupService bookmarkGroupService, IBookmarkService bookmarkService) { IBookmarkGroupService bookmarkGroupService, IBookmarkService bookmarkService) {
this.bookmarkContainerService = bookmarkContainerService; this.bookmarkContainerService = bookmarkContainerService;
this.bookmarkGroupService = bookmarkGroupService; this.bookmarkGroupService = bookmarkGroupService;
this.bookmarkService = bookmarkService; this.bookmarkService = bookmarkService;
this.userId = this.GetAuthorizedUserId();
} }
[HttpGet] [HttpGet]
public IList<BookmarkContainerDto> GetAllBookmarkContainers() { public IList<BookmarkContainerDto> GetAllBookmarkContainers() {
return this.bookmarkContainerService.GetUserBookmarkContainers(this.userId) return this.bookmarkContainerService.GetUserBookmarkContainers(this.GetAuthorizedUserId())
.Select(bc => bc.MapToDto()) .Select(bc => bc.MapToDto())
.ToList(); .ToList();
} }
[HttpGet] [HttpGet]
[Route("{bookmarkContainerId}")]
public BookmarkContainerDto? GetBookmarkContainer(int bookmarkContainerId) { public BookmarkContainerDto? GetBookmarkContainer(int bookmarkContainerId) {
return this.bookmarkContainerService return this.bookmarkContainerService
.GetBookmarkContainer(this.userId, bookmarkContainerId, true, true) .GetBookmarkContainer(this.GetAuthorizedUserId(), bookmarkContainerId, true, true)
?.MapToDto(); ?.MapToDto();
} }
[HttpGet] [HttpGet]
[Route("{bookmarkId}")]
public BookmarkDto? GetBookmark(int bookmarkId) { public BookmarkDto? GetBookmark(int bookmarkId) {
return this.bookmarkService return this.bookmarkService
.GetBookmark(this.userId, bookmarkId) .GetBookmark(this.GetAuthorizedUserId(), bookmarkId)
?.MapToDto(); ?.MapToDto();
} }
@ -51,15 +49,21 @@ namespace Start.Server.Controllers {
public BookmarkDto? CreateBookmark(string title, string url, string? notes, public BookmarkDto? CreateBookmark(string title, string url, string? notes,
int bookmarkGroupId) { int bookmarkGroupId) {
return this.bookmarkService return this.bookmarkService
.CreateBookmark(this.userId, title, url, notes, bookmarkGroupId) .CreateBookmark(this.GetAuthorizedUserId(), title, url, notes, bookmarkGroupId)
?.MapToDto(); ?.MapToDto();
} }
[HttpPost] [HttpPost]
public BookmarkContainerDto? CreateBookmarkContainer(string title) { public BookmarkContainerDto? CreateBookmarkContainer([FromBody] string title) {
return this.bookmarkContainerService return this.bookmarkContainerService
.CreateBookmarkContainer(this.userId, title) .CreateBookmarkContainer(this.GetAuthorizedUserId(), title)
?.MapToDto(); ?.MapToDto();
} }
[HttpDelete]
public bool DeleteBookmarkContainer(int bookmarkContainerId) {
return this.bookmarkContainerService
.DeleteBookmarkContainer(this.GetAuthorizedUserId(), bookmarkContainerId);
}
} }
} }

View file

@ -50,6 +50,7 @@ namespace Start.Server.Data.Services {
BookmarkContainer newContainer = new(userId, title); BookmarkContainer newContainer = new(userId, title);
this.db.BookmarkContainers.Add(newContainer); this.db.BookmarkContainers.Add(newContainer);
this.db.SaveChanges();
return newContainer; return newContainer;
} }

Binary file not shown.

View file

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
namespace Start.Shared { namespace Start.Shared {
public class BookmarkContainerDto { public class BookmarkContainerDto {
@ -18,6 +19,7 @@ namespace Start.Shared {
this.BookmarkContainerId = bookmarkContainerId; this.BookmarkContainerId = bookmarkContainerId;
} }
[JsonConstructor]
public BookmarkContainerDto(int bookmarkContainerId, string title, public BookmarkContainerDto(int bookmarkContainerId, string title,
IList<BookmarkGroupDto>? bookmarkGroups) : this(bookmarkContainerId, title) { IList<BookmarkGroupDto>? bookmarkGroups) : this(bookmarkContainerId, title) {
this.BookmarkGroups = bookmarkGroups; this.BookmarkGroups = bookmarkGroups;