Initial commit

This commit is contained in:
2023-07-23 13:34:00 +01:00
commit 967c16b6bf
65 changed files with 2868 additions and 0 deletions

View File

@ -0,0 +1,32 @@
namespace Groceries.Common;
using Microsoft.AspNetCore.Mvc;
public static class TurboControllerExtensions
{
public static TurboStreamResult TurboStream(
this Controller controller,
TurboStreamAction action,
string target,
object? model)
{
return controller.TurboStream(action, target, null, model);
}
public static TurboStreamResult TurboStream(
this Controller controller,
TurboStreamAction action,
string target,
string? viewName,
object? model)
{
controller.ViewData.Model = model;
return new TurboStreamResult(action, target)
{
ViewName = viewName,
ViewData = controller.ViewData,
TempData = controller.TempData,
};
}
}

View File

@ -0,0 +1,19 @@
namespace Groceries.Common;
public static class TurboHttpRequestExtensions
{
public static bool IsTurboFrameRequest(this HttpRequest request)
{
return request.Headers.ContainsKey("Turbo-Frame");
}
public static bool IsTurboFrameRequest(this HttpRequest request, string frameId)
{
return request.Headers.TryGetValue("Turbo-Frame", out var values) && values.Contains(frameId);
}
public static bool AcceptsTurboStream(this HttpRequest request)
{
return request.GetTypedHeaders().Accept.Any(value => value.MediaType == "text/vnd.turbo-stream.html");
}
}

View File

@ -0,0 +1,182 @@
namespace Groceries.Common;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.Options;
using System.Diagnostics;
using System.Globalization;
public enum TurboStreamAction
{
Append,
Prepend,
Replace,
Update,
Remove,
Before,
After,
}
public class TurboStreamResult : ActionResult, IStatusCodeActionResult
{
public TurboStreamResult(TurboStreamAction action, string target)
{
Action = action;
Target = target;
}
public TurboStreamAction Action { get; set; }
public string Target { get; set; }
public string ContentType => "text/vnd.turbo-stream.html";
/// <summary>
/// Gets or sets the HTTP status code.
/// </summary>
public int? StatusCode { get; set; }
/// <summary>
/// Gets or sets the name or path of the partial view that is rendered to the response.
/// </summary>
/// <remarks>
/// When <c>null</c>, defaults to <see cref="ControllerActionDescriptor.ActionName"/>.
/// </remarks>
public string? ViewName { get; set; }
/// <summary>
/// Gets the view data model.
/// </summary>
public object? Model => ViewData.Model;
/// <summary>
/// Gets or sets the <see cref="ViewDataDictionary"/> for this result.
/// </summary>
public ViewDataDictionary ViewData { get; set; } = null!;
/// <summary>
/// Gets or sets the <see cref="ITempDataDictionary"/> for this result.
/// </summary>
public ITempDataDictionary TempData { get; set; } = null!;
/// <summary>
/// Gets or sets the <see cref="IViewEngine"/> used to locate views.
/// </summary>
/// <remarks>
/// When <c>null</c>, an instance of <see cref="ICompositeViewEngine"/> from
/// <c>ActionContext.HttpContext.RequestServices</c> is used.
/// </remarks>
public IViewEngine? ViewEngine { get; set; }
/// <inheritdoc/>
public override Task ExecuteResultAsync(ActionContext context)
{
var services = context.HttpContext.RequestServices;
var executor = services.GetRequiredService<IActionResultExecutor<TurboStreamResult>>();
return executor.ExecuteAsync(context, this);
}
}
public class TurboStreamResultExecutor : PartialViewResultExecutor, IActionResultExecutor<TurboStreamResult>
{
public TurboStreamResultExecutor(
IOptions<MvcViewOptions> viewOptions,
IHttpResponseStreamWriterFactory writerFactory,
ICompositeViewEngine viewEngine,
ITempDataDictionaryFactory tempDataFactory,
DiagnosticListener diagnosticListener,
ILoggerFactory loggerFactory,
IModelMetadataProvider modelMetadataProvider)
: base(viewOptions, writerFactory, viewEngine, tempDataFactory, diagnosticListener, loggerFactory, modelMetadataProvider)
{
}
/// <inheritdoc/>
public Task ExecuteAsync(ActionContext context, TurboStreamResult result)
{
var viewEngine = result.ViewEngine ?? ViewEngine;
var viewName = result.ViewName ?? GetActionName(context)!;
var viewEngineResult = viewEngine.GetView(executingFilePath: null, viewPath: viewName, isMainPage: false);
var originalViewEngineResult = viewEngineResult;
if (!viewEngineResult.Success)
{
viewEngineResult = viewEngine.FindView(context, viewName, isMainPage: false);
}
viewEngineResult.EnsureSuccessful(originalViewEngineResult.SearchedLocations);
var action = result.Action.ToString().ToLowerInvariant();
var preContent = result.Action switch
{
TurboStreamAction.Remove => $"<turbo-stream action=\"{action}\">\n",
_ => $"<turbo-stream action=\"{action}\" target=\"{result.Target}\">\n<template>\n",
};
var postContent = result.Action switch
{
TurboStreamAction.Remove => "</turbo-stream>",
_ => "</template>\n</turbo-stream>",
};
result.ViewData["RenderingToTurboStream"] = true;
using var view = new WrapperView(viewEngineResult.View, preContent, postContent);
return ExecuteAsync(context, view, result.ViewData, result.TempData, result.ContentType, result.StatusCode);
}
private static string? GetActionName(ActionContext context)
{
const string actionNameKey = "action";
if (!context.RouteData.Values.TryGetValue(actionNameKey, out var routeValue))
{
return null;
}
string? normalizedValue = null;
if (context.ActionDescriptor.RouteValues.TryGetValue(actionNameKey, out var value) && !string.IsNullOrEmpty(value))
{
normalizedValue = value;
}
var stringRouteValue = Convert.ToString(routeValue, CultureInfo.InvariantCulture);
if (string.Equals(normalizedValue, stringRouteValue, StringComparison.OrdinalIgnoreCase))
{
return normalizedValue;
}
return stringRouteValue;
}
}
public sealed class WrapperView : IDisposable, IView
{
private readonly IView innerView;
private readonly string preContent;
private readonly string postContent;
public WrapperView(IView innerView, string preContent, string postContent)
{
this.innerView = innerView;
this.preContent = preContent;
this.postContent = postContent;
}
public string Path => string.Empty;
public void Dispose()
{
(innerView as IDisposable)?.Dispose();
}
public async Task RenderAsync(ViewContext context)
{
context.Writer.Write(preContent);
await innerView.RenderAsync(context);
context.Writer.Write(postContent);
}
}

View File

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="color-scheme" content="light dark" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Groceries@(ViewBag.Title != null ? Html.Raw($" &ndash; {ViewBag.Title}") : "")</title>
<link rel="stylesheet" type="text/css" href="/css/main.css" asp-append-version="true" data-turbo-track="reload" />
<script type="module" src="/js/main.js" asp-append-version="true" data-turbo-track="reload"></script>
<script type="module" src="/lib/hotwired/turbo/dist/turbo.es2017-esm.js"></script>
@RenderSection("head", required: false)
</head>
<body>
<partial name="_LayoutSidebar" />
<main class="main-content">
@*<turbo-frame id="main" target="_top">*@
@RenderBody()
@*</turbo-frame>*@
</main>
<dialog class="modal" data-controller="modal" data-action="turbo:frame-load->modal#open turbo:before-cache@document->modal#close popstate@window->modal#close">
<turbo-frame id="modal" data-modal-target="frame"></turbo-frame>
</dialog>
</body>
</html>

View File

@ -0,0 +1,53 @@
@{
var controller = ViewContext.RouteData.Values["controller"]?.ToString() ?? string.Empty;
}
<input class="sidebar__toggle" id="sidebarToggle" type="checkbox" role="button" data-turbo-permanent />
<label for="sidebarToggle">Menu</label>
<section class="sidebar">
<header class="sidebar__header">
Groceries
</header>
<nav class="sidebar__body">
<ul>
<li class="sidebar__item @(controller == "Home" ? "sidebar__item--active" : "")">
@* dashboard squares icon *@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 5v2h-4V5h4M9 5v6H5V5h4m10 8v6h-4v-6h4M9 17v2H5v-2h4M21 3h-8v6h8V3zM11 3H3v10h8V3zm10 8h-8v10h8V11zm-10 4H3v6h8v-6z"/></svg>
<a class="sidebar__link" asp-controller="Home" asp-action="Index">Dashboard</a>
</li>
<li class="sidebar__item @(controller == "Lists" ? "sidebar__item--active" : "")">
@* receipt long icon *@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24"><path d="M0,0h24v24H0V0z" fill="none"/><g><path d="M19.5,3.5L18,2l-1.5,1.5L15,2l-1.5,1.5L12,2l-1.5,1.5L9,2L7.5,3.5L6,2v14H3v3c0,1.66,1.34,3,3,3h12c1.66,0,3-1.34,3-3V2 L19.5,3.5z M15,20H6c-0.55,0-1-0.45-1-1v-1h10V20z M19,19c0,0.55-0.45,1-1,1s-1-0.45-1-1v-3H8V5h11V19z"/><rect height="2" width="6" x="9" y="7"/><rect height="2" width="2" x="16" y="7"/><rect height="2" width="6" x="9" y="10"/><rect height="2" width="2" x="16" y="10"/></g></svg>
<a class="sidebar__link" asp-controller="Lists" asp-action="Index" asp-route-page="1">Lists</a>
</li>
<li class="sidebar__item @(controller == "Transactions" ? "sidebar__item--active" : "")">
@* shopping cart icon *@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M15.55 13c.75 0 1.41-.41 1.75-1.03l3.58-6.49c.37-.66-.11-1.48-.87-1.48H5.21l-.94-2H1v2h2l3.6 7.59-1.35 2.44C4.52 15.37 5.48 17 7 17h12v-2H7l1.1-2h7.45zM6.16 6h12.15l-2.76 5H8.53L6.16 6zM7 18c-1.1 0-1.99.9-1.99 2S5.9 22 7 22s2-.9 2-2-.9-2-2-2zm10 0c-1.1 0-1.99.9-1.99 2s.89 2 1.99 2 2-.9 2-2-.9-2-2-2z"/></svg>
<a class="sidebar__link" asp-controller="Transactions" asp-action="Index" asp-route-page="1">Transactions</a>
</li>
<li class="sidebar__item @(controller == "Items" ? "sidebar__item--active" : "")">
@* category shapes icon *@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 2l-5.5 9h11L12 2zm0 3.84L13.93 9h-3.87L12 5.84zM17.5 13c-2.49 0-4.5 2.01-4.5 4.5s2.01 4.5 4.5 4.5 4.5-2.01 4.5-4.5-2.01-4.5-4.5-4.5zm0 7c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5zM3 21.5h8v-8H3v8zm2-6h4v4H5v-4z"/></svg>
<a class="sidebar__link" asp-controller="Items" asp-action="Index" asp-route-page="1">Items</a>
</li>
<li class="sidebar__item @(controller == "Stores" ? "sidebar__item--active" : "")">
@* store building icon *@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M18.36 9l.6 3H5.04l.6-3h12.72M20 4H4v2h16V4zm0 3H4l-1 5v2h1v6h10v-6h4v6h2v-6h1v-2l-1-5zM6 18v-4h6v4H6z"/></svg>
<a class="sidebar__link" asp-controller="Stores" asp-action="Index" asp-route-page="1">Stores</a>
</li>
</ul>
</nav>
@*<div class="slide-toggle">
<label class="slide-toggle__option">
<svg class="icon icon--sm" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24"><rect fill="none" height="24" width="24"/><path d="M12,9c1.65,0,3,1.35,3,3s-1.35,3-3,3s-3-1.35-3-3S10.35,9,12,9 M12,7c-2.76,0-5,2.24-5,5s2.24,5,5,5s5-2.24,5-5 S14.76,7,12,7L12,7z M2,13l2,0c0.55,0,1-0.45,1-1s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S1.45,13,2,13z M20,13l2,0c0.55,0,1-0.45,1-1 s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S19.45,13,20,13z M11,2v2c0,0.55,0.45,1,1,1s1-0.45,1-1V2c0-0.55-0.45-1-1-1S11,1.45,11,2z M11,20v2c0,0.55,0.45,1,1,1s1-0.45,1-1v-2c0-0.55-0.45-1-1-1C11.45,19,11,19.45,11,20z M5.99,4.58c-0.39-0.39-1.03-0.39-1.41,0 c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0s0.39-1.03,0-1.41L5.99,4.58z M18.36,16.95 c-0.39-0.39-1.03-0.39-1.41,0c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0c0.39-0.39,0.39-1.03,0-1.41 L18.36,16.95z M19.42,5.99c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06c-0.39,0.39-0.39,1.03,0,1.41 s1.03,0.39,1.41,0L19.42,5.99z M7.05,18.36c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06 c-0.39,0.39-0.39,1.03,0,1.41s1.03,0.39,1.41,0L7.05,18.36z"/></svg>
Light
<input class="slide-toggle__control" type="radio" name="colorScheme" value="light" />
</label>
<label class="slide-toggle__option">
<svg class="icon icon--sm" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24"><rect fill="none" height="24" width="24"/><path d="M9.37,5.51C9.19,6.15,9.1,6.82,9.1,7.5c0,4.08,3.32,7.4,7.4,7.4c0.68,0,1.35-0.09,1.99-0.27C17.45,17.19,14.93,19,12,19 c-3.86,0-7-3.14-7-7C5,9.07,6.81,6.55,9.37,5.51z M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36 c-0.98,1.37-2.58,2.26-4.4,2.26c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"/></svg>
Dark
<input class="slide-toggle__control" type="radio" name="colorScheme" value="dark" />
</label>
</div>*@
</section>

View File

@ -0,0 +1,9 @@
<turbo-frame id="modal" data-modal-target="frame">
<article class="card">
<header class="card__header row">
<h2 class="row__fill">@ViewBag.Title</h2>
<button class="button modal__close-button" data-action="modal#close">&#x1F5D9;</button>
</header>
@RenderBody()
</article>
</turbo-frame>

View File

@ -0,0 +1,33 @@
@model IListPageModel
@{
var routeData = new Dictionary<string, string>(
ViewContext.RouteData.Values
.Where(data => data.Value != null)
.Select(data => KeyValuePair.Create(data.Key, (string)data.Value!))
.Concat(Context.Request.Query.Select(param => KeyValuePair.Create(param.Key, (string)param.Value!))));
}
<div class="table__paginator">
<span>
Showing @(Model!.Offset + 1) to @(Model.Offset + Model.Count) of @Model.Total results
</span>
<nav class="button-group">
@if (Model.Page == 1)
{
<span class="link link--disabled">Previous</span>
}
else
{
<a class="link" asp-all-route-data="routeData" asp-route-page="@(Model.Page - 1)">Previous</a>
}
@if (Model.Page == Model.LastPage)
{
<span class="link link--disabled">Next</span>
}
else
{
<a class="link" asp-all-route-data="routeData" asp-route-page="@(Model.Page + 1)">Next</a>
}
</nav>
</div>

View File

@ -0,0 +1,12 @@
<turbo-stream action="@ViewBag.Action" target="@ViewBag.Target">
@if (ViewBag.Action != "remove")
{
<template>
@RenderBody()
</template>
}
else
{
IgnoreBody();
}
</turbo-stream>